import fs from 'node:fs'; import path from 'node:path'; import { parse } from 'svelte/compiler'; /** * Resolves the framework's own public components (`mochi-framework/components`) to the on-disk `.svelte` files they * re-export, bridging the barrel's named exports to the relative paths the island preprocessor understands — so a * `mochi:*` directive can sit straight on a package import (``). * * The mapping is read from the actual barrel next to this file rather than a hardcoded list, so it can't drift from what * a given `mochi-framework` version ships, in-repo through a workspace symlink or from a consumer's `node_modules`. */ /** The specifier a directive may target to reach a framework component. */ export const FRAMEWORK_COMPONENTS_SPECIFIER = 'mochi-framework/components'; // This file lives in `src/compiler/`; the barrel is `src/components/index.ts`. const BARREL_PATH = path.join(path.dirname(Bun.fileURLToPath(import.meta.url)), '..', 'components', 'index.ts'); export interface FrameworkComponent { /** Absolute path to the underlying `.svelte`/`.md`/`.svx` file. */ resolvedPath: string; /** Export the underlying module is imported from (`'default'` for `export { default as X }`). */ exportName: string; } // The barrel is static within a process (the framework doesn't change under a // running build), so parse it once and reuse. let cache: Map | null = null; /** An export/import specifier name is an Identifier or, rarely, a string literal. */ function specifierName(node: { type: string; name?: string; value?: unknown }): string { return node.type === 'Identifier' ? node.name! : String(node.value); } function parseBarrel(): Map { const map = new Map(); let source: string; try { source = fs.readFileSync(BARREL_PATH, 'utf8'); } catch { return map; } const barrelDir = path.dirname(BARREL_PATH); // Svelte's own acorn-typescript parser yields a real AST where pattern-matching the source would guess: wrapping the // barrel in a module script makes its `export { … } from '…'` re-exports parse as `ExportNamedDeclaration` nodes. Per // specifier, `local` is the export name in the source file and `exported` the name the barrel exposes. const ast = parse(``, { modern: true }); for (const node of ast.module?.content.body ?? []) { if (node.type !== 'ExportNamedDeclaration' || !node.source || typeof node.source.value !== 'string') { continue; } if (!/\.(svelte|md|svx)$/.test(node.source.value)) { continue; } const resolvedPath = path.resolve(barrelDir, node.source.value); for (const spec of node.specifiers) { if (spec.type !== 'ExportSpecifier') { continue; } map.set(specifierName(spec.exported), { resolvedPath, exportName: specifierName(spec.local) }); } } return map; } /** Resolve a `mochi-framework/components` named export to its underlying file, or `null` for a typo or non-component export, which falls through to the usual unresolved-island compile error. */ export function resolveFrameworkComponent(exportName: string): FrameworkComponent | null { cache ??= parseBarrel(); return cache.get(exportName) ?? null; }