@1agh/maude 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/cli/bin/maude.exe +15 -0
- package/cli/bin/maude.mjs +45 -0
- package/cli/bin/mdcc.exe +10 -0
- package/cli/bin/mdcc.mjs +7 -0
- package/cli/cli-wrapper.cjs +67 -0
- package/cli/commands/config.mjs +94 -0
- package/cli/commands/design.mjs +386 -0
- package/cli/commands/help.mjs +57 -0
- package/cli/commands/init.mjs +178 -0
- package/cli/commands/version.mjs +7 -0
- package/cli/install.cjs +113 -0
- package/cli/lib/argv.mjs +37 -0
- package/cli/lib/argv.test.mjs +46 -0
- package/cli/lib/copy-tree.mjs +78 -0
- package/package.json +94 -0
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +397 -0
- package/plugins/design/dev-server/annotations-layer.tsx +1717 -0
- package/plugins/design/dev-server/api.ts +674 -0
- package/plugins/design/dev-server/bin/_screenshot-playwright.mjs +50 -0
- package/plugins/design/dev-server/bin/bootstrap-check.sh +83 -0
- package/plugins/design/dev-server/bin/canvas-edit.sh +48 -0
- package/plugins/design/dev-server/bin/handoff.sh +27 -0
- package/plugins/design/dev-server/bin/screenshot.sh +232 -0
- package/plugins/design/dev-server/bin/server-up.sh +135 -0
- package/plugins/design/dev-server/bin/slug.sh +22 -0
- package/plugins/design/dev-server/bin/smoke.sh +272 -0
- package/plugins/design/dev-server/build.ts +267 -0
- package/plugins/design/dev-server/canvas-build.ts +219 -0
- package/plugins/design/dev-server/canvas-edit.ts +388 -0
- package/plugins/design/dev-server/canvas-header.ts +165 -0
- package/plugins/design/dev-server/canvas-icons.tsx +131 -0
- package/plugins/design/dev-server/canvas-lib-inline.ts +260 -0
- package/plugins/design/dev-server/canvas-lib-resolver.ts +85 -0
- package/plugins/design/dev-server/canvas-lib.tsx +1995 -0
- package/plugins/design/dev-server/canvas-meta.schema.json +181 -0
- package/plugins/design/dev-server/canvas-pipeline.ts +270 -0
- package/plugins/design/dev-server/canvas-shell.tsx +813 -0
- package/plugins/design/dev-server/client/app.jsx +2027 -0
- package/plugins/design/dev-server/client/hmr.mjs +85 -0
- package/plugins/design/dev-server/client/iframe-lazy.mjs +121 -0
- package/plugins/design/dev-server/client/index.html +15 -0
- package/plugins/design/dev-server/client/styles/0-reset.css +18 -0
- package/plugins/design/dev-server/client/styles/1-tokens.css +297 -0
- package/plugins/design/dev-server/client/styles/2-layout.css +35 -0
- package/plugins/design/dev-server/client/styles/3-shell.css +906 -0
- package/plugins/design/dev-server/client/styles/4-components.css +1268 -0
- package/plugins/design/dev-server/client/styles/5-utilities.css +4 -0
- package/plugins/design/dev-server/client/styles/_index.css +24 -0
- package/plugins/design/dev-server/client/styles.css +1419 -0
- package/plugins/design/dev-server/config.schema.json +147 -0
- package/plugins/design/dev-server/context-menu.tsx +343 -0
- package/plugins/design/dev-server/context.ts +173 -0
- package/plugins/design/dev-server/dist/client.bundle.js +20323 -0
- package/plugins/design/dev-server/dist/styles.css +2875 -0
- package/plugins/design/dev-server/examples/README.md +9 -0
- package/plugins/design/dev-server/examples/perf-100-artboards.tsx +113 -0
- package/plugins/design/dev-server/fs-watch.ts +63 -0
- package/plugins/design/dev-server/handoff.ts +721 -0
- package/plugins/design/dev-server/history.ts +125 -0
- package/plugins/design/dev-server/hmr-broadcast.ts +114 -0
- package/plugins/design/dev-server/http.ts +413 -0
- package/plugins/design/dev-server/input-router.tsx +485 -0
- package/plugins/design/dev-server/inspect.ts +365 -0
- package/plugins/design/dev-server/locator.ts +159 -0
- package/plugins/design/dev-server/mem.ts +97 -0
- package/plugins/design/dev-server/runtime-bundle.ts +235 -0
- package/plugins/design/dev-server/server.mjs +1246 -0
- package/plugins/design/dev-server/server.ts +131 -0
- package/plugins/design/dev-server/test/_helpers.ts +81 -0
- package/plugins/design/dev-server/test/active-state.test.ts +145 -0
- package/plugins/design/dev-server/test/annotations-api.test.ts +146 -0
- package/plugins/design/dev-server/test/annotations-layer.test.ts +419 -0
- package/plugins/design/dev-server/test/binary-smoke.test.ts +47 -0
- package/plugins/design/dev-server/test/bundle-smoke.test.ts +29 -0
- package/plugins/design/dev-server/test/canvas-build.test.ts +78 -0
- package/plugins/design/dev-server/test/canvas-edit.test.ts +139 -0
- package/plugins/design/dev-server/test/canvas-header.test.ts +127 -0
- package/plugins/design/dev-server/test/canvas-lib-inline.test.ts +146 -0
- package/plugins/design/dev-server/test/canvas-lib-resolver.test.ts +112 -0
- package/plugins/design/dev-server/test/canvas-meta-api.test.ts +236 -0
- package/plugins/design/dev-server/test/canvas-pipeline.test.ts +180 -0
- package/plugins/design/dev-server/test/canvas-route.test.ts +176 -0
- package/plugins/design/dev-server/test/fs-watch.test.ts +41 -0
- package/plugins/design/dev-server/test/handoff-static-frames.test.ts +215 -0
- package/plugins/design/dev-server/test/handoff.test.ts +281 -0
- package/plugins/design/dev-server/test/history-rollback.test.ts +62 -0
- package/plugins/design/dev-server/test/hmr-broadcast.test.ts +108 -0
- package/plugins/design/dev-server/test/input-router.test.ts +316 -0
- package/plugins/design/dev-server/test/locator.test.ts +214 -0
- package/plugins/design/dev-server/test/perf-harness.ts +193 -0
- package/plugins/design/dev-server/test/phase-3.6-smoke.test.ts +77 -0
- package/plugins/design/dev-server/test/runtime-bundle.test.ts +69 -0
- package/plugins/design/dev-server/test/server-lifecycle.test.ts +28 -0
- package/plugins/design/dev-server/test/tool-palette.test.tsx +55 -0
- package/plugins/design/dev-server/test/use-annotation-selection.test.tsx +77 -0
- package/plugins/design/dev-server/test/use-artboard-drag.test.ts +325 -0
- package/plugins/design/dev-server/test/use-selection-set.test.tsx +166 -0
- package/plugins/design/dev-server/test/use-snap-guides.test.ts +190 -0
- package/plugins/design/dev-server/test/use-tool-mode.test.tsx +93 -0
- package/plugins/design/dev-server/test/ws-handshake.test.ts +33 -0
- package/plugins/design/dev-server/tool-palette.tsx +278 -0
- package/plugins/design/dev-server/tsconfig.json +26 -0
- package/plugins/design/dev-server/use-annotation-selection.tsx +92 -0
- package/plugins/design/dev-server/use-annotations-visibility.tsx +43 -0
- package/plugins/design/dev-server/use-artboard-drag.tsx +445 -0
- package/plugins/design/dev-server/use-selection-set.tsx +224 -0
- package/plugins/design/dev-server/use-snap-guides.tsx +215 -0
- package/plugins/design/dev-server/use-tool-mode.tsx +114 -0
- package/plugins/design/dev-server/ws.ts +90 -0
- package/plugins/design/templates/_shell.html +177 -0
- package/plugins/design/templates/canvas.tsx.template +54 -0
- package/plugins/design/templates/design-system-inspiration/_MAPPING.md +277 -0
- package/plugins/design/templates/design-system-inspiration/_README.md +71 -0
- package/plugins/design/templates/design-system-inspiration/audience-consumer/components-banner.html +68 -0
- package/plugins/design/templates/design-system-inspiration/audience-consumer/components-empty-state-generous.html +39 -0
- package/plugins/design/templates/design-system-inspiration/audience-consumer/components-feature-grid.html +62 -0
- package/plugins/design/templates/design-system-inspiration/audience-consumer/components-marketing-card.html +63 -0
- package/plugins/design/templates/design-system-inspiration/audience-consumer/components-testimonial.html +80 -0
- package/plugins/design/templates/design-system-inspiration/audience-developer/components-code-block.html +71 -0
- package/plugins/design/templates/design-system-inspiration/audience-developer/components-diff-view.html +65 -0
- package/plugins/design/templates/design-system-inspiration/audience-developer/components-log-stream.html +62 -0
- package/plugins/design/templates/design-system-inspiration/audience-developer/components-monospace-table.html +57 -0
- package/plugins/design/templates/design-system-inspiration/audience-developer/components-terminal-pane.html +67 -0
- package/plugins/design/templates/design-system-inspiration/audience-developer/type-mono.html +57 -0
- package/plugins/design/templates/design-system-inspiration/audience-pro/colors-presence.html +74 -0
- package/plugins/design/templates/design-system-inspiration/audience-pro/components-command-palette.html +90 -0
- package/plugins/design/templates/design-system-inspiration/audience-pro/components-keyboard.html +51 -0
- package/plugins/design/templates/design-system-inspiration/audience-pro/components-list.html +89 -0
- package/plugins/design/templates/design-system-inspiration/audience-pro/components-shortcuts-overlay.html +85 -0
- package/plugins/design/templates/design-system-inspiration/audience-pro/components-toast-menu.html +80 -0
- package/plugins/design/templates/design-system-inspiration/core/INDEX.md.tpl +25 -0
- package/plugins/design/templates/design-system-inspiration/core/README.orchestration.md.tpl +71 -0
- package/plugins/design/templates/design-system-inspiration/core/README.philosophy.md.tpl +77 -0
- package/plugins/design/templates/design-system-inspiration/core/SKILL.md.tpl +50 -0
- package/plugins/design/templates/design-system-inspiration/core/colors_and_type.css.tpl +111 -0
- package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +28 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/_layout.css +113 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/colors-accent.html +48 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/colors-surfaces.html +49 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/colors-text.html +52 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/components-buttons.html +83 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/components-cards.html +79 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/components-inputs.html +82 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/motion.html +66 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/spacing-scale.html +53 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/type-scale.html +62 -0
- package/plugins/design/templates/design-system-inspiration/foundations/borders.html +39 -0
- package/plugins/design/templates/design-system-inspiration/foundations/elevation.html +46 -0
- package/plugins/design/templates/design-system-inspiration/foundations/focus.html +48 -0
- package/plugins/design/templates/design-system-inspiration/foundations/grid.html +51 -0
- package/plugins/design/templates/design-system-inspiration/foundations/iconography.html +73 -0
- package/plugins/design/templates/design-system-inspiration/foundations/opacity.html +45 -0
- package/plugins/design/templates/design-system-inspiration/foundations/radii.html +40 -0
- package/plugins/design/templates/design-system-inspiration/foundations/selection.html +45 -0
- package/plugins/design/templates/design-system-inspiration/meta/accessibility.html +62 -0
- package/plugins/design/templates/design-system-inspiration/meta/i18n.html +73 -0
- package/plugins/design/templates/design-system-inspiration/meta/presence-multiplayer.html +71 -0
- package/plugins/design/templates/design-system-inspiration/meta/tokens-index.html +80 -0
- package/plugins/design/templates/design-system-inspiration/patterns/patterns-auth.html +78 -0
- package/plugins/design/templates/design-system-inspiration/patterns/patterns-data-density.html +61 -0
- package/plugins/design/templates/design-system-inspiration/patterns/patterns-error-pages.html +70 -0
- package/plugins/design/templates/design-system-inspiration/patterns/patterns-form-layouts.html +70 -0
- package/plugins/design/templates/design-system-inspiration/patterns/patterns-onboarding.html +71 -0
- package/plugins/design/templates/design-system-inspiration/patterns/patterns-pricing.html +83 -0
- package/plugins/design/templates/design-system-inspiration/platform-desktop/components-resize-panels.html +63 -0
- package/plugins/design/templates/design-system-inspiration/platform-desktop/ui_kits-desktop-index.html +55 -0
- package/plugins/design/templates/design-system-inspiration/platform-desktop/ui_kits-desktop-showcase.html +302 -0
- package/plugins/design/templates/design-system-inspiration/platform-mobile/components-bottom-sheet.html +63 -0
- package/plugins/design/templates/design-system-inspiration/platform-mobile/components-pull-to-refresh.html +74 -0
- package/plugins/design/templates/design-system-inspiration/platform-mobile/components-segmented-control.html +51 -0
- package/plugins/design/templates/design-system-inspiration/platform-mobile/components-tab-bar.html +57 -0
- package/plugins/design/templates/design-system-inspiration/platform-mobile/ui_kits-mobile-index.html +58 -0
- package/plugins/design/templates/design-system-inspiration/platform-mobile/ui_kits-mobile-showcase.html +237 -0
- package/plugins/design/templates/design-system-inspiration/status/colors-status.html +49 -0
- package/plugins/design/templates/design-system-inspiration/status/components-status.html +63 -0
- package/plugins/design/templates/design-system-inspiration/status/skeletons.html +74 -0
- package/plugins/design/templates/design-system-inspiration/theme-both/colors-themes-side-by-side.html +59 -0
- package/plugins/design/templates/design-system-inspiration/universal/components-callout.html +74 -0
- package/plugins/design/templates/design-system-inspiration/universal/components-dialogs.html +81 -0
- package/plugins/design/templates/design-system-inspiration/universal/components-tables.html +101 -0
- package/plugins/design/templates/design-system-inspiration/universal/components-toggles.html +74 -0
- package/plugins/design/templates/design-system-inspiration/universal/components-tooltips.html +74 -0
- package/plugins/design/templates/design-system-inspiration/universal/empty-state.html.tpl +34 -0
- package/plugins/design/templates/design-system-inspiration/universal/logo.html +42 -0
- package/plugins/design/templates/ds-specimen.tsx.template +59 -0
- package/plugins/flow/.claude-plugin/config.schema.json +398 -0
- package/plugins/flow/README.md +45 -0
- package/plugins/flow/templates/ai-skeleton/INDEX.md +50 -0
- package/plugins/flow/templates/ai-skeleton/README.md +46 -0
- package/plugins/flow/templates/ai-skeleton/browser/har/.gitkeep +0 -0
- package/plugins/flow/templates/ai-skeleton/browser/snapshots/.gitkeep +0 -0
- package/plugins/flow/templates/ai-skeleton/business/README.md +12 -0
- package/plugins/flow/templates/ai-skeleton/context/README.md +12 -0
- package/plugins/flow/templates/ai-skeleton/decisions/README.md +20 -0
- package/plugins/flow/templates/ai-skeleton/design-import/.gitkeep +0 -0
- package/plugins/flow/templates/ai-skeleton/dev-logs/.gitkeep +0 -0
- package/plugins/flow/templates/ai-skeleton/device/.gitkeep +0 -0
- package/plugins/flow/templates/ai-skeleton/docs/README.md +5 -0
- package/plugins/flow/templates/ai-skeleton/logs/.gitkeep +0 -0
- package/plugins/flow/templates/ai-skeleton/logs/README.md +13 -0
- package/plugins/flow/templates/ai-skeleton/plans/README.md +16 -0
- package/plugins/flow/templates/ai-skeleton/plans/archive/.gitkeep +0 -0
- package/plugins/flow/templates/ai-skeleton/release-guide.md +35 -0
- package/plugins/flow/templates/ai-skeleton/reviews/README.md +15 -0
- package/plugins/flow/templates/ai-skeleton/scenarios/README.md +26 -0
- package/plugins/flow/templates/ai-skeleton/scenarios/_lib/.gitkeep +0 -0
- package/plugins/flow/templates/ai-skeleton/state/STATE.md +25 -0
- package/plugins/flow/templates/ai-skeleton/templates/HANDOFF.md +32 -0
- package/plugins/flow/templates/ai-skeleton/workflows.config.json +74 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
// /design:handoff emitter — shadcn registry-item.json sidecar (Phase 3.6 Task 7 + 12b).
|
|
2
|
+
//
|
|
3
|
+
// Walks a canvas TSX file, classifies its imports, strips `data-cd-id` attrs
|
|
4
|
+
// (those are dev-time scaffolding the production drop has no business with),
|
|
5
|
+
// optionally bundles the actually-used subset of `_components.css` +
|
|
6
|
+
// `colors_and_type.css`, and emits a JSON sidecar conforming to
|
|
7
|
+
// https://ui.shadcn.com/schema/registry-item.json.
|
|
8
|
+
//
|
|
9
|
+
// Consumer pattern:
|
|
10
|
+
// bunx shadcn add file://./<Slug>.registry.json
|
|
11
|
+
//
|
|
12
|
+
// Same oxc-parser + magic-string + lightningcss toolchain as canvas-pipeline.ts
|
|
13
|
+
// and canvas-edit.ts — one mental model, three call sites.
|
|
14
|
+
//
|
|
15
|
+
// Strip semantics (Task 12):
|
|
16
|
+
// - data-cd-id → removed (pipeline-emitted; not source)
|
|
17
|
+
// - JSDoc header → kept (cold-read context for future Claude / human reader)
|
|
18
|
+
// - inline DesignCanvas/DCSection/DCArtboard chrome → kept as-is; the
|
|
19
|
+
// consumer's host page wraps or unwraps as needed.
|
|
20
|
+
//
|
|
21
|
+
// CSS bundling (Task 12b):
|
|
22
|
+
// - Scan canvas TSX for every string literal that appears as a `className`
|
|
23
|
+
// attribute value. Tokenise on whitespace → set of class names.
|
|
24
|
+
// - Parse `_components.css` with lightningcss; keep rules whose selector list
|
|
25
|
+
// contains any selector whose `base class` (the first class segment, after
|
|
26
|
+
// stripping pseudo + descendant tail) is in the set.
|
|
27
|
+
// - Walk the kept CSS for `var(--name)` references, intersect with
|
|
28
|
+
// `colors_and_type.css`, emit only the matched custom properties under
|
|
29
|
+
// cssVars.theme (the schema's first-class theme-token slot).
|
|
30
|
+
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
|
|
33
|
+
import MagicString from 'magic-string';
|
|
34
|
+
import { parseSync } from 'oxc-parser';
|
|
35
|
+
|
|
36
|
+
import { buildLibMap, inlineUsedExports } from './canvas-lib-inline.ts';
|
|
37
|
+
import { canvasLibPath } from './canvas-lib-resolver.ts';
|
|
38
|
+
|
|
39
|
+
// biome-ignore lint/suspicious/noExplicitAny: oxc AST nodes are heterogeneous.
|
|
40
|
+
type AnyNode = any;
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Public types — shaped to match shadcn's registry-item.json schema. Reference:
|
|
44
|
+
// https://ui.shadcn.com/schema/registry-item.json
|
|
45
|
+
// We don't ship every optional field — only the ones we can produce reliably.
|
|
46
|
+
|
|
47
|
+
export interface RegistryItemFile {
|
|
48
|
+
/** Relative path the file should land at in the consumer project. */
|
|
49
|
+
path: string;
|
|
50
|
+
/** File contents. */
|
|
51
|
+
content: string;
|
|
52
|
+
/** shadcn file-type discriminator. */
|
|
53
|
+
type:
|
|
54
|
+
| 'registry:component'
|
|
55
|
+
| 'registry:block'
|
|
56
|
+
| 'registry:ui'
|
|
57
|
+
| 'registry:style'
|
|
58
|
+
| 'registry:lib'
|
|
59
|
+
| 'registry:hook'
|
|
60
|
+
| 'registry:theme';
|
|
61
|
+
/** Optional override for where the file lands in the consumer project. */
|
|
62
|
+
target?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface RegistryItem {
|
|
66
|
+
$schema: string;
|
|
67
|
+
name: string;
|
|
68
|
+
type: 'registry:block' | 'registry:component' | 'registry:ui';
|
|
69
|
+
title?: string;
|
|
70
|
+
description?: string;
|
|
71
|
+
/** npm package specifiers (e.g. ["react", "lucide-react"]). */
|
|
72
|
+
dependencies: string[];
|
|
73
|
+
/** Other registry items this depends on (e.g. ["button", "card"]). */
|
|
74
|
+
registryDependencies: string[];
|
|
75
|
+
/** Files to drop. Index 0 is conventionally the entry component. */
|
|
76
|
+
files: RegistryItemFile[];
|
|
77
|
+
/** CSS custom properties grouped by theme (light/dark). Empty when no tokens used. */
|
|
78
|
+
cssVars?: {
|
|
79
|
+
theme?: Record<string, string>;
|
|
80
|
+
light?: Record<string, string>;
|
|
81
|
+
dark?: Record<string, string>;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface EmitOptions {
|
|
86
|
+
/** Absolute path to canvas .tsx file. */
|
|
87
|
+
canvasAbsPath: string;
|
|
88
|
+
/** Pretty title from meta.json (becomes registry item `title`). */
|
|
89
|
+
title?: string;
|
|
90
|
+
/** One-line description from meta.json.subtitle (becomes `description`). */
|
|
91
|
+
description?: string;
|
|
92
|
+
/** Optional path to project's `_components.css` for CSS bundling (Task 12b). */
|
|
93
|
+
componentsCssPath?: string;
|
|
94
|
+
/** Optional path to project's tokens CSS for cssVars resolution (Task 12b). */
|
|
95
|
+
tokensCssPath?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Absolute path to design root. When provided, `@maude/canvas-lib` imports
|
|
98
|
+
* in the canvas are inlined from the dev-server-bundled canvas-lib so the
|
|
99
|
+
* emitted drop is self-contained (Phase 3.6.1 Task 9; per DDR-025 the lib
|
|
100
|
+
* lives in the dev-server, not under designRoot). The argument is kept for
|
|
101
|
+
* back-compat with the CLI shape — handoff inlining no longer reads it.
|
|
102
|
+
*/
|
|
103
|
+
designRoot?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Strip data-cd-id from source — the inverse of canvas-pipeline.ts pass 1.
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Remove every ` data-cd-id="<hex>"` attribute from a TSX source string.
|
|
111
|
+
* Pure: caller persists. Uses the same oxc-parser + magic-string toolchain as
|
|
112
|
+
* the pipeline that emitted them.
|
|
113
|
+
*/
|
|
114
|
+
export function stripDataCdId(canvasAbsPath: string, source: string): string {
|
|
115
|
+
const parsed = parseSync(canvasAbsPath, source, { sourceType: 'module' });
|
|
116
|
+
if (parsed.errors && parsed.errors.length > 0) {
|
|
117
|
+
const first = parsed.errors[0];
|
|
118
|
+
throw new Error(
|
|
119
|
+
`oxc-parser failed on ${canvasAbsPath} (${parsed.errors.length} errors). First: ${first?.message ?? 'unknown'}`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const s = new MagicString(source);
|
|
123
|
+
|
|
124
|
+
function visit(node: AnyNode): void {
|
|
125
|
+
if (!node || typeof node !== 'object') return;
|
|
126
|
+
if (Array.isArray(node)) {
|
|
127
|
+
for (const c of node) visit(c);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (typeof node.type !== 'string') return;
|
|
131
|
+
|
|
132
|
+
if (node.type === 'JSXOpeningElement') {
|
|
133
|
+
const attrs = node.attributes as AnyNode[] | undefined;
|
|
134
|
+
if (Array.isArray(attrs)) {
|
|
135
|
+
for (const a of attrs) {
|
|
136
|
+
if (
|
|
137
|
+
a?.type === 'JSXAttribute' &&
|
|
138
|
+
a.name?.type === 'JSXIdentifier' &&
|
|
139
|
+
a.name.name === 'data-cd-id' &&
|
|
140
|
+
typeof a.start === 'number' &&
|
|
141
|
+
typeof a.end === 'number'
|
|
142
|
+
) {
|
|
143
|
+
// Trim the leading whitespace too — author-friendly output.
|
|
144
|
+
let from = a.start as number;
|
|
145
|
+
while (from > 0 && /\s/.test(source[from - 1] as string)) from--;
|
|
146
|
+
s.remove(from, a.end);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const k of Object.keys(node)) {
|
|
153
|
+
if (k === 'loc' || k === 'range' || k === 'start' || k === 'end' || k === 'type') continue;
|
|
154
|
+
visit(node[k]);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
visit(parsed.program);
|
|
159
|
+
return s.toString();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Import classification — npm specifier vs shadcn `@/components/ui/*`.
|
|
164
|
+
|
|
165
|
+
interface ClassifiedImports {
|
|
166
|
+
/** npm package names, deduped. */
|
|
167
|
+
dependencies: string[];
|
|
168
|
+
/** shadcn primitive names (e.g. `button` from `@/components/ui/button`). */
|
|
169
|
+
registryDependencies: string[];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Classify import specifiers in a TSX source. Heuristic:
|
|
174
|
+
* - `@/components/ui/<name>` → registry dependency `<name>`
|
|
175
|
+
* - bare specifier starting with letter / `@` not in the above pattern
|
|
176
|
+
* → npm dependency (the package portion)
|
|
177
|
+
* - relative imports (`./...`, `../...`) → ignored (consumer ships its own)
|
|
178
|
+
*/
|
|
179
|
+
export function classifyImports(canvasAbsPath: string, source: string): ClassifiedImports {
|
|
180
|
+
const deps = new Set<string>();
|
|
181
|
+
const regDeps = new Set<string>();
|
|
182
|
+
|
|
183
|
+
// Bun.Transpiler.scanImports() is the documented fast path; pulls every
|
|
184
|
+
// ImportDeclaration / dynamic import / require call.
|
|
185
|
+
const scanner = new Bun.Transpiler({ loader: 'tsx' });
|
|
186
|
+
const imports = scanner.scanImports(source);
|
|
187
|
+
for (const imp of imports) {
|
|
188
|
+
const spec = imp.path;
|
|
189
|
+
if (!spec) continue;
|
|
190
|
+
if (spec.startsWith('./') || spec.startsWith('../') || spec.startsWith('/')) continue;
|
|
191
|
+
if (spec.startsWith('@/components/ui/')) {
|
|
192
|
+
const name = spec.slice('@/components/ui/'.length).split('/')[0];
|
|
193
|
+
if (name) regDeps.add(name);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
deps.add(packageNameOf(spec));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
void canvasAbsPath;
|
|
200
|
+
return {
|
|
201
|
+
dependencies: [...deps].sort(),
|
|
202
|
+
registryDependencies: [...regDeps].sort(),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Extract the npm package name from a bare specifier. Handles scoped packages
|
|
208
|
+
* (`@scope/name`) and subpath imports (`react-dom/client` → `react-dom`).
|
|
209
|
+
*/
|
|
210
|
+
function packageNameOf(spec: string): string {
|
|
211
|
+
if (spec.startsWith('@')) {
|
|
212
|
+
const parts = spec.split('/');
|
|
213
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : spec;
|
|
214
|
+
}
|
|
215
|
+
const slash = spec.indexOf('/');
|
|
216
|
+
return slash > 0 ? spec.slice(0, slash) : spec;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// className harvester — collect every class name string appearing as a JSX
|
|
221
|
+
// className attribute value (`className="a b"` or `className={'a b'}`).
|
|
222
|
+
|
|
223
|
+
export function collectClassNames(canvasAbsPath: string, source: string): Set<string> {
|
|
224
|
+
const parsed = parseSync(canvasAbsPath, source, { sourceType: 'module' });
|
|
225
|
+
const out = new Set<string>();
|
|
226
|
+
|
|
227
|
+
function pushTokens(str: string): void {
|
|
228
|
+
for (const tok of str.split(/\s+/)) {
|
|
229
|
+
if (tok) out.add(tok);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function visit(node: AnyNode): void {
|
|
234
|
+
if (!node || typeof node !== 'object') return;
|
|
235
|
+
if (Array.isArray(node)) {
|
|
236
|
+
for (const c of node) visit(c);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (typeof node.type !== 'string') return;
|
|
240
|
+
|
|
241
|
+
if (
|
|
242
|
+
node.type === 'JSXAttribute' &&
|
|
243
|
+
node.name?.type === 'JSXIdentifier' &&
|
|
244
|
+
node.name.name === 'className'
|
|
245
|
+
) {
|
|
246
|
+
// Walk the value subtree and harvest every string-literal / template
|
|
247
|
+
// quasi we find. Covers literal, JSXExpressionContainer(Literal),
|
|
248
|
+
// TemplateLiteral, BinaryExpression of literals, conditional with
|
|
249
|
+
// literal branches, clsx/cn calls with literal args, etc.
|
|
250
|
+
harvestStrings(node.value, pushTokens);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
for (const k of Object.keys(node)) {
|
|
255
|
+
if (k === 'loc' || k === 'range' || k === 'start' || k === 'end' || k === 'type') continue;
|
|
256
|
+
visit(node[k]);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
visit(parsed.program);
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function harvestStrings(node: AnyNode, sink: (s: string) => void): void {
|
|
265
|
+
if (!node || typeof node !== 'object') return;
|
|
266
|
+
if (Array.isArray(node)) {
|
|
267
|
+
for (const c of node) harvestStrings(c, sink);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const t = node.type;
|
|
271
|
+
if (typeof t !== 'string') return;
|
|
272
|
+
if (t === 'Literal' || t === 'StringLiteral') {
|
|
273
|
+
if (typeof node.value === 'string') sink(node.value);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (t === 'TemplateLiteral') {
|
|
277
|
+
for (const q of node.quasis ?? []) {
|
|
278
|
+
const raw = q?.value?.cooked ?? q?.value?.raw ?? '';
|
|
279
|
+
if (typeof raw === 'string') sink(raw);
|
|
280
|
+
}
|
|
281
|
+
for (const e of node.expressions ?? []) harvestStrings(e, sink);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
for (const k of Object.keys(node)) {
|
|
285
|
+
if (k === 'loc' || k === 'range' || k === 'start' || k === 'end' || k === 'type') continue;
|
|
286
|
+
harvestStrings(node[k], sink);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// CSS subset extraction — keep rules whose base class is in the harvested set,
|
|
292
|
+
// plus the var(--*) references inside those rules.
|
|
293
|
+
|
|
294
|
+
interface CssBundleResult {
|
|
295
|
+
/** CSS source containing only used rules. May be empty string. */
|
|
296
|
+
css: string;
|
|
297
|
+
/** Names of CSS custom properties referenced by the kept rules. */
|
|
298
|
+
tokens: Set<string>;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Filter a CSS file to only the rules whose first class selector is in `keep`.
|
|
303
|
+
* Naive but reliable parser: walks top-level rule blocks via balanced braces.
|
|
304
|
+
* Avoids pulling lightningcss into the visitor path for the v1 of this feature.
|
|
305
|
+
*/
|
|
306
|
+
export function filterComponentsCss(cssSource: string, keep: Set<string>): CssBundleResult {
|
|
307
|
+
const out: string[] = [];
|
|
308
|
+
const tokens = new Set<string>();
|
|
309
|
+
|
|
310
|
+
let i = 0;
|
|
311
|
+
const n = cssSource.length;
|
|
312
|
+
while (i < n) {
|
|
313
|
+
// Skip whitespace.
|
|
314
|
+
while (i < n && /\s/.test(cssSource[i] as string)) i++;
|
|
315
|
+
if (i >= n) break;
|
|
316
|
+
|
|
317
|
+
// Handle comments at top level — preserve them if attached to a kept rule;
|
|
318
|
+
// for simplicity, drop them all (they're documentation, not necessary).
|
|
319
|
+
if (cssSource[i] === '/' && cssSource[i + 1] === '*') {
|
|
320
|
+
const end = cssSource.indexOf('*/', i + 2);
|
|
321
|
+
i = end < 0 ? n : end + 2;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// @-rules: @media { ... }, @keyframes { ... }, etc. Recurse into @media,
|
|
326
|
+
// emit @keyframes wholesale if any class inside its body is referenced
|
|
327
|
+
// (animations are class-attached; we keep them when their owning class
|
|
328
|
+
// survives). Simpler v1: keep every @-rule whose body, when filtered, has
|
|
329
|
+
// content; emit the filtered body.
|
|
330
|
+
if (cssSource[i] === '@') {
|
|
331
|
+
const ruleStart = i;
|
|
332
|
+
// Find prelude end at next `{` or `;`.
|
|
333
|
+
let j = i;
|
|
334
|
+
while (j < n && cssSource[j] !== '{' && cssSource[j] !== ';') j++;
|
|
335
|
+
if (j >= n) break;
|
|
336
|
+
if (cssSource[j] === ';') {
|
|
337
|
+
// Naked @import / @charset / @namespace — drop.
|
|
338
|
+
i = j + 1;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
// Body starts at j. Scan to matching `}`.
|
|
342
|
+
const bodyEnd = matchBrace(cssSource, j);
|
|
343
|
+
if (bodyEnd < 0) {
|
|
344
|
+
i = n;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const prelude = cssSource.slice(ruleStart, j).trim();
|
|
348
|
+
const body = cssSource.slice(j + 1, bodyEnd);
|
|
349
|
+
const isKeyframes = /^@(-\w+-)?keyframes\b/.test(prelude);
|
|
350
|
+
if (isKeyframes) {
|
|
351
|
+
// Keep keyframes wholesale if its name shows up via animation refs in
|
|
352
|
+
// the kept ruleset. For v1 we keep all @keyframes; size impact is
|
|
353
|
+
// small (canvases rarely define many).
|
|
354
|
+
const block = cssSource.slice(ruleStart, bodyEnd + 1);
|
|
355
|
+
out.push(block);
|
|
356
|
+
collectVars(block, tokens);
|
|
357
|
+
i = bodyEnd + 1;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
// Recurse — filter inside @media.
|
|
361
|
+
const inner = filterComponentsCss(body, keep);
|
|
362
|
+
if (inner.css.trim().length > 0) {
|
|
363
|
+
out.push(`${prelude} {\n${inner.css.trimEnd()}\n}`);
|
|
364
|
+
for (const t of inner.tokens) tokens.add(t);
|
|
365
|
+
}
|
|
366
|
+
i = bodyEnd + 1;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Selector rule: read prelude up to `{`.
|
|
371
|
+
const ruleStart = i;
|
|
372
|
+
let j = i;
|
|
373
|
+
while (j < n && cssSource[j] !== '{') j++;
|
|
374
|
+
if (j >= n) break;
|
|
375
|
+
const bodyEnd = matchBrace(cssSource, j);
|
|
376
|
+
if (bodyEnd < 0) break;
|
|
377
|
+
const prelude = cssSource.slice(ruleStart, j).trim();
|
|
378
|
+
if (selectorListIntersects(prelude, keep)) {
|
|
379
|
+
const block = cssSource.slice(ruleStart, bodyEnd + 1);
|
|
380
|
+
out.push(block);
|
|
381
|
+
collectVars(block, tokens);
|
|
382
|
+
}
|
|
383
|
+
i = bodyEnd + 1;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return { css: out.join('\n\n'), tokens };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function matchBrace(s: string, openIdx: number): number {
|
|
390
|
+
let depth = 0;
|
|
391
|
+
for (let i = openIdx; i < s.length; i++) {
|
|
392
|
+
const c = s[i];
|
|
393
|
+
if (c === '{') depth++;
|
|
394
|
+
else if (c === '}') {
|
|
395
|
+
depth--;
|
|
396
|
+
if (depth === 0) return i;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return -1;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* True if the comma-separated selector list contains any selector whose
|
|
404
|
+
* first class segment is in `keep`. Splits on commas at depth 0 (won't fail on
|
|
405
|
+
* `:is(.a,.b)` and similar but stays naive for v1 — the canvases this serves
|
|
406
|
+
* don't use complex CSSWG selectors).
|
|
407
|
+
*/
|
|
408
|
+
function selectorListIntersects(prelude: string, keep: Set<string>): boolean {
|
|
409
|
+
for (const sel of prelude.split(',')) {
|
|
410
|
+
const cls = firstClass(sel.trim());
|
|
411
|
+
if (!cls) continue;
|
|
412
|
+
// Direct hit, or BEM-base hit (.btn--ghost survives when `btn` is kept,
|
|
413
|
+
// .card__title survives when `card` is kept). Matches author intent —
|
|
414
|
+
// canvases that already opt into BEM modifiers expect the whole family to
|
|
415
|
+
// travel with the base class.
|
|
416
|
+
if (keep.has(cls)) return true;
|
|
417
|
+
const bem = cls.split(/(?:--|__)/, 1)[0];
|
|
418
|
+
if (bem && bem !== cls && keep.has(bem)) return true;
|
|
419
|
+
}
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function firstClass(selector: string): string | null {
|
|
424
|
+
// Scan for `.name` — pick the first one. Stops at descendant combinators.
|
|
425
|
+
const m = selector.match(/\.([A-Za-z_-][A-Za-z0-9_-]*)/);
|
|
426
|
+
return m?.[1] ? m[1] : null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function collectVars(css: string, tokens: Set<string>): void {
|
|
430
|
+
for (const m of css.matchAll(/var\(\s*(--[A-Za-z0-9_-]+)/g)) {
|
|
431
|
+
if (m[1]) tokens.add(m[1]);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ---------------------------------------------------------------------------
|
|
436
|
+
// Token resolution — read `colors_and_type.css`, pluck only the custom
|
|
437
|
+
// properties listed in `tokens`. Output is keyed by token-without-`--`.
|
|
438
|
+
|
|
439
|
+
export function filterTokensCss(
|
|
440
|
+
cssSource: string,
|
|
441
|
+
tokens: Set<string>
|
|
442
|
+
): { theme: Record<string, string>; usedCss: string } {
|
|
443
|
+
const theme: Record<string, string> = {};
|
|
444
|
+
// The expectation here is that the tokens CSS declares each `--foo: value;`
|
|
445
|
+
// inside `:root` or theme-scoped blocks. We keep things simple: regex over
|
|
446
|
+
// top-level `--name: value;` declarations. Multiple themes get merged into
|
|
447
|
+
// `theme` (consumer can split later); a richer light/dark scheme is a
|
|
448
|
+
// later iteration.
|
|
449
|
+
const re = /(--[A-Za-z0-9_-]+)\s*:\s*([^;]+);/g;
|
|
450
|
+
const usedDeclarations: string[] = [];
|
|
451
|
+
let m: RegExpExecArray | null = re.exec(cssSource);
|
|
452
|
+
while (m !== null) {
|
|
453
|
+
const name = m[1] as string;
|
|
454
|
+
const value = (m[2] as string).trim();
|
|
455
|
+
if (tokens.has(name)) {
|
|
456
|
+
theme[name.slice(2)] = value;
|
|
457
|
+
usedDeclarations.push(`${name}: ${value};`);
|
|
458
|
+
}
|
|
459
|
+
m = re.exec(cssSource);
|
|
460
|
+
}
|
|
461
|
+
// Build a minimal :root usedCss block — useful when the consumer wants the
|
|
462
|
+
// raw declarations rather than the shadcn cssVars sugar.
|
|
463
|
+
const usedCss =
|
|
464
|
+
usedDeclarations.length > 0 ? `:root {\n ${usedDeclarations.join('\n ')}\n}\n` : '';
|
|
465
|
+
return { theme, usedCss };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
// Phase 4 T7 — handoff-static frame overrides.
|
|
470
|
+
//
|
|
471
|
+
// Dev-time canvas-lib carries the full infinite-canvas engine: DesignCanvas
|
|
472
|
+
// runs `useViewportController`, mounts `DCMiniMap` + `DCZoomToolbar`, walks
|
|
473
|
+
// children via `harvestArtboards`, etc. None of that belongs in a shadcn
|
|
474
|
+
// registry item — the consumer of a handed-off canvas wants the design as
|
|
475
|
+
// rendered, not the authoring engine.
|
|
476
|
+
//
|
|
477
|
+
// `applyHandoffStaticOverrides` rewrites the three frame functions in the
|
|
478
|
+
// libMap to minimal static variants with empty `deps`. When `inlineUsedExports`
|
|
479
|
+
// then BFS-resolves what the user canvas imports (`DesignCanvas`, `DCSection`,
|
|
480
|
+
// `DCArtboard`), it finds these stub bodies and never reaches the engine code
|
|
481
|
+
// (`useViewportController`, `DCMiniMap`, `DCZoomToolbar`, `WorldContext`,
|
|
482
|
+
// `harvestArtboards`, `synthDefaultGrid`, `computeFit`, ...).
|
|
483
|
+
//
|
|
484
|
+
// The static frames intentionally mirror the standalone-mode rendering branch
|
|
485
|
+
// of the dev-time components — same DOM, same classes, same data attributes —
|
|
486
|
+
// so the DS's `_components.css` rules still apply 1:1.
|
|
487
|
+
|
|
488
|
+
const STATIC_DESIGN_CANVAS = `function DesignCanvas({ children }) {
|
|
489
|
+
return <div className="dc-canvas">{children}</div>;
|
|
490
|
+
}`;
|
|
491
|
+
|
|
492
|
+
const STATIC_DC_SECTION = `function DCSection({ id, title, subtitle, children }) {
|
|
493
|
+
return (
|
|
494
|
+
<section className="dc-section" data-dc-section={id}>
|
|
495
|
+
<header>
|
|
496
|
+
<h2>{title}</h2>
|
|
497
|
+
{subtitle ? <p className="sku">{subtitle}</p> : null}
|
|
498
|
+
</header>
|
|
499
|
+
<div className="dc-section-body">{children}</div>
|
|
500
|
+
</section>
|
|
501
|
+
);
|
|
502
|
+
}`;
|
|
503
|
+
|
|
504
|
+
const STATIC_DC_ARTBOARD = `function DCArtboard({ id, label, width, height, children }) {
|
|
505
|
+
return (
|
|
506
|
+
<article className="dc-artboard" data-dc-screen={id} style={{ width, height }}>
|
|
507
|
+
<header className="dc-artboard-label sku">{label}</header>
|
|
508
|
+
<div className="dc-artboard-body">{children}</div>
|
|
509
|
+
</article>
|
|
510
|
+
);
|
|
511
|
+
}`;
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Names this routine overrides. Exported so tests can pin the list. Adding
|
|
515
|
+
* a new engine-bearing top-level export to canvas-lib that the canvas might
|
|
516
|
+
* import requires either (a) extending this map with a static variant, or
|
|
517
|
+
* (b) extending `inlineUsedExports`'s skip-set.
|
|
518
|
+
*/
|
|
519
|
+
export const HANDOFF_STATIC_FRAME_EXPORTS = ['DesignCanvas', 'DCSection', 'DCArtboard'] as const;
|
|
520
|
+
|
|
521
|
+
export function applyHandoffStaticOverrides(
|
|
522
|
+
libMap: Map<string, { name: string; source: string; deps: string[] }>
|
|
523
|
+
): void {
|
|
524
|
+
if (libMap.has('DesignCanvas')) {
|
|
525
|
+
libMap.set('DesignCanvas', { name: 'DesignCanvas', source: STATIC_DESIGN_CANVAS, deps: [] });
|
|
526
|
+
}
|
|
527
|
+
if (libMap.has('DCSection')) {
|
|
528
|
+
libMap.set('DCSection', { name: 'DCSection', source: STATIC_DC_SECTION, deps: [] });
|
|
529
|
+
}
|
|
530
|
+
if (libMap.has('DCArtboard')) {
|
|
531
|
+
libMap.set('DCArtboard', { name: 'DCArtboard', source: STATIC_DC_ARTBOARD, deps: [] });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ---------------------------------------------------------------------------
|
|
536
|
+
// Main entry — emit the registry-item.json structure.
|
|
537
|
+
|
|
538
|
+
export async function emitRegistryItem(opts: EmitOptions): Promise<RegistryItem> {
|
|
539
|
+
const canvasFile = Bun.file(opts.canvasAbsPath);
|
|
540
|
+
if (!(await canvasFile.exists())) {
|
|
541
|
+
throw new Error(`Canvas not found: ${opts.canvasAbsPath}`);
|
|
542
|
+
}
|
|
543
|
+
const rawTsx = await canvasFile.text();
|
|
544
|
+
|
|
545
|
+
// Strip dev-time scaffolding.
|
|
546
|
+
let tsx = stripDataCdId(opts.canvasAbsPath, rawTsx);
|
|
547
|
+
|
|
548
|
+
// Inline canvas-lib helpers — when the canvas imports from @maude/canvas-lib,
|
|
549
|
+
// we splice the resolved exports + their transitive deps into the canvas
|
|
550
|
+
// source and strip the specifier. Phase 3.6.1 Task 9.
|
|
551
|
+
//
|
|
552
|
+
// Phase 4 T7 — engine exports (useViewportController, DCMiniMap,
|
|
553
|
+
// DCZoomToolbar, ...) MUST NOT travel into a handed-off registry item.
|
|
554
|
+
// The trick: replace `DesignCanvas`, `DCArtboard`, `DCSection` in the
|
|
555
|
+
// libMap with their static-frame variants before BFS. The static variants
|
|
556
|
+
// have empty deps, so the transitive walk never reaches the engine code.
|
|
557
|
+
if (opts.designRoot) {
|
|
558
|
+
const libPath = canvasLibPath(opts.designRoot);
|
|
559
|
+
const libFile = Bun.file(libPath);
|
|
560
|
+
if (await libFile.exists()) {
|
|
561
|
+
const libSource = await libFile.text();
|
|
562
|
+
const libMap = buildLibMap(libPath, libSource);
|
|
563
|
+
applyHandoffStaticOverrides(libMap);
|
|
564
|
+
const inlined = inlineUsedExports(tsx, libMap);
|
|
565
|
+
tsx = inlined.content;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Classify imports.
|
|
570
|
+
const { dependencies, registryDependencies } = classifyImports(opts.canvasAbsPath, tsx);
|
|
571
|
+
// @maude/canvas-lib is a dev-time virtual specifier — never ship as dep.
|
|
572
|
+
const depsFiltered = dependencies.filter((d) => d !== '@maude/canvas-lib');
|
|
573
|
+
|
|
574
|
+
// React + ReactDOM always shipped as runtime deps — the canvas authoring
|
|
575
|
+
// contract requires React 19 (DDR-012). scanImports already finds `react`
|
|
576
|
+
// when JSX is present (Bun.Transpiler tracks jsx-runtime usage), but it
|
|
577
|
+
// doesn't surface react-dom unless explicitly imported. Force-include both.
|
|
578
|
+
const depSet = new Set(depsFiltered);
|
|
579
|
+
depSet.add('react');
|
|
580
|
+
depSet.add('react-dom');
|
|
581
|
+
const finalDeps = [...depSet].sort();
|
|
582
|
+
|
|
583
|
+
// Compute slug for `name` field — kebab-case of the file stem.
|
|
584
|
+
const slug = kebabCase(path.basename(opts.canvasAbsPath, path.extname(opts.canvasAbsPath)));
|
|
585
|
+
|
|
586
|
+
// Files: index 0 is the canvas TSX (always). 1+ are CSS bundles (optional —
|
|
587
|
+
// when componentsCssPath / tokensCssPath are passed).
|
|
588
|
+
const files: RegistryItemFile[] = [
|
|
589
|
+
{
|
|
590
|
+
path: `components/${slug}.tsx`,
|
|
591
|
+
content: tsx,
|
|
592
|
+
type: 'registry:component',
|
|
593
|
+
},
|
|
594
|
+
];
|
|
595
|
+
|
|
596
|
+
let cssVars: RegistryItem['cssVars'] | undefined;
|
|
597
|
+
|
|
598
|
+
if (opts.componentsCssPath) {
|
|
599
|
+
const componentsCss = await Bun.file(opts.componentsCssPath)
|
|
600
|
+
.text()
|
|
601
|
+
.catch(() => '');
|
|
602
|
+
if (componentsCss) {
|
|
603
|
+
const classNames = collectClassNames(opts.canvasAbsPath, tsx);
|
|
604
|
+
const { css, tokens } = filterComponentsCss(componentsCss, classNames);
|
|
605
|
+
if (css.trim().length > 0) {
|
|
606
|
+
files.push({
|
|
607
|
+
path: `styles/${slug}.css`,
|
|
608
|
+
content: `${css}\n`,
|
|
609
|
+
type: 'registry:style',
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
if (opts.tokensCssPath && tokens.size > 0) {
|
|
613
|
+
const tokensCss = await Bun.file(opts.tokensCssPath)
|
|
614
|
+
.text()
|
|
615
|
+
.catch(() => '');
|
|
616
|
+
if (tokensCss) {
|
|
617
|
+
const { theme, usedCss } = filterTokensCss(tokensCss, tokens);
|
|
618
|
+
if (Object.keys(theme).length > 0) {
|
|
619
|
+
cssVars = { theme };
|
|
620
|
+
}
|
|
621
|
+
// The consumer will graft cssVars into globals.css via shadcn's CLI;
|
|
622
|
+
// we also include the raw token block as a fallback for non-shadcn
|
|
623
|
+
// consumers.
|
|
624
|
+
if (usedCss.length > 0) {
|
|
625
|
+
files.push({
|
|
626
|
+
path: `styles/${slug}.tokens.css`,
|
|
627
|
+
content: usedCss,
|
|
628
|
+
type: 'registry:theme',
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const item: RegistryItem = {
|
|
637
|
+
$schema: 'https://ui.shadcn.com/schema/registry-item.json',
|
|
638
|
+
name: slug,
|
|
639
|
+
type: 'registry:block',
|
|
640
|
+
title: opts.title,
|
|
641
|
+
description: opts.description,
|
|
642
|
+
dependencies: finalDeps,
|
|
643
|
+
registryDependencies,
|
|
644
|
+
files,
|
|
645
|
+
...(cssVars ? { cssVars } : {}),
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
// Drop undefined keys for a clean JSON.
|
|
649
|
+
if (!item.title) (item as Partial<RegistryItem>).title = undefined;
|
|
650
|
+
if (!item.description) (item as Partial<RegistryItem>).description = undefined;
|
|
651
|
+
|
|
652
|
+
return item;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function kebabCase(s: string): string {
|
|
656
|
+
return s
|
|
657
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
658
|
+
.replace(/[\s_]+/g, '-')
|
|
659
|
+
.toLowerCase()
|
|
660
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
661
|
+
.replace(/-+/g, '-')
|
|
662
|
+
.replace(/^-|-$/g, '');
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Write the registry-item.json sidecar next to a canvas. Caller picks the
|
|
667
|
+
* destination path; conventional default is `<canvas-dir>/<Slug>.registry.json`.
|
|
668
|
+
*/
|
|
669
|
+
export async function writeRegistryItem(destPath: string, item: RegistryItem): Promise<void> {
|
|
670
|
+
const json = `${JSON.stringify(item, null, 2)}\n`;
|
|
671
|
+
const tmp = `${destPath}.tmp.${Math.random().toString(36).slice(2, 10)}`;
|
|
672
|
+
await Bun.write(tmp, json);
|
|
673
|
+
const { rename } = await import('node:fs/promises');
|
|
674
|
+
await rename(tmp, destPath);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// ---------------------------------------------------------------------------
|
|
678
|
+
// CLI entry — invoked from bin/handoff.sh (the orchestrator wrapper) when
|
|
679
|
+
// /design:handoff shells out. Keeps Bun-startup costs off the hot path.
|
|
680
|
+
|
|
681
|
+
if (import.meta.main) {
|
|
682
|
+
const argv = process.argv.slice(2);
|
|
683
|
+
if (argv[0] === '--emit' && argv.length >= 2) {
|
|
684
|
+
const canvas = argv[1] as string;
|
|
685
|
+
const designRoot = argv[2];
|
|
686
|
+
const opts: EmitOptions = { canvasAbsPath: canvas };
|
|
687
|
+
if (designRoot) {
|
|
688
|
+
opts.designRoot = designRoot;
|
|
689
|
+
opts.componentsCssPath = path.join(designRoot, 'system/project/preview/_components.css');
|
|
690
|
+
opts.tokensCssPath = path.join(designRoot, 'system/project/colors_and_type.css');
|
|
691
|
+
}
|
|
692
|
+
// Try to read meta.json sidecar for title/description.
|
|
693
|
+
const metaPath = canvas.replace(/\.tsx$/, '.meta.json');
|
|
694
|
+
try {
|
|
695
|
+
const metaFile = Bun.file(metaPath);
|
|
696
|
+
if (await metaFile.exists()) {
|
|
697
|
+
const meta = (await metaFile.json()) as { title?: string; subtitle?: string };
|
|
698
|
+
opts.title = meta.title;
|
|
699
|
+
opts.description = meta.subtitle;
|
|
700
|
+
}
|
|
701
|
+
} catch {
|
|
702
|
+
// ignore — meta is optional
|
|
703
|
+
}
|
|
704
|
+
try {
|
|
705
|
+
const item = await emitRegistryItem(opts);
|
|
706
|
+
const dest = canvas.replace(/\.tsx$/, '.registry.json');
|
|
707
|
+
await writeRegistryItem(dest, item);
|
|
708
|
+
console.log(
|
|
709
|
+
JSON.stringify({ dest, files: item.files.length, deps: item.dependencies.length })
|
|
710
|
+
);
|
|
711
|
+
process.exit(0);
|
|
712
|
+
} catch (err) {
|
|
713
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
714
|
+
console.error(`handoff: ${msg}`);
|
|
715
|
+
process.exit(2);
|
|
716
|
+
}
|
|
717
|
+
} else {
|
|
718
|
+
console.error('Usage: bun run handoff.ts --emit <canvas-abs-path> [designRoot]');
|
|
719
|
+
process.exit(2);
|
|
720
|
+
}
|
|
721
|
+
}
|