@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,215 @@
|
|
|
1
|
+
// Phase 4 T7 — verify `applyHandoffStaticOverrides()` strips engine-bearing
|
|
2
|
+
// code from handoff inline output. Engine exports (useViewportController,
|
|
3
|
+
// DCMiniMap, DCZoomToolbar, ...) MUST NOT travel into a registry item.
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from 'bun:test';
|
|
6
|
+
|
|
7
|
+
import { buildLibMap, inlineUsedExports } from '../canvas-lib-inline.ts';
|
|
8
|
+
import { HANDOFF_STATIC_FRAME_EXPORTS, applyHandoffStaticOverrides } from '../handoff.ts';
|
|
9
|
+
|
|
10
|
+
const LIB_PATH = '/virtual/_lib/canvas-lib.tsx';
|
|
11
|
+
|
|
12
|
+
// Trimmed canvas-lib sample — DesignCanvas pulls useViewportController +
|
|
13
|
+
// WorldContext + DCMiniMap + DCZoomToolbar transitively (the real shape).
|
|
14
|
+
const LIB_SOURCE = `
|
|
15
|
+
import { createContext, useContext } from "react";
|
|
16
|
+
|
|
17
|
+
export function useViewportController() {
|
|
18
|
+
return { viewport: { x: 0, y: 0, zoom: 1 } };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function DCMiniMap() { return <div className="dc-mm" />; }
|
|
22
|
+
export function DCZoomToolbar() { return <div className="dc-zoom-tb" />; }
|
|
23
|
+
|
|
24
|
+
const WorldContext = createContext(null);
|
|
25
|
+
function harvestArtboards(c) { return c; }
|
|
26
|
+
function synthDefaultGrid(s) { return s; }
|
|
27
|
+
|
|
28
|
+
export function DesignCanvas({ children }) {
|
|
29
|
+
const ctl = useViewportController();
|
|
30
|
+
const seeds = harvestArtboards(children);
|
|
31
|
+
const layout = synthDefaultGrid(seeds);
|
|
32
|
+
return (
|
|
33
|
+
<WorldContext.Provider value={{ ctl, layout }}>
|
|
34
|
+
<div className="dc-canvas">
|
|
35
|
+
<div className="dc-world" style={{transform:'translate(0,0) scale(1)'}}>{children}</div>
|
|
36
|
+
<DCMiniMap />
|
|
37
|
+
<DCZoomToolbar />
|
|
38
|
+
</div>
|
|
39
|
+
</WorldContext.Provider>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function DCSection({ id, title, children }) {
|
|
44
|
+
const ctx = useContext(WorldContext);
|
|
45
|
+
return <section className="dc-section" data-dc-section={id} data-ctx={!!ctx}><h2>{title}</h2>{children}</section>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function DCArtboard({ id, label, width, height, children }) {
|
|
49
|
+
const ctx = useContext(WorldContext);
|
|
50
|
+
return (
|
|
51
|
+
<article className="dc-artboard" data-dc-screen={id} data-has-ctx={!!ctx} style={{ width, height }}>
|
|
52
|
+
<header>{label}</header>
|
|
53
|
+
<div>{children}</div>
|
|
54
|
+
</article>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const SAMPLE_CANVAS = `import { DesignCanvas, DCSection, DCArtboard } from "@maude/canvas-lib";
|
|
60
|
+
export default function X() {
|
|
61
|
+
return (
|
|
62
|
+
<DesignCanvas>
|
|
63
|
+
<DCSection id="s" title="S">
|
|
64
|
+
<DCArtboard id="a" label="A" width={100} height={100}>hi</DCArtboard>
|
|
65
|
+
</DCSection>
|
|
66
|
+
</DesignCanvas>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
describe('handoff-static-frames', () => {
|
|
72
|
+
test('HANDOFF_STATIC_FRAME_EXPORTS lists DesignCanvas + DCSection + DCArtboard', () => {
|
|
73
|
+
expect([...HANDOFF_STATIC_FRAME_EXPORTS].sort()).toEqual([
|
|
74
|
+
'DCArtboard',
|
|
75
|
+
'DCSection',
|
|
76
|
+
'DesignCanvas',
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('without overrides — engine code IS pulled into the handoff inline', () => {
|
|
81
|
+
const libMap = buildLibMap(LIB_PATH, LIB_SOURCE);
|
|
82
|
+
const r = inlineUsedExports(SAMPLE_CANVAS, libMap);
|
|
83
|
+
// BFS dragged in everything DesignCanvas needs.
|
|
84
|
+
expect(r.content).toContain('useViewportController');
|
|
85
|
+
expect(r.content).toContain('DCMiniMap');
|
|
86
|
+
expect(r.content).toContain('DCZoomToolbar');
|
|
87
|
+
expect(r.content).toContain('WorldContext');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('with overrides — engine code is stripped from handoff inline', () => {
|
|
91
|
+
const libMap = buildLibMap(LIB_PATH, LIB_SOURCE);
|
|
92
|
+
applyHandoffStaticOverrides(libMap);
|
|
93
|
+
const r = inlineUsedExports(SAMPLE_CANVAS, libMap);
|
|
94
|
+
expect(r.content).not.toContain('useViewportController');
|
|
95
|
+
expect(r.content).not.toContain('DCMiniMap');
|
|
96
|
+
expect(r.content).not.toContain('DCZoomToolbar');
|
|
97
|
+
expect(r.content).not.toContain('WorldContext');
|
|
98
|
+
expect(r.content).not.toContain('harvestArtboards');
|
|
99
|
+
expect(r.content).not.toContain('synthDefaultGrid');
|
|
100
|
+
// The static frame replacements ARE present.
|
|
101
|
+
expect(r.content).toContain('function DesignCanvas');
|
|
102
|
+
expect(r.content).toContain('function DCSection');
|
|
103
|
+
expect(r.content).toContain('function DCArtboard');
|
|
104
|
+
// The static DesignCanvas is the minimal `<div className="dc-canvas">` form.
|
|
105
|
+
expect(r.content).toContain('<div className="dc-canvas">{children}</div>');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('overrides are a no-op when the canvas does not import the frame', () => {
|
|
109
|
+
const libMap = buildLibMap(LIB_PATH, LIB_SOURCE);
|
|
110
|
+
applyHandoffStaticOverrides(libMap);
|
|
111
|
+
// Canvas only imports DCArtboard — DesignCanvas + DCSection stay unused.
|
|
112
|
+
const canvas = `import { DCArtboard } from "@maude/canvas-lib";
|
|
113
|
+
export default function Y() {
|
|
114
|
+
return <DCArtboard id="x" label="X" width={10} height={10}>hi</DCArtboard>;
|
|
115
|
+
}
|
|
116
|
+
`;
|
|
117
|
+
const r = inlineUsedExports(canvas, libMap);
|
|
118
|
+
expect(r.content).toContain('function DCArtboard');
|
|
119
|
+
expect(r.content).not.toContain('function DesignCanvas');
|
|
120
|
+
expect(r.content).not.toContain('function DCSection');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
125
|
+
// Phase 4.2 — drag + snap exports must also be stripped from handoff output.
|
|
126
|
+
// The real DCArtboard references useArtboardDrag (and friends) internally;
|
|
127
|
+
// the static-frame override breaks that chain. This test pins that behavior.
|
|
128
|
+
|
|
129
|
+
const LIB_SOURCE_4_2 = `
|
|
130
|
+
import { createContext, useContext } from "react";
|
|
131
|
+
|
|
132
|
+
export function useViewportController() { return { viewport: { x: 0, y: 0, zoom: 1 } }; }
|
|
133
|
+
export function DCMiniMap() { return <div />; }
|
|
134
|
+
export function DCZoomToolbar() { return <div />; }
|
|
135
|
+
|
|
136
|
+
const WorldContext = createContext(null);
|
|
137
|
+
function harvestArtboards(c) { return c; }
|
|
138
|
+
function synthDefaultGrid(s) { return s; }
|
|
139
|
+
|
|
140
|
+
export function computeSnap(p, others, opts) { return { x: p.x, y: p.y, guides: [] }; }
|
|
141
|
+
export function useSnapGuides() { /* placeholder */ }
|
|
142
|
+
export function useArtboardDrag(opts) {
|
|
143
|
+
const _snap = computeSnap;
|
|
144
|
+
return { bindHandle: () => ({}), dragState: { kind: "idle" } };
|
|
145
|
+
}
|
|
146
|
+
const DragStateContext = createContext(null);
|
|
147
|
+
export function SnapGuideOverlay() {
|
|
148
|
+
const ctx = useContext(DragStateContext);
|
|
149
|
+
return ctx ? <div className="dc-snap-guide" /> : null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function DesignCanvas({ children }) {
|
|
153
|
+
const ctl = useViewportController();
|
|
154
|
+
const seeds = harvestArtboards(children);
|
|
155
|
+
const layout = synthDefaultGrid(seeds);
|
|
156
|
+
return (
|
|
157
|
+
<WorldContext.Provider value={{ ctl, layout }}>
|
|
158
|
+
<DragStateContext.Provider value={null}>
|
|
159
|
+
<div className="dc-canvas">{children}<DCMiniMap /><DCZoomToolbar /><SnapGuideOverlay /></div>
|
|
160
|
+
</DragStateContext.Provider>
|
|
161
|
+
</WorldContext.Provider>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function DCSection({ id, title, children }) {
|
|
166
|
+
return <section data-id={id}><h2>{title}</h2>{children}</section>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function DCArtboard({ id, label, width, height, children }) {
|
|
170
|
+
const drag = useArtboardDrag({ artboardId: id });
|
|
171
|
+
return (
|
|
172
|
+
<article className="dc-artboard" data-dc-screen={id} {...drag.bindHandle()} style={{ width, height }}>
|
|
173
|
+
<header>{label}</header>
|
|
174
|
+
<div>{children}</div>
|
|
175
|
+
</article>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
`;
|
|
179
|
+
|
|
180
|
+
const PHASE_4_2_CANVAS = `import { DesignCanvas, DCSection, DCArtboard } from "@maude/canvas-lib";
|
|
181
|
+
export default function X() {
|
|
182
|
+
return (
|
|
183
|
+
<DesignCanvas>
|
|
184
|
+
<DCSection id="s" title="S">
|
|
185
|
+
<DCArtboard id="a" label="A" width={100} height={100}>hi</DCArtboard>
|
|
186
|
+
</DCSection>
|
|
187
|
+
</DesignCanvas>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
`;
|
|
191
|
+
|
|
192
|
+
describe('handoff-static-frames / Phase 4.2 drag + snap exports', () => {
|
|
193
|
+
test('without overrides — drag + snap engine code IS pulled in (regression baseline)', () => {
|
|
194
|
+
const libMap = buildLibMap(LIB_PATH, LIB_SOURCE_4_2);
|
|
195
|
+
const r = inlineUsedExports(PHASE_4_2_CANVAS, libMap);
|
|
196
|
+
// DCArtboard depends on useArtboardDrag → BFS reaches it.
|
|
197
|
+
expect(r.content).toContain('useArtboardDrag');
|
|
198
|
+
// DesignCanvas wraps SnapGuideOverlay → BFS reaches it.
|
|
199
|
+
expect(r.content).toContain('SnapGuideOverlay');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('with overrides — drag + snap engine code is stripped', () => {
|
|
203
|
+
const libMap = buildLibMap(LIB_PATH, LIB_SOURCE_4_2);
|
|
204
|
+
applyHandoffStaticOverrides(libMap);
|
|
205
|
+
const r = inlineUsedExports(PHASE_4_2_CANVAS, libMap);
|
|
206
|
+
expect(r.content).not.toContain('useArtboardDrag');
|
|
207
|
+
expect(r.content).not.toContain('SnapGuideOverlay');
|
|
208
|
+
expect(r.content).not.toContain('computeSnap');
|
|
209
|
+
expect(r.content).not.toContain('useSnapGuides');
|
|
210
|
+
expect(r.content).not.toContain('DragStateContext');
|
|
211
|
+
// Static frames still land as the minimal markup.
|
|
212
|
+
expect(r.content).toContain('function DesignCanvas');
|
|
213
|
+
expect(r.content).toContain('function DCArtboard');
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// handoff.ts — Phase 3.6 Task 7 + 12b. Tests the shadcn registry-item emitter:
|
|
2
|
+
// data-cd-id strip, import classification, className harvest, CSS subset
|
|
3
|
+
// extraction, full emit round-trip.
|
|
4
|
+
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
6
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
import { transpileCanvasSource } from '../canvas-pipeline.ts';
|
|
11
|
+
import {
|
|
12
|
+
type RegistryItem,
|
|
13
|
+
classifyImports,
|
|
14
|
+
collectClassNames,
|
|
15
|
+
emitRegistryItem,
|
|
16
|
+
filterComponentsCss,
|
|
17
|
+
filterTokensCss,
|
|
18
|
+
stripDataCdId,
|
|
19
|
+
writeRegistryItem,
|
|
20
|
+
} from '../handoff.ts';
|
|
21
|
+
|
|
22
|
+
const CANVAS = '/abs/Canvas.tsx';
|
|
23
|
+
|
|
24
|
+
describe('handoff / stripDataCdId', () => {
|
|
25
|
+
test('removes pipeline-emitted data-cd-id attrs', () => {
|
|
26
|
+
const src = `function Demo() { return <div className="btn">x</div>; }`;
|
|
27
|
+
const withIds = transpileCanvasSource(CANVAS, src).withIds;
|
|
28
|
+
expect(withIds).toContain('data-cd-id=');
|
|
29
|
+
const stripped = stripDataCdId(CANVAS, withIds);
|
|
30
|
+
expect(stripped).not.toContain('data-cd-id');
|
|
31
|
+
// Same shape as the original source (data-cd-id strip is idempotent on the
|
|
32
|
+
// original).
|
|
33
|
+
expect(stripped).toBe(src);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('strip is idempotent on already-stripped source', () => {
|
|
37
|
+
const src = 'function Demo() { return <div>x</div>; }';
|
|
38
|
+
expect(stripDataCdId(CANVAS, src)).toBe(src);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('preserves other attributes byte-for-byte', () => {
|
|
42
|
+
const src = `function Demo() { return <button className="btn" aria-label="ok" data-cd-id="deadbeef">go</button>; }`;
|
|
43
|
+
const out = stripDataCdId(CANVAS, src);
|
|
44
|
+
expect(out).toContain('className="btn"');
|
|
45
|
+
expect(out).toContain('aria-label="ok"');
|
|
46
|
+
expect(out).not.toContain('data-cd-id');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('handoff / classifyImports', () => {
|
|
51
|
+
test('separates npm specifiers from registry deps', () => {
|
|
52
|
+
const src = `
|
|
53
|
+
import { useState } from "react";
|
|
54
|
+
import * as ReactDOM from "react-dom/client";
|
|
55
|
+
import { motion } from "motion/react";
|
|
56
|
+
import { Button } from "@/components/ui/button";
|
|
57
|
+
import { Card } from "@/components/ui/card";
|
|
58
|
+
import { localThing } from "./local";
|
|
59
|
+
function Demo() { return <div/>; }
|
|
60
|
+
`;
|
|
61
|
+
const r = classifyImports(CANVAS, src);
|
|
62
|
+
expect(r.dependencies).toEqual(['motion', 'react', 'react-dom']);
|
|
63
|
+
expect(r.registryDependencies).toEqual(['button', 'card']);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('relative imports are ignored', () => {
|
|
67
|
+
const src = `
|
|
68
|
+
import { sibling } from "./a";
|
|
69
|
+
import { up } from "../b/c";
|
|
70
|
+
function Demo() { return <div/>; }
|
|
71
|
+
`;
|
|
72
|
+
const r = classifyImports(CANVAS, src);
|
|
73
|
+
// Bun.Transpiler.scanImports auto-surfaces "react" when JSX is present
|
|
74
|
+
// (it's the implicit jsx-runtime import). That's fine — react is in the
|
|
75
|
+
// mandatory dep floor anyway. The relative imports must NOT appear.
|
|
76
|
+
expect(r.dependencies.every((d) => !d.startsWith('.'))).toBe(true);
|
|
77
|
+
expect(r.dependencies).not.toContain('./a');
|
|
78
|
+
expect(r.registryDependencies).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('handoff / collectClassNames', () => {
|
|
83
|
+
test('gathers tokens from className literals', () => {
|
|
84
|
+
const src = `
|
|
85
|
+
function Demo() {
|
|
86
|
+
return (
|
|
87
|
+
<div className="btn btn--ghost">
|
|
88
|
+
<span className="sku">x</span>
|
|
89
|
+
<em className={"tile " + "tile--active"}>y</em>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
94
|
+
const names = collectClassNames(CANVAS, src);
|
|
95
|
+
expect(names.has('btn')).toBe(true);
|
|
96
|
+
expect(names.has('btn--ghost')).toBe(true);
|
|
97
|
+
expect(names.has('sku')).toBe(true);
|
|
98
|
+
// String concat → first literal contributes "tile ", second contributes
|
|
99
|
+
// "tile--active". Both surface.
|
|
100
|
+
expect(names.has('tile')).toBe(true);
|
|
101
|
+
expect(names.has('tile--active')).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('TemplateLiteral quasis contribute', () => {
|
|
105
|
+
const src = `
|
|
106
|
+
function Demo() {
|
|
107
|
+
const active = true;
|
|
108
|
+
return <div className={\`btn \${active ? 'btn--active' : ''}\`}>x</div>;
|
|
109
|
+
}
|
|
110
|
+
`;
|
|
111
|
+
const names = collectClassNames(CANVAS, src);
|
|
112
|
+
expect(names.has('btn')).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('handoff / filterComponentsCss', () => {
|
|
117
|
+
const cssSource = `
|
|
118
|
+
/* Buttons */
|
|
119
|
+
.btn {
|
|
120
|
+
padding: 6px var(--space-3);
|
|
121
|
+
color: var(--fg-0);
|
|
122
|
+
}
|
|
123
|
+
.btn--ghost {
|
|
124
|
+
background: transparent;
|
|
125
|
+
}
|
|
126
|
+
.btn:hover { color: var(--accent); }
|
|
127
|
+
.sku {
|
|
128
|
+
letter-spacing: var(--tracking-sku);
|
|
129
|
+
}
|
|
130
|
+
.unused {
|
|
131
|
+
color: red;
|
|
132
|
+
}
|
|
133
|
+
@media (min-width: 1200px) {
|
|
134
|
+
.btn { padding: 8px var(--space-4); }
|
|
135
|
+
.unused { font-size: 16px; }
|
|
136
|
+
}
|
|
137
|
+
`;
|
|
138
|
+
|
|
139
|
+
test('keeps rules whose base class is in the keep set', () => {
|
|
140
|
+
const r = filterComponentsCss(cssSource, new Set(['btn']));
|
|
141
|
+
expect(r.css).toContain('.btn {');
|
|
142
|
+
expect(r.css).toContain('.btn--ghost');
|
|
143
|
+
expect(r.css).toContain('.btn:hover');
|
|
144
|
+
expect(r.css).not.toContain('.unused');
|
|
145
|
+
expect(r.css).not.toContain('.sku');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('collects var(--*) tokens from kept rules', () => {
|
|
149
|
+
const r = filterComponentsCss(cssSource, new Set(['btn']));
|
|
150
|
+
expect(r.tokens.has('--space-3')).toBe(true);
|
|
151
|
+
expect(r.tokens.has('--fg-0')).toBe(true);
|
|
152
|
+
expect(r.tokens.has('--accent')).toBe(true);
|
|
153
|
+
expect(r.tokens.has('--tracking-sku')).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('recurses into @media rules', () => {
|
|
157
|
+
const r = filterComponentsCss(cssSource, new Set(['btn']));
|
|
158
|
+
expect(r.css).toContain('@media');
|
|
159
|
+
expect(r.css).toContain('--space-4');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('empty keep set produces empty output', () => {
|
|
163
|
+
const r = filterComponentsCss(cssSource, new Set());
|
|
164
|
+
expect(r.css.trim()).toBe('');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('handoff / filterTokensCss', () => {
|
|
169
|
+
const tokensCss = `
|
|
170
|
+
:root {
|
|
171
|
+
--fg-0: #111;
|
|
172
|
+
--bg-0: #fff;
|
|
173
|
+
--accent: #d97706;
|
|
174
|
+
--unused: #abc;
|
|
175
|
+
}
|
|
176
|
+
`;
|
|
177
|
+
|
|
178
|
+
test('extracts only requested tokens', () => {
|
|
179
|
+
const r = filterTokensCss(tokensCss, new Set(['--fg-0', '--accent']));
|
|
180
|
+
expect(r.theme['fg-0']).toBe('#111');
|
|
181
|
+
expect(r.theme.accent).toBe('#d97706');
|
|
182
|
+
expect(r.theme.unused).toBeUndefined();
|
|
183
|
+
expect(r.usedCss).toContain('--fg-0: #111;');
|
|
184
|
+
expect(r.usedCss).toContain('--accent: #d97706;');
|
|
185
|
+
expect(r.usedCss).not.toContain('--unused');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('handoff / emitRegistryItem end-to-end', () => {
|
|
190
|
+
let tmpDir = '';
|
|
191
|
+
beforeAll(() => {
|
|
192
|
+
tmpDir = mkdtempSync(path.join(tmpdir(), 'handoff-'));
|
|
193
|
+
});
|
|
194
|
+
afterAll(() => {
|
|
195
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('emits valid registry-item with stripped TSX + bundled CSS + cssVars', async () => {
|
|
199
|
+
// Lay out a minimal canvas + DS in the tmp dir.
|
|
200
|
+
const canvasAbs = path.join(tmpDir, 'Test Canvas.tsx');
|
|
201
|
+
const componentsCssAbs = path.join(tmpDir, '_components.css');
|
|
202
|
+
const tokensCssAbs = path.join(tmpDir, 'tokens.css');
|
|
203
|
+
await Bun.write(
|
|
204
|
+
canvasAbs,
|
|
205
|
+
`import { useState } from "react";
|
|
206
|
+
import { Button } from "@/components/ui/button";
|
|
207
|
+
export default function TestCanvas() {
|
|
208
|
+
return (
|
|
209
|
+
<div className="mdcc">
|
|
210
|
+
<h1 className="sku">Hi</h1>
|
|
211
|
+
<button className="btn btn--ghost" data-cd-id="cafef00d">Go</button>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}`
|
|
215
|
+
);
|
|
216
|
+
await Bun.write(
|
|
217
|
+
componentsCssAbs,
|
|
218
|
+
`.btn { padding: 6px var(--space-3); color: var(--fg-0); }
|
|
219
|
+
.btn--ghost { background: transparent; }
|
|
220
|
+
.sku { letter-spacing: var(--tracking-sku); }
|
|
221
|
+
.unused { color: red; }`
|
|
222
|
+
);
|
|
223
|
+
await Bun.write(
|
|
224
|
+
tokensCssAbs,
|
|
225
|
+
':root { --space-3: 12px; --fg-0: #111; --tracking-sku: 0.04em; --unused: #abc; }'
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const item = await emitRegistryItem({
|
|
229
|
+
canvasAbsPath: canvasAbs,
|
|
230
|
+
title: 'Test Canvas',
|
|
231
|
+
description: 'Smoke harness',
|
|
232
|
+
componentsCssPath: componentsCssAbs,
|
|
233
|
+
tokensCssPath: tokensCssAbs,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(item.$schema).toBe('https://ui.shadcn.com/schema/registry-item.json');
|
|
237
|
+
expect(item.name).toBe('test-canvas');
|
|
238
|
+
expect(item.type).toBe('registry:block');
|
|
239
|
+
expect(item.title).toBe('Test Canvas');
|
|
240
|
+
expect(item.description).toBe('Smoke harness');
|
|
241
|
+
expect(item.dependencies).toContain('react');
|
|
242
|
+
expect(item.dependencies).toContain('react-dom');
|
|
243
|
+
expect(item.registryDependencies).toEqual(['button']);
|
|
244
|
+
|
|
245
|
+
// files[0] is the TSX, sans data-cd-id.
|
|
246
|
+
expect(item.files[0]?.path).toBe('components/test-canvas.tsx');
|
|
247
|
+
expect(item.files[0]?.type).toBe('registry:component');
|
|
248
|
+
expect(item.files[0]?.content).not.toContain('data-cd-id');
|
|
249
|
+
|
|
250
|
+
// CSS bundle present + token bundle present + cssVars present.
|
|
251
|
+
const cssFile = item.files.find((f) => f.type === 'registry:style');
|
|
252
|
+
expect(cssFile).toBeDefined();
|
|
253
|
+
expect(cssFile?.content).toContain('.btn');
|
|
254
|
+
expect(cssFile?.content).toContain('.sku');
|
|
255
|
+
expect(cssFile?.content).not.toContain('.unused');
|
|
256
|
+
|
|
257
|
+
const tokenFile = item.files.find((f) => f.type === 'registry:theme');
|
|
258
|
+
expect(tokenFile?.content).toContain('--space-3');
|
|
259
|
+
expect(tokenFile?.content).toContain('--fg-0');
|
|
260
|
+
expect(tokenFile?.content).not.toContain('--unused');
|
|
261
|
+
|
|
262
|
+
expect(item.cssVars?.theme?.['space-3']).toBe('12px');
|
|
263
|
+
expect(item.cssVars?.theme?.['fg-0']).toBe('#111');
|
|
264
|
+
|
|
265
|
+
// Persisted form is valid JSON.
|
|
266
|
+
const dest = path.join(tmpDir, 'Test Canvas.registry.json');
|
|
267
|
+
await writeRegistryItem(dest, item);
|
|
268
|
+
const parsed: RegistryItem = await Bun.file(dest).json();
|
|
269
|
+
expect(parsed.name).toBe('test-canvas');
|
|
270
|
+
expect(parsed.files.length).toBe(item.files.length);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('emit without CSS paths produces TSX-only registry-item', async () => {
|
|
274
|
+
const canvasAbs = path.join(tmpDir, 'NoCss.tsx');
|
|
275
|
+
await Bun.write(canvasAbs, 'export default function NoCss() { return <div>x</div>; }');
|
|
276
|
+
const item = await emitRegistryItem({ canvasAbsPath: canvasAbs });
|
|
277
|
+
expect(item.files.length).toBe(1);
|
|
278
|
+
expect(item.files[0]?.type).toBe('registry:component');
|
|
279
|
+
expect(item.cssVars).toBeUndefined();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Smoke: snapshot writer + rollback reader round-trip (history.ts directly,
|
|
2
|
+
// no server process).
|
|
3
|
+
|
|
4
|
+
import { describe, expect, test } from 'bun:test';
|
|
5
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
import { createContext } from '../context.ts';
|
|
10
|
+
import { createHistory } from '../history.ts';
|
|
11
|
+
|
|
12
|
+
function withSandbox<T>(fn: (ctx: ReturnType<typeof createContext>) => Promise<T>): Promise<T> {
|
|
13
|
+
const root = mkdtempSync(join(tmpdir(), 'mdcc-history-'));
|
|
14
|
+
mkdirSync(join(root, '.design', 'ui'), { recursive: true });
|
|
15
|
+
writeFileSync(join(root, '.design', 'config.json'), '{"name":"t"}');
|
|
16
|
+
writeFileSync(join(root, '.design', 'ui', 'a.html'), '<doc>a</doc>');
|
|
17
|
+
|
|
18
|
+
// createContext() reads --root from argv; spoof it.
|
|
19
|
+
const origArgv = process.argv;
|
|
20
|
+
process.argv = [...origArgv, '--root', root];
|
|
21
|
+
try {
|
|
22
|
+
const ctx = createContext();
|
|
23
|
+
return fn(ctx);
|
|
24
|
+
} finally {
|
|
25
|
+
process.argv = origArgv;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('history.ts', () => {
|
|
30
|
+
test('writeSnapshot + readSnapshot round-trip', async () => {
|
|
31
|
+
await withSandbox(async (ctx) => {
|
|
32
|
+
const hist = createHistory(ctx);
|
|
33
|
+
const wrote = await hist.writeSnapshot('.design/ui/a.html', '<doc>v1</doc>', 'pre-edit');
|
|
34
|
+
expect(wrote.slug).toBe('ui-a');
|
|
35
|
+
expect(wrote.reason).toBe('pre-edit');
|
|
36
|
+
expect(wrote.size).toBeGreaterThan(0);
|
|
37
|
+
|
|
38
|
+
const list = await hist.listSnapshots('.design/ui/a.html');
|
|
39
|
+
expect(list.length).toBe(1);
|
|
40
|
+
expect(list[0]?.ts).toBe(wrote.ts);
|
|
41
|
+
|
|
42
|
+
const read = await hist.readSnapshot('.design/ui/a.html', wrote.ts);
|
|
43
|
+
expect(read).not.toBeNull();
|
|
44
|
+
expect(new TextDecoder().decode(read?.content)).toBe('<doc>v1</doc>');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('rollback overwrites the target file', async () => {
|
|
49
|
+
await withSandbox(async (ctx) => {
|
|
50
|
+
const hist = createHistory(ctx);
|
|
51
|
+
const fp = '.design/ui/a.html';
|
|
52
|
+
// Take a snapshot of the original content, mutate the file, then roll back.
|
|
53
|
+
const original = await Bun.file(join(ctx.paths.repoRoot, fp)).text();
|
|
54
|
+
const snap = await hist.writeSnapshot(fp, original, 'baseline');
|
|
55
|
+
await Bun.write(join(ctx.paths.repoRoot, fp), '<doc>modified</doc>');
|
|
56
|
+
const restored = await hist.rollback(fp, snap.ts);
|
|
57
|
+
expect(restored).not.toBeNull();
|
|
58
|
+
const after = await Bun.file(join(ctx.paths.repoRoot, fp)).text();
|
|
59
|
+
expect(after).toBe(original);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// hmr-broadcast — Phase 3.6.1 Task 8. fs:any → canvas-hmr WS message classifier.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from 'bun:test';
|
|
4
|
+
|
|
5
|
+
import { type Context, createBus } from '../context.ts';
|
|
6
|
+
import { HMR_DEBOUNCE_MS, type HmrMessage, createHmrBroadcaster } from '../hmr-broadcast.ts';
|
|
7
|
+
|
|
8
|
+
function mkCtx(): Context {
|
|
9
|
+
const bus = createBus();
|
|
10
|
+
return {
|
|
11
|
+
cfg: {} as Context['cfg'],
|
|
12
|
+
projectLabel: '',
|
|
13
|
+
paths: {} as Context['paths'],
|
|
14
|
+
bus,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function awaitNextFlush(): Promise<void> {
|
|
19
|
+
await new Promise((r) => setTimeout(r, HMR_DEBOUNCE_MS + 20));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('hmr-broadcast / classification', () => {
|
|
23
|
+
test('.tsx → mode: module', async () => {
|
|
24
|
+
const ctx = mkCtx();
|
|
25
|
+
const got: HmrMessage[] = [];
|
|
26
|
+
const h = createHmrBroadcaster(ctx, (m) => got.push(m));
|
|
27
|
+
ctx.bus.emit('fs:any', 'ui/Docs Site.tsx');
|
|
28
|
+
await awaitNextFlush();
|
|
29
|
+
expect(got).toHaveLength(1);
|
|
30
|
+
expect(got[0]?.mode).toBe('module');
|
|
31
|
+
expect(got[0]?.file).toBe('ui/Docs Site.tsx');
|
|
32
|
+
expect(got[0]?.scope).toBe('canvas');
|
|
33
|
+
h.stop();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('.css → mode: css', async () => {
|
|
37
|
+
const ctx = mkCtx();
|
|
38
|
+
const got: HmrMessage[] = [];
|
|
39
|
+
const h = createHmrBroadcaster(ctx, (m) => got.push(m));
|
|
40
|
+
ctx.bus.emit('fs:any', 'ui/Docs Site.css');
|
|
41
|
+
await awaitNextFlush();
|
|
42
|
+
expect(got[0]?.mode).toBe('css');
|
|
43
|
+
h.stop();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('_lib/* → mode: hard, scope: lib', async () => {
|
|
47
|
+
const ctx = mkCtx();
|
|
48
|
+
const got: HmrMessage[] = [];
|
|
49
|
+
const h = createHmrBroadcaster(ctx, (m) => got.push(m));
|
|
50
|
+
ctx.bus.emit('fs:any', '_lib/canvas-lib.tsx');
|
|
51
|
+
await awaitNextFlush();
|
|
52
|
+
expect(got[0]?.mode).toBe('hard');
|
|
53
|
+
expect(got[0]?.scope).toBe('lib');
|
|
54
|
+
h.stop();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('unrelated extensions are dropped', async () => {
|
|
58
|
+
const ctx = mkCtx();
|
|
59
|
+
const got: HmrMessage[] = [];
|
|
60
|
+
const h = createHmrBroadcaster(ctx, (m) => got.push(m));
|
|
61
|
+
ctx.bus.emit('fs:any', 'ui/screenshot.png');
|
|
62
|
+
ctx.bus.emit('fs:any', '_locator.json');
|
|
63
|
+
await awaitNextFlush();
|
|
64
|
+
expect(got).toHaveLength(0);
|
|
65
|
+
h.stop();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('hmr-broadcast / debouncing', () => {
|
|
70
|
+
test('two rapid events collapse to one message', async () => {
|
|
71
|
+
const ctx = mkCtx();
|
|
72
|
+
const got: HmrMessage[] = [];
|
|
73
|
+
const h = createHmrBroadcaster(ctx, (m) => got.push(m));
|
|
74
|
+
ctx.bus.emit('fs:any', 'ui/Smoke.tsx');
|
|
75
|
+
ctx.bus.emit('fs:any', 'ui/Smoke.tsx');
|
|
76
|
+
ctx.bus.emit('fs:any', 'ui/Smoke.tsx');
|
|
77
|
+
await awaitNextFlush();
|
|
78
|
+
expect(got).toHaveLength(1);
|
|
79
|
+
h.stop();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('coalescing prefers hard > module > css', async () => {
|
|
83
|
+
const ctx = mkCtx();
|
|
84
|
+
const got: HmrMessage[] = [];
|
|
85
|
+
const h = createHmrBroadcaster(ctx, (m) => got.push(m));
|
|
86
|
+
// Burst of mixed events; final classification should be the strongest
|
|
87
|
+
// (hard, from the _lib change).
|
|
88
|
+
ctx.bus.emit('fs:any', 'ui/Smoke.css');
|
|
89
|
+
ctx.bus.emit('fs:any', 'ui/Smoke.tsx');
|
|
90
|
+
ctx.bus.emit('fs:any', '_lib/canvas-lib.tsx');
|
|
91
|
+
await awaitNextFlush();
|
|
92
|
+
expect(got).toHaveLength(1);
|
|
93
|
+
expect(got[0]?.mode).toBe('hard');
|
|
94
|
+
h.stop();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('hmr-broadcast / stop', () => {
|
|
99
|
+
test('stop() prevents further broadcasts', async () => {
|
|
100
|
+
const ctx = mkCtx();
|
|
101
|
+
const got: HmrMessage[] = [];
|
|
102
|
+
const h = createHmrBroadcaster(ctx, (m) => got.push(m));
|
|
103
|
+
h.stop();
|
|
104
|
+
ctx.bus.emit('fs:any', 'ui/X.tsx');
|
|
105
|
+
await awaitNextFlush();
|
|
106
|
+
expect(got).toHaveLength(0);
|
|
107
|
+
});
|
|
108
|
+
});
|