Docker / build (push) Failing after 1m43s
Import this repo's components into a Claude Design project so the design
agent builds with the real Cloudrite library instead of generic components.
The repo is a Next.js app, not a component library: no dist/, no exports
map, no .d.ts tree, and Tailwind exists only as build directives. So
.design-sync/ carries a small build that manufactures a consumable package
from source (staged to the gitignored .ds-pkg/):
- tsconfig.dts.json declaration emit, for real <Name>Props contracts
- build-css.mjs Tailwind v4 -> a static stylesheet via @tailwindcss/postcss
- make-pkg.mjs stages the package; index.js exports all 292 symbols
while index.d.ts exports only the 61 roots, so shadcn's
flat compound parts don't each become a preview card.
Also generates the per-root parts tables in docs/
- shims/next-link router-free anchor; the real next/link needs
AppRouterContext and drags in the App Router runtime
- fonts/ Space Grotesk + Inter woff2 (SIL OFL). next/font supplies
these at runtime in the app, so the bundle has none
previews/ holds 40 authored preview stories, conventions.md is prepended to
the generated README as the design agent's usage guide, and NOTES.md records
the gotchas a future sync would otherwise rediscover.
Co-Authored-By: Claude Opus 5 <[email protected]>
213 lines
10 KiB
JavaScript
213 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
// Stage .ds-pkg/ — the synthetic "published package" the design-sync converter
|
|
// consumes.
|
|
//
|
|
// This repo is a Next.js app, not a component library: no dist/, no exports
|
|
// map, no .d.ts tree. The converter's synth-entry fallback would work, but it
|
|
// leaves every <Name>Props body empty (props are resolved by ts-morph from a
|
|
// .d.ts tree that doesn't exist here), and an empty props contract is what the
|
|
// claude.ai/design agent would code against.
|
|
//
|
|
// So we give it a real one, derived entirely from the repo's own source:
|
|
//
|
|
// .ds-pkg/package.json name/version/module/types, so PKG_DIR + findTypesRoot
|
|
// resolve the way they would for a published package
|
|
// .ds-pkg/index.d.ts re-exports the tsc declaration emit (tsconfig.dts.json)
|
|
// for the ROOT of each module only. The converter reads
|
|
// this entry as the component list, and shadcn ships
|
|
// ~292 flat exports (CardHeader, CardTitle, …) — every
|
|
// one of which would otherwise become its own preview
|
|
// card. Roots here, parts in the docs table below.
|
|
// .ds-pkg/index.js re-exports the SOURCE .tsx — every export, parts
|
|
// included, so window.Cloudrite carries the whole API.
|
|
// esbuild bundles from source, so the runtime bundle is
|
|
// the real components, never a recompiled copy
|
|
// .ds-pkg/styles.css copy of .design-sync/compiled.css, with fonts/
|
|
// .ds-pkg/fonts/*.woff2 alongside it. cfg.cssEntry is bounded to PKG_DIR by
|
|
// the converter, so the stylesheet has to live inside
|
|
// the staged package; the relative url(./fonts/…)
|
|
// references survive the copy because both move together
|
|
// .design-sync/docs/*.md per-root docs: the group, and the compound parts
|
|
// table (shadcn exports parts flat — CardHeader, not
|
|
// Card.Header — so the converter's namespace-based
|
|
// subcomponent grouping can't see them, and without
|
|
// this the agent never learns the parts exist)
|
|
//
|
|
// Run order (all three, in this order — see cfg.buildCmd):
|
|
// node_modules/.bin/tsc -p .design-sync/tsconfig.dts.json
|
|
// node .design-sync/build-css.mjs
|
|
// node .design-sync/make-pkg.mjs
|
|
|
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const REPO = resolve(HERE, '..');
|
|
const PKG_DIR = join(REPO, '.ds-pkg');
|
|
const TYPES = join(PKG_DIR, 'types');
|
|
const DOCS = join(HERE, 'docs');
|
|
|
|
// ── source files to expose ───────────────────────────────────────────────
|
|
// Skipped, with reasons:
|
|
// ui/toaster.tsx — exports `Toaster`, colliding with ui/sonner.tsx's. The
|
|
// app ships both (v0 scaffolding); sonner is the current
|
|
// one, so it wins and the legacy pair stays out of the DS.
|
|
// chatwidget.tsx — a Chatwoot script injector. Renders no markup and fires a
|
|
// fetch('/api/chatwoot') on mount; nothing to design with.
|
|
// ui/use-mobile — hook only, no component export.
|
|
const SKIP = new Set(['components/ui/toaster.tsx', 'components/chatwidget.tsx', 'components/ui/use-mobile.tsx']);
|
|
|
|
// The root component of each file is its first PascalCase export — true for
|
|
// every shadcn primitive except toast.tsx, which lists the provider first.
|
|
const ROOT_OVERRIDE = { 'components/ui/toast.tsx': 'Toast' };
|
|
|
|
// Not a design component — a context wrapper. Stays in the bundle (the agent
|
|
// may need to wrap), but gets no card.
|
|
const NO_CARD = new Set(['ThemeProvider']);
|
|
|
|
const GROUPS = {
|
|
sections: ['Header', 'Hero', 'Services', 'Features', 'Process', 'Contact', 'Footer'],
|
|
actions: ['Button', 'ButtonGroup', 'Toggle', 'ToggleGroup'],
|
|
forms: ['Input', 'Textarea', 'Label', 'Checkbox', 'RadioGroup', 'Select', 'Switch', 'Slider',
|
|
'Form', 'Field', 'InputGroup', 'InputOTP', 'Calendar'],
|
|
layout: ['Card', 'Separator', 'AspectRatio', 'ScrollArea', 'ResizablePanelGroup', 'Sidebar', 'Item', 'Empty'],
|
|
navigation: ['Breadcrumb', 'NavigationMenu', 'Menubar', 'Pagination', 'Tabs', 'Command'],
|
|
overlays: ['Dialog', 'AlertDialog', 'Sheet', 'Drawer', 'Popover', 'HoverCard', 'Tooltip',
|
|
'DropdownMenu', 'ContextMenu'],
|
|
feedback: ['Alert', 'Badge', 'Progress', 'Skeleton', 'Spinner', 'Toast', 'Toaster'],
|
|
display: ['Table', 'Avatar', 'Accordion', 'Collapsible', 'Carousel', 'ChartContainer', 'Kbd'],
|
|
};
|
|
const groupOf = (name) =>
|
|
Object.entries(GROUPS).find(([, names]) => names.includes(name))?.[0] ?? 'misc';
|
|
|
|
// ── collect exports per source file, in declaration order ────────────────
|
|
function walk(dir, test, out = []) {
|
|
for (const e of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
const p = join(dir, e.name);
|
|
if (e.isDirectory()) walk(p, test, out);
|
|
else if (test(e.name)) out.push(p);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// Ordered PascalCase exports of a .d.ts: `export { A, B }` lists win (they
|
|
// carry the file's own ordering), else `export declare` order.
|
|
function exportsOf(dtsPath) {
|
|
const s = readFileSync(dtsPath, 'utf8');
|
|
const names = [];
|
|
const push = (n) => { if (/^[A-Z]/.test(n) && !names.includes(n)) names.push(n); };
|
|
const lists = [...s.matchAll(/export\s*\{([^}]*)\}/g)];
|
|
if (lists.length) {
|
|
for (const m of lists) {
|
|
for (const raw of m[1].split(',')) {
|
|
const n = raw.trim();
|
|
if (!n) continue;
|
|
const as = n.split(/\s+as\s+/);
|
|
push((as[1] ?? as[0]).trim());
|
|
}
|
|
}
|
|
}
|
|
for (const m of s.matchAll(/export\s+declare\s+(?:const|function|class)\s+([A-Za-z0-9_$]+)/g)) push(m[1]);
|
|
return names;
|
|
}
|
|
|
|
// Prop signature of a part, for the docs table. Falls back to '—' when the
|
|
// declaration isn't a plain function (forwardRef consts etc.).
|
|
function propSigOf(dtsPath, name) {
|
|
const s = readFileSync(dtsPath, 'utf8');
|
|
const re = new RegExp(`declare (?:function|const) ${name}\\b([\\s\\S]*?)(?=\\ndeclare |\\nexport |$)`);
|
|
const m = re.exec(s);
|
|
if (!m) return null;
|
|
const props = /:\s*([^)]*?)\)\s*:/.exec(m[1].replace(/\{[^{}]*\}/g, '{…}'));
|
|
return props ? props[1].replace(/\s+/g, ' ').trim() : null;
|
|
}
|
|
|
|
const srcFiles = walk(join(REPO, 'components'), (n) => /\.tsx$/.test(n))
|
|
.map((p) => relative(REPO, p).split('\\').join('/'))
|
|
.filter((p) => !SKIP.has(p));
|
|
|
|
const units = [];
|
|
for (const src of srcFiles) {
|
|
const dts = join(TYPES, src.replace(/\.tsx$/, '.d.ts'));
|
|
if (!existsSync(dts)) { console.error(`! no declaration for ${src} — skipped`); continue; }
|
|
const names = exportsOf(dts);
|
|
if (!names.length) continue;
|
|
const root = ROOT_OVERRIDE[src] ?? names[0];
|
|
units.push({ src, dts, names, root, parts: names.filter((n) => n !== root) });
|
|
}
|
|
|
|
// ── emit the package ─────────────────────────────────────────────────────
|
|
mkdirSync(PKG_DIR, { recursive: true });
|
|
const appPkg = JSON.parse(readFileSync(join(REPO, 'package.json'), 'utf8'));
|
|
writeFileSync(join(PKG_DIR, 'package.json'), JSON.stringify({
|
|
name: 'cloudrite',
|
|
version: appPkg.version ?? '0.1.0',
|
|
private: true,
|
|
type: 'module',
|
|
module: 'index.js',
|
|
main: 'index.js',
|
|
types: 'index.d.ts',
|
|
}, null, 2) + '\n');
|
|
|
|
// Stylesheet + fonts move into the package together so url(./fonts/…) still
|
|
// resolves. build-css.mjs must have run first.
|
|
const compiled = join(HERE, 'compiled.css');
|
|
if (!existsSync(compiled)) {
|
|
console.error('! .design-sync/compiled.css missing — run node .design-sync/build-css.mjs first');
|
|
process.exit(1);
|
|
}
|
|
copyFileSync(compiled, join(PKG_DIR, 'styles.css'));
|
|
mkdirSync(join(PKG_DIR, 'fonts'), { recursive: true });
|
|
for (const f of readdirSync(join(HERE, 'fonts')).filter((f) => /\.(woff2?|ttf|otf)$/.test(f))) {
|
|
copyFileSync(join(HERE, 'fonts', f), join(PKG_DIR, 'fonts', f));
|
|
}
|
|
|
|
const banner = '// Generated by .design-sync/make-pkg.mjs — do not edit.\n';
|
|
writeFileSync(join(PKG_DIR, 'index.js'),
|
|
banner + units.map((u) => `export * from '../${u.src.replace(/\.tsx$/, '')}';`).join('\n') + '\n');
|
|
writeFileSync(join(PKG_DIR, 'index.d.ts'),
|
|
banner + units
|
|
.filter((u) => !NO_CARD.has(u.root))
|
|
.map((u) => `export { ${u.root} } from './types/${u.src.replace(/\.tsx$/, '')}';`)
|
|
.join('\n') + '\n');
|
|
|
|
// ── per-root docs ────────────────────────────────────────────────────────
|
|
rmSync(DOCS, { recursive: true, force: true });
|
|
mkdirSync(DOCS, { recursive: true });
|
|
let withParts = 0;
|
|
for (const u of units) {
|
|
if (NO_CARD.has(u.root)) continue;
|
|
const lines = [
|
|
'---',
|
|
`category: ${groupOf(u.root)}`,
|
|
'---',
|
|
'',
|
|
`# ${u.root}`,
|
|
'',
|
|
`Source: \`${u.src}\`.`,
|
|
'',
|
|
];
|
|
if (u.parts.length) {
|
|
withParts++;
|
|
lines.push(
|
|
`\`${u.root}\` is a compound component. Its parts are exported **flat** (\`${u.parts[0]}\`, not`,
|
|
`\`${u.root}.${u.parts[0].startsWith(u.root) ? u.parts[0].slice(u.root.length) : u.parts[0]}\`) and each is a separate top-level export of the bundle:`,
|
|
'',
|
|
'| Part | Props |',
|
|
'| --- | --- |',
|
|
);
|
|
for (const p of u.parts) {
|
|
const sig = propSigOf(u.dts, p);
|
|
lines.push(`| \`${p}\` | ${sig ? `\`${sig}\`` : '—'} |`);
|
|
}
|
|
lines.push('', `All parts accept \`className\` and are composed as children of \`${u.root}\`.`, '');
|
|
}
|
|
writeFileSync(join(DOCS, `${u.root}.md`), lines.join('\n'));
|
|
}
|
|
|
|
const roots = units.filter((u) => !NO_CARD.has(u.root)).map((u) => u.root);
|
|
console.error(`.ds-pkg: ${units.length} modules, ${units.reduce((n, u) => n + u.names.length, 0)} exports`);
|
|
console.error(`docs: ${roots.length} roots (${withParts} compound) → ${relative(REPO, DOCS)}`);
|
|
console.error(`roots: ${roots.join(' ')}`);
|