// Non-storybook `package` adapter. Bundles dist/ when present (the authoritative // component list comes from shipped .d.ts; with no dist it synthesizes an // entry from src/ as a last resort) and opportunistically enriches each // component from src/ — JSDoc and dir-derived group. Every enrichment miss // degrades to the plain-dist behaviour. // // Discovery is heuristic-based; each heuristic has a `.design-sync/config.json` // override (ASSUMPTION comments below name them) so repos that don't match the // defaults write config, not code. `componentSrcMap` is the single override // knob for component inclusion: non-null value = add/pin src path, null = // exclude a .d.ts-exported internal. import { existsSync, writeFileSync, readFileSync } from 'node:fs'; import { dirname, join, relative, resolve } from 'node:path'; import { Project, Node, ts } from 'ts-morph'; // forked from design-sync lib/source-kit.mjs — synth-entry process shim (Next.js app reads process.env.NEXT_PUBLIC_*) import { leadingJsdoc, readText, slash, walk } from '../../.ds-sync/lib/common.mjs'; import { resolveDistEntry } from '../../.ds-sync/lib/bundle.mjs'; import { exportedNames, isComponentName } from '../../.ds-sync/lib/dts.mjs'; const NON_IMPL_RX = /\.(stories|test|spec)\./; const SRC_IMPL_RX = /\.(tsx|jsx)$/; // Dir names that don't usefully group components — skip so the emitted path // is `components//` not `components/components/`. const GENERIC_DIR = new Set(['components', 'component', 'src', 'lib', 'ui', 'packages', 'react']); const slug = (s) => s.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'general'; // No .d.ts → scan src files for PascalCase value exports via ts-morph. function deriveComponentsFromSrc(srcFiles) { const project = new Project({ skipAddingFilesFromTsConfig: true, compilerOptions: { jsx: ts.JsxEmit.Preserve, allowJs: true, skipLibCheck: true }, }); const seen = new Set(); for (const p of srcFiles) { if (NON_IMPL_RX.test(p) || !SRC_IMPL_RX.test(p)) continue; const sf = project.addSourceFileAtPathIfExists(p); if (!sf) continue; for (const [name, decls] of sf.getExportedDeclarations()) { // `export default function Button()` is keyed as 'default' — recover // the declared name from the function/class node. const real = name === 'default' ? decls.map((d) => d.getName?.()).find((n) => n && n !== 'default') : name; if (!real || !/^[A-Z][A-Za-z0-9]*$/.test(real)) continue; if (decls.some((d) => Node.isVariableDeclaration(d) || Node.isFunctionDeclaration(d) || Node.isClassDeclaration(d))) { seen.add(real); } } } return [...seen].sort().map((name) => ({ name, group: 'general' })); } export async function resolvePackage(ctx) { const { PKG_DIR, pkgJson, ENTRY_OVERRIDE, PKG, OUT, cfg } = ctx; const srcMap = cfg.componentSrcMap ?? {}; // ── 1. src/ discovery (best-effort; feeds enrichment + synth-entry fallback). // ASSUMPTION: source root is first of src/ | lib/ | components/. Override: cfg.srcDir. const srcRoot = [cfg.srcDir, 'src', 'lib', 'components'] .map((d) => d && resolve(PKG_DIR, d)) .find((d) => d && existsSync(d)); const srcFiles = srcRoot ? walk(srcRoot, (n) => /\.(tsx|jsx|mdx?)$/.test(n)) : []; // ── 2. entry: dist if it exists, else synthesize from src/ (last resort). let entry = resolveDistEntry({ pkgDir: PKG_DIR, pkgJson, override: ENTRY_OVERRIDE, pkgName: PKG, soft: true }); let synthEntry = false; if (!entry) { if (!srcRoot) { console.error(`[NO_DIST] ${PKG} has no built entry and no src/ to synthesize from — run its build.`); process.exit(1); } // Next route files (app/**/layout.tsx, page.tsx) that call `next/font` // loaders (Manrope(), IBM_Plex_Mono()) at module top-level crash the browser // IIFE: esbuild can't resolve the Next-only loader → stubs it to `undefined` // → `undefined()` aborts the whole bundle → window. stays empty and // every component vanishes. These files are never DS components anyway, so // drop any `next/font`-importing module from the synth-entry set. const comps = srcFiles.filter( (p) => SRC_IMPL_RX.test(p) && !NON_IMPL_RX.test(p) && !/from\s+['"]next\/font/.test(readFileSync(p, 'utf8')), ); // Next.js app code reads process.env.NEXT_PUBLIC_* at module top-level; in // the browser IIFE `process` is undefined → every component throws. Emit a // shim module and import it FIRST (ESM evaluates the first import's body // before the re-exported component modules), so process exists before any // component body runs. const shim = resolve(OUT, '.pkg-shim.mjs'); writeFileSync( shim, 'globalThis.process ??= { env: {} };\n' + 'globalThis.process.env ??= {};\n' + 'const __e = globalThis.process.env;\n' + "__e.NODE_ENV ??= 'development';\n" + "__e.NEXT_PUBLIC_ENABLE_PREVIEW ??= '1';\n" + "__e.NEXT_PUBLIC_BASE_PATH ??= '';\n" + "__e.NEXT_PUBLIC_API_BASE_URL ??= '';\n" + "__e.NEXT_PUBLIC_TRADEIN_CONTACT_EMAIL ??= 'trade-in@example.com';\n", ); entry = join(OUT, '.pkg-entry.mjs'); // `export *` does NOT re-export a module's default. Components authored as // `export default function ` (the v2 views/nav/overlay/panel) would be // absent from window. → [BUNDLE_EXPORT] "not a component". Re-export // each named default under its declared name so default-exported components // reach the global alongside the named ones. const defaultReexports = comps .map((p) => { const m = readFileSync(p, 'utf8').match( /export\s+default\s+(?:async\s+)?(?:function|class)\s+([A-Z][A-Za-z0-9]*)/, ); return m ? `export { default as ${m[1]} } from ${JSON.stringify(p)};` : null; }) .filter(Boolean); writeFileSync( entry, `import ${JSON.stringify(slash(shim))};\n` + comps.map((p) => `export * from ${JSON.stringify(p)};`).join('\n') + '\n' + defaultReexports.join('\n') + '\n', ); synthEntry = true; console.error( `[NO_DIST] no built entry — synthesizing from ${comps.length} src files (run the package's build for best results)`, ); } // ── 3. component list: from shipped .d.ts (authoritative when dist exists). // ASSUMPTION: components = PascalCase value exports in the .d.ts tree. // Override: cfg.componentSrcMap (non-null adds/pins, null excludes). const exported = exportedNames(PKG_DIR, pkgJson); const names = new Set([...exported].filter(isComponentName)); for (const [k, v] of Object.entries(srcMap)) { if (v === null) { names.delete(k); continue; } // Names reach `