@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,125 @@
|
|
|
1
|
+
// _history/<slug>/ snapshot stack — consumed by /design:rollback.
|
|
2
|
+
// Snapshots are written as opaque artifacts (full file content + meta.json).
|
|
3
|
+
// The orchestrator (slash commands) used to write these via Bash; we now expose
|
|
4
|
+
// a server-side API so the same logic lives in one place and is callable both
|
|
5
|
+
// from the WS layer and from a future server-driven auto-snapshot hook.
|
|
6
|
+
|
|
7
|
+
import type { Dirent } from 'node:fs';
|
|
8
|
+
import { readdir } from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
import type { Context } from './context.ts';
|
|
12
|
+
|
|
13
|
+
export interface Snapshot {
|
|
14
|
+
slug: string;
|
|
15
|
+
ts: string; // ISO
|
|
16
|
+
reason: string; // free-form: "pre-edit", "post-edit", "manual", etc.
|
|
17
|
+
contentPath: string; // absolute path to the snapshot blob inside _history/
|
|
18
|
+
metaPath: string;
|
|
19
|
+
size: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface History {
|
|
23
|
+
snapshotPath(slug: string, ts: string, ext?: string): string;
|
|
24
|
+
metaPath(slug: string, ts: string): string;
|
|
25
|
+
writeSnapshot(file: string, contentBytes: Uint8Array | string, reason: string): Promise<Snapshot>;
|
|
26
|
+
listSnapshots(file: string): Promise<Snapshot[]>;
|
|
27
|
+
readSnapshot(file: string, ts: string): Promise<{ content: Uint8Array; meta: Snapshot } | null>;
|
|
28
|
+
rollback(file: string, ts: string): Promise<{ content: Uint8Array; meta: Snapshot } | null>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function fileSlug(file: string, designRel: string): string {
|
|
32
|
+
let p = file.replace(/^\/+|\/+$/g, '');
|
|
33
|
+
try {
|
|
34
|
+
p = decodeURIComponent(p);
|
|
35
|
+
} catch {
|
|
36
|
+
/* ignore */
|
|
37
|
+
}
|
|
38
|
+
const prefix = `${designRel.replace(/^\/+|\/+$/g, '')}/`;
|
|
39
|
+
if (p.startsWith(prefix)) p = p.slice(prefix.length);
|
|
40
|
+
return p
|
|
41
|
+
.replace(/\//g, '-')
|
|
42
|
+
.replace(/\s+/g, '_')
|
|
43
|
+
.replace(/\.html$/i, '')
|
|
44
|
+
.replace(/^\.+/, '')
|
|
45
|
+
.toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function tsForFilename(ts: string): string {
|
|
49
|
+
return ts.replace(/[:.]/g, '-');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createHistory(ctx: Context): History {
|
|
53
|
+
function snapshotPath(slug: string, ts: string, ext = '.html'): string {
|
|
54
|
+
return path.join(ctx.paths.historyDir, slug, `${tsForFilename(ts)}${ext}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function metaPath(slug: string, ts: string): string {
|
|
58
|
+
return path.join(ctx.paths.historyDir, slug, `${tsForFilename(ts)}.json`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function writeSnapshot(file: string, contentBytes: Uint8Array | string, reason: string) {
|
|
62
|
+
const slug = fileSlug(file, ctx.paths.designRel);
|
|
63
|
+
const ts = new Date().toISOString();
|
|
64
|
+
const contentPath = snapshotPath(slug, ts);
|
|
65
|
+
const meta: Snapshot = {
|
|
66
|
+
slug,
|
|
67
|
+
ts,
|
|
68
|
+
reason,
|
|
69
|
+
contentPath,
|
|
70
|
+
metaPath: metaPath(slug, ts),
|
|
71
|
+
size:
|
|
72
|
+
typeof contentBytes === 'string'
|
|
73
|
+
? Buffer.byteLength(contentBytes, 'utf8')
|
|
74
|
+
: contentBytes.byteLength,
|
|
75
|
+
};
|
|
76
|
+
await Bun.write(contentPath, contentBytes);
|
|
77
|
+
await Bun.write(meta.metaPath, JSON.stringify({ ...meta, file }, null, 2));
|
|
78
|
+
return meta;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function listSnapshots(file: string): Promise<Snapshot[]> {
|
|
82
|
+
const slug = fileSlug(file, ctx.paths.designRel);
|
|
83
|
+
const dir = path.join(ctx.paths.historyDir, slug);
|
|
84
|
+
let entries: Dirent[];
|
|
85
|
+
try {
|
|
86
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
const out: Snapshot[] = [];
|
|
91
|
+
for (const e of entries) {
|
|
92
|
+
if (!e.isFile() || !e.name.endsWith('.json')) continue;
|
|
93
|
+
try {
|
|
94
|
+
const meta = JSON.parse(await Bun.file(path.join(dir, e.name)).text()) as Snapshot;
|
|
95
|
+
if (meta?.ts && meta.contentPath) out.push(meta);
|
|
96
|
+
} catch {
|
|
97
|
+
/* ignore corrupt sidecar */
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function readSnapshot(file: string, ts: string) {
|
|
104
|
+
const slug = fileSlug(file, ctx.paths.designRel);
|
|
105
|
+
const m = metaPath(slug, ts);
|
|
106
|
+
const c = snapshotPath(slug, ts);
|
|
107
|
+
try {
|
|
108
|
+
const meta = JSON.parse(await Bun.file(m).text()) as Snapshot;
|
|
109
|
+
const content = new Uint8Array(await Bun.file(c).arrayBuffer());
|
|
110
|
+
return { content, meta };
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function rollback(file: string, ts: string) {
|
|
117
|
+
const r = await readSnapshot(file, ts);
|
|
118
|
+
if (!r) return null;
|
|
119
|
+
const target = path.join(ctx.paths.repoRoot, file);
|
|
120
|
+
await Bun.write(target, r.content);
|
|
121
|
+
return r;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { snapshotPath, metaPath, writeSnapshot, listSnapshots, readSnapshot, rollback };
|
|
125
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// HMR broadcaster (Phase 3.6.1 Task 8).
|
|
2
|
+
//
|
|
3
|
+
// Bridges fs-watch.ts → ws.ts: classifies each change event under the design
|
|
4
|
+
// root and emits a `canvas-hmr` WS message. The iframe-side client (inlined in
|
|
5
|
+
// _shell.html) reacts to the message:
|
|
6
|
+
//
|
|
7
|
+
// - `mode: "css"` → swap <link> href with a cache-bust query (no module
|
|
8
|
+
// reload, no React state loss). Sub-150ms latency.
|
|
9
|
+
// - `mode: "module"` → location.reload() of the canvas iframe. State is lost
|
|
10
|
+
// but the change always picks up. <250ms latency.
|
|
11
|
+
// - `mode: "hard"` → location.reload() (used when canvas-lib.tsx or any
|
|
12
|
+
// `_lib/**` file changes — every open canvas needs to
|
|
13
|
+
// re-bundle).
|
|
14
|
+
//
|
|
15
|
+
// We deliberately do NOT try to wire Bun's `import.meta.hot.accept(...)`. Bun
|
|
16
|
+
// supports HMR in `bun dev` mode (HTML import roots), but canvases here are
|
|
17
|
+
// loaded via the importmap + Bun.build-produced ESM — there's no React Fast
|
|
18
|
+
// Refresh runtime to register with. Full-reload is the reliable path.
|
|
19
|
+
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
|
|
22
|
+
import type { Context } from './context.ts';
|
|
23
|
+
|
|
24
|
+
const DEBOUNCE_MS = 50;
|
|
25
|
+
|
|
26
|
+
export interface HmrMessage {
|
|
27
|
+
type: 'canvas-hmr';
|
|
28
|
+
mode: 'css' | 'module' | 'hard';
|
|
29
|
+
/**
|
|
30
|
+
* Canvas-relative path of the file that changed, slash-normalised. Absent
|
|
31
|
+
* when mode === 'hard' (the change is global — every canvas reloads).
|
|
32
|
+
*/
|
|
33
|
+
file?: string;
|
|
34
|
+
/** Cache-bust token — etag-like. Caller appends to <link> href. */
|
|
35
|
+
version: number;
|
|
36
|
+
/** Echo of `_lib`-scoped changes for debug + scope reasoning. */
|
|
37
|
+
scope?: 'lib' | 'canvas';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface HmrBroadcaster {
|
|
41
|
+
/** Stop subscribing to fs-watch events. */
|
|
42
|
+
stop(): void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Subscribe to fs:any / fs:css events. Debounces same-path changes, classifies
|
|
47
|
+
* the change, and forwards via `broadcast`. `broadcast` is wired to the
|
|
48
|
+
* existing ws.ts fanout; tests inject a stub.
|
|
49
|
+
*/
|
|
50
|
+
export function createHmrBroadcaster(
|
|
51
|
+
ctx: Context,
|
|
52
|
+
broadcast: (msg: HmrMessage) => void
|
|
53
|
+
): HmrBroadcaster {
|
|
54
|
+
let pending: ReturnType<typeof setTimeout> | null = null;
|
|
55
|
+
let pendingMsg: HmrMessage | null = null;
|
|
56
|
+
|
|
57
|
+
function flush() {
|
|
58
|
+
if (pendingMsg) broadcast(pendingMsg);
|
|
59
|
+
pendingMsg = null;
|
|
60
|
+
pending = null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function classify(filename: string): HmrMessage | null {
|
|
64
|
+
const rel = filename.replace(/\\/g, '/');
|
|
65
|
+
const ext = path.extname(rel).toLowerCase();
|
|
66
|
+
const version = Date.now();
|
|
67
|
+
if (rel.startsWith('_lib/')) {
|
|
68
|
+
return { type: 'canvas-hmr', mode: 'hard', scope: 'lib', version };
|
|
69
|
+
}
|
|
70
|
+
if (ext === '.css') {
|
|
71
|
+
return { type: 'canvas-hmr', mode: 'css', file: rel, version, scope: 'canvas' };
|
|
72
|
+
}
|
|
73
|
+
if (ext === '.tsx' || ext === '.jsx' || ext === '.ts' || ext === '.js') {
|
|
74
|
+
return { type: 'canvas-hmr', mode: 'module', file: rel, version, scope: 'canvas' };
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function enqueue(msg: HmrMessage) {
|
|
80
|
+
// Coalesce: hard > module > css. If a hard reload is queued, ignore any
|
|
81
|
+
// softer follow-up within the debounce window.
|
|
82
|
+
if (pendingMsg) {
|
|
83
|
+
const rank: Record<HmrMessage['mode'], number> = { css: 0, module: 1, hard: 2 };
|
|
84
|
+
if (rank[msg.mode] < rank[pendingMsg.mode]) {
|
|
85
|
+
// Keep the existing (harder) message; just refresh the timer.
|
|
86
|
+
} else {
|
|
87
|
+
pendingMsg = msg;
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
pendingMsg = msg;
|
|
91
|
+
}
|
|
92
|
+
if (pending) clearTimeout(pending);
|
|
93
|
+
pending = setTimeout(flush, DEBOUNCE_MS);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const offAny = ctx.bus.on('fs:any', (rel: string) => {
|
|
97
|
+
const msg = classify(rel);
|
|
98
|
+
if (msg) enqueue(msg);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
stop() {
|
|
103
|
+
offAny();
|
|
104
|
+
if (pending) clearTimeout(pending);
|
|
105
|
+
pending = null;
|
|
106
|
+
pendingMsg = null;
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Helpers — exported for tests.
|
|
113
|
+
|
|
114
|
+
export const HMR_DEBOUNCE_MS = DEBOUNCE_MS;
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
// HTTP layer for Bun.serve.
|
|
2
|
+
//
|
|
3
|
+
// Designed for extension — Phase 3.6 adds /ui/:slug + /_bun_hmr by appending to
|
|
4
|
+
// the route table without rewriting this module. The `fetch` export is the
|
|
5
|
+
// top-level fall-through for paths Bun's `routes` field doesn't cover.
|
|
6
|
+
|
|
7
|
+
import { watch } from 'node:fs';
|
|
8
|
+
import { dirname, join, posix } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
import type { Api } from './api.ts';
|
|
12
|
+
import { buildCanvasModule } from './canvas-build.ts';
|
|
13
|
+
import { canvasLibPath } from './canvas-lib-resolver.ts';
|
|
14
|
+
import { TranspileError } from './canvas-pipeline.ts';
|
|
15
|
+
import type { Context } from './context.ts';
|
|
16
|
+
import type { Inspect } from './inspect.ts';
|
|
17
|
+
import { canvasSlug, writeLocator } from './locator.ts';
|
|
18
|
+
import { RUNTIME_PACKAGES, getRuntimeBundle, packageForSlug, slugFor } from './runtime-bundle.ts';
|
|
19
|
+
|
|
20
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
export const MIME: Record<string, string> = {
|
|
23
|
+
'.html': 'text/html; charset=utf-8',
|
|
24
|
+
'.css': 'text/css; charset=utf-8',
|
|
25
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
26
|
+
'.mjs': 'application/javascript; charset=utf-8',
|
|
27
|
+
'.jsx': 'text/plain; charset=utf-8',
|
|
28
|
+
'.ts': 'text/plain; charset=utf-8',
|
|
29
|
+
'.tsx': 'text/plain; charset=utf-8',
|
|
30
|
+
'.json': 'application/json; charset=utf-8',
|
|
31
|
+
'.md': 'text/markdown; charset=utf-8',
|
|
32
|
+
'.svg': 'image/svg+xml',
|
|
33
|
+
'.png': 'image/png',
|
|
34
|
+
'.jpg': 'image/jpeg',
|
|
35
|
+
'.jpeg': 'image/jpeg',
|
|
36
|
+
'.gif': 'image/gif',
|
|
37
|
+
'.webp': 'image/webp',
|
|
38
|
+
'.ico': 'image/x-icon',
|
|
39
|
+
'.woff': 'font/woff',
|
|
40
|
+
'.woff2': 'font/woff2',
|
|
41
|
+
'.ttf': 'font/ttf',
|
|
42
|
+
'.otf': 'font/otf',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function ext(p: string): string {
|
|
46
|
+
const i = p.lastIndexOf('.');
|
|
47
|
+
return i === -1 ? '' : p.slice(i).toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function safePathUnderRoot(reqUrl: string, repoRoot: string): string | null {
|
|
51
|
+
let pathname: string;
|
|
52
|
+
try {
|
|
53
|
+
pathname = decodeURIComponent(new URL(reqUrl, 'http://x').pathname);
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const sep = '/';
|
|
58
|
+
const normalized = posix.normalize(posix.join(repoRoot, pathname));
|
|
59
|
+
if (normalized !== repoRoot && !normalized.startsWith(repoRoot + sep)) return null;
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// `dist/` lives next to server.ts when running source-mode (bun run server.ts)
|
|
64
|
+
// and inside the standalone binary's embedded FS in --compile mode.
|
|
65
|
+
const DIST_DIR = join(HERE, 'dist');
|
|
66
|
+
const CLIENT_DIR = join(HERE, 'client');
|
|
67
|
+
const TEMPLATES_DIR = join(HERE, '..', 'templates');
|
|
68
|
+
|
|
69
|
+
// In-memory transpile cache. Key = absolute canvas path; value = the last
|
|
70
|
+
// transpile keyed by mtime. Repeat GETs against an unchanged source skip the
|
|
71
|
+
// parse + ID-injection + Bun.Transpiler entirely.
|
|
72
|
+
interface CanvasCacheEntry {
|
|
73
|
+
mtimeMs: number;
|
|
74
|
+
etag: string;
|
|
75
|
+
js: string;
|
|
76
|
+
}
|
|
77
|
+
const canvasCache = new Map<string, CanvasCacheEntry>();
|
|
78
|
+
|
|
79
|
+
async function serveCanvasTsx(
|
|
80
|
+
absPath: string,
|
|
81
|
+
req: Request,
|
|
82
|
+
ctx: Context,
|
|
83
|
+
locatorAbsPath: string
|
|
84
|
+
): Promise<Response> {
|
|
85
|
+
const file = Bun.file(absPath);
|
|
86
|
+
if (!(await file.exists())) return new Response('Not found', { status: 404 });
|
|
87
|
+
|
|
88
|
+
// `stat` via Bun.file().lastModified — falls back to 0 if unavailable.
|
|
89
|
+
const mtimeMs = typeof file.lastModified === 'number' ? file.lastModified : 0;
|
|
90
|
+
let cached = canvasCache.get(absPath);
|
|
91
|
+
|
|
92
|
+
if (!cached || cached.mtimeMs !== mtimeMs) {
|
|
93
|
+
const source = await file.text();
|
|
94
|
+
let result: Awaited<ReturnType<typeof buildCanvasModule>>;
|
|
95
|
+
try {
|
|
96
|
+
result = await buildCanvasModule(absPath, source, {
|
|
97
|
+
designRoot: ctx.paths.designRoot,
|
|
98
|
+
});
|
|
99
|
+
} catch (err) {
|
|
100
|
+
if (err instanceof TranspileError) {
|
|
101
|
+
return new Response(`Transpile error: ${err.message}`, {
|
|
102
|
+
status: 500,
|
|
103
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
107
|
+
return new Response(`Canvas build error: ${msg}`, {
|
|
108
|
+
status: 500,
|
|
109
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
cached = { mtimeMs, etag: result.etag, js: result.js };
|
|
113
|
+
canvasCache.set(absPath, cached);
|
|
114
|
+
// Persist the locator map. Awaited so the inspector / Phase-12 layers
|
|
115
|
+
// panel sees a consistent (cdId -> source) view by the time the canvas
|
|
116
|
+
// mounts. Per-path mutex inside writeLocator() makes concurrent transpiles
|
|
117
|
+
// safe.
|
|
118
|
+
await writeLocator(locatorAbsPath, canvasSlug(absPath, ctx.paths.designRoot), result.locator);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const ifNoneMatch = req.headers.get('if-none-match');
|
|
122
|
+
if (ifNoneMatch === cached.etag) {
|
|
123
|
+
return new Response(null, {
|
|
124
|
+
status: 304,
|
|
125
|
+
headers: { ETag: cached.etag, 'Cache-Control': 'no-cache' },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return new Response(cached.js, {
|
|
129
|
+
status: 200,
|
|
130
|
+
headers: {
|
|
131
|
+
'Content-Type': 'application/javascript; charset=utf-8',
|
|
132
|
+
ETag: cached.etag,
|
|
133
|
+
'Cache-Control': 'no-cache',
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function serveFile(absPath: string, headers: Record<string, string> = {}): Promise<Response> {
|
|
139
|
+
const file = Bun.file(absPath);
|
|
140
|
+
if (!(await file.exists())) return new Response('Not found', { status: 404 });
|
|
141
|
+
const e = ext(absPath);
|
|
142
|
+
return new Response(file, {
|
|
143
|
+
headers: {
|
|
144
|
+
'Content-Type': MIME[e] || 'application/octet-stream',
|
|
145
|
+
'Cache-Control': 'no-store',
|
|
146
|
+
...headers,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface Http {
|
|
152
|
+
routes: Record<string, (req: Request) => Response | Promise<Response>>;
|
|
153
|
+
fetch(req: Request): Promise<Response>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function createHttp(ctx: Context, api: Api, inspect: Inspect): Http {
|
|
157
|
+
// Cache invalidation — when canvas-lib changes, every cached canvas bundle
|
|
158
|
+
// is stale because canvas-lib is inlined into each one via the resolver
|
|
159
|
+
// plugin. Drop the whole cache so the next request rebuilds with the fresh
|
|
160
|
+
// lib. Without this, the HMR "hard reload" message reaches the browser but
|
|
161
|
+
// the iframe re-fetches a stale-but-fresh-mtime bundle and the change never
|
|
162
|
+
// takes effect.
|
|
163
|
+
//
|
|
164
|
+
// Per DDR-025 canvas-lib ships with the dev-server, so we watch the
|
|
165
|
+
// dev-server-internal file directly instead of relying on the project-side
|
|
166
|
+
// fs:any watcher. The synthetic `_lib/canvas-lib.tsx` rel-path lets the
|
|
167
|
+
// existing hmr-broadcast classifier emit a hard reload without bespoke
|
|
168
|
+
// wiring. The legacy `fs:any` _lib/ listener stays for downstream projects
|
|
169
|
+
// still carrying a pre-4.0.5 `<designRoot>/_lib/`, but that file is now
|
|
170
|
+
// ignored at build time — clearing the cache here is harmless.
|
|
171
|
+
ctx.bus.on('fs:any', (rel: string) => {
|
|
172
|
+
if (rel.startsWith('_lib/')) {
|
|
173
|
+
canvasCache.clear();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
let libWatcher: ReturnType<typeof watch> | null = null;
|
|
178
|
+
try {
|
|
179
|
+
libWatcher = watch(canvasLibPath(), () => {
|
|
180
|
+
canvasCache.clear();
|
|
181
|
+
ctx.bus.emit('fs:any', '_lib/canvas-lib.tsx');
|
|
182
|
+
});
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.warn(
|
|
185
|
+
'[canvas-lib] failed to watch dev-server canvas-lib:',
|
|
186
|
+
err instanceof Error ? err.message : err
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
void libWatcher;
|
|
190
|
+
|
|
191
|
+
async function readJson<T = unknown>(req: Request, max = 256 * 1024): Promise<T | null> {
|
|
192
|
+
try {
|
|
193
|
+
const text = await req.text();
|
|
194
|
+
if (text.length > max) throw new Error('body too large');
|
|
195
|
+
return text ? (JSON.parse(text) as T) : null;
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const routes = {
|
|
202
|
+
'/_health': () =>
|
|
203
|
+
Response.json({
|
|
204
|
+
ok: true,
|
|
205
|
+
app: 'design',
|
|
206
|
+
project: ctx.cfg.name,
|
|
207
|
+
pid: process.pid,
|
|
208
|
+
}),
|
|
209
|
+
|
|
210
|
+
'/_active': () => Response.json(inspect.state),
|
|
211
|
+
|
|
212
|
+
'/_config': () => Response.json(ctx.cfg),
|
|
213
|
+
|
|
214
|
+
'/_index-data': async () =>
|
|
215
|
+
Response.json(await api.buildIndexData(), { headers: { 'Cache-Control': 'no-store' } }),
|
|
216
|
+
|
|
217
|
+
'/_system-data': async () =>
|
|
218
|
+
Response.json(await api.buildSystemData(), { headers: { 'Cache-Control': 'no-store' } }),
|
|
219
|
+
|
|
220
|
+
'/_comments-all': async () =>
|
|
221
|
+
Response.json(await api.loadAllComments(), { headers: { 'Cache-Control': 'no-store' } }),
|
|
222
|
+
|
|
223
|
+
'/_comments': async (req: Request) => {
|
|
224
|
+
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
225
|
+
const file = new URL(req.url).searchParams.get('file');
|
|
226
|
+
if (!file) return new Response('file query param required', { status: 400 });
|
|
227
|
+
const comments = await api.loadCommentsForFile(file);
|
|
228
|
+
return Response.json({ file, comments }, { headers: { 'Cache-Control': 'no-store' } });
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
'/_api/canvas-meta': async (req: Request) => {
|
|
232
|
+
// Phase 4 T5 — sibling `<canvas>.meta.json` read / merge.
|
|
233
|
+
// GET ?file=<repo-relative-canvas-path> → full meta or {}
|
|
234
|
+
// PATCH (or POST) body { file, patch: {...} } → shallow-merged meta
|
|
235
|
+
const url = new URL(req.url);
|
|
236
|
+
if (req.method === 'GET') {
|
|
237
|
+
const file = url.searchParams.get('file');
|
|
238
|
+
if (!file) return new Response('file query param required', { status: 400 });
|
|
239
|
+
const meta = await api.loadCanvasMeta(file);
|
|
240
|
+
return Response.json(meta ?? {}, { headers: { 'Cache-Control': 'no-store' } });
|
|
241
|
+
}
|
|
242
|
+
if (req.method === 'PATCH' || req.method === 'POST') {
|
|
243
|
+
const body = await readJson<{ file?: string; patch?: Record<string, unknown> }>(req);
|
|
244
|
+
if (!body || typeof body.file !== 'string' || !body.file) {
|
|
245
|
+
return new Response('body must include { file, patch }', { status: 400 });
|
|
246
|
+
}
|
|
247
|
+
if (!body.patch || typeof body.patch !== 'object') {
|
|
248
|
+
return new Response('body.patch must be an object', { status: 400 });
|
|
249
|
+
}
|
|
250
|
+
const next = await api.patchCanvasMeta(body.file, body.patch);
|
|
251
|
+
if (!next) return new Response('Not found or rejected', { status: 404 });
|
|
252
|
+
return Response.json(next, { headers: { 'Cache-Control': 'no-store' } });
|
|
253
|
+
}
|
|
254
|
+
return new Response('Method not allowed', { status: 405 });
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
'/_api/annotations': async (req: Request) => {
|
|
258
|
+
// Phase 5 — `<designRoot>/<slug>.annotations.svg` read / overwrite.
|
|
259
|
+
// GET ?file=<repo-relative-canvas-path> → SVG text (empty if absent)
|
|
260
|
+
// PUT body { file, svg } → 204 on write, 4xx otherwise
|
|
261
|
+
const url = new URL(req.url);
|
|
262
|
+
if (req.method === 'GET') {
|
|
263
|
+
const file = url.searchParams.get('file');
|
|
264
|
+
if (!file) return new Response('file query param required', { status: 400 });
|
|
265
|
+
const svg = await api.loadAnnotations(file);
|
|
266
|
+
return new Response(svg ?? '', {
|
|
267
|
+
status: 200,
|
|
268
|
+
headers: {
|
|
269
|
+
'Content-Type': 'image/svg+xml; charset=utf-8',
|
|
270
|
+
'Cache-Control': 'no-store',
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (req.method === 'PUT' || req.method === 'POST') {
|
|
275
|
+
const body = await readJson<{ file?: string; svg?: string }>(req, 1024 * 1024 + 1024);
|
|
276
|
+
if (!body || typeof body.file !== 'string' || !body.file) {
|
|
277
|
+
return new Response('body must include { file, svg }', { status: 400 });
|
|
278
|
+
}
|
|
279
|
+
if (typeof body.svg !== 'string') {
|
|
280
|
+
return new Response('body.svg must be a string', { status: 400 });
|
|
281
|
+
}
|
|
282
|
+
const ok = await api.saveAnnotations(body.file, body.svg);
|
|
283
|
+
if (!ok) return new Response('rejected', { status: 400 });
|
|
284
|
+
return new Response(null, { status: 204 });
|
|
285
|
+
}
|
|
286
|
+
return new Response('Method not allowed', { status: 405 });
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
'/_canvas-state': async (req: Request) => {
|
|
290
|
+
const url = new URL(req.url);
|
|
291
|
+
if (req.method === 'GET') {
|
|
292
|
+
const file = url.searchParams.get('file');
|
|
293
|
+
if (!file) return new Response('file query param required', { status: 400 });
|
|
294
|
+
const state = await api.loadCanvasState(file);
|
|
295
|
+
return Response.json(state ?? {}, { headers: { 'Cache-Control': 'no-store' } });
|
|
296
|
+
}
|
|
297
|
+
if (req.method === 'POST') {
|
|
298
|
+
const body = await readJson<{ file?: string }>(req);
|
|
299
|
+
if (!body || typeof body.file !== 'string' || !body.file) {
|
|
300
|
+
return new Response('body must include file (string)', { status: 400 });
|
|
301
|
+
}
|
|
302
|
+
await api.saveCanvasState(body.file, body as Record<string, unknown>);
|
|
303
|
+
return new Response(null, { status: 204 });
|
|
304
|
+
}
|
|
305
|
+
return new Response('Method not allowed', { status: 405 });
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
'/_hmr': async (req: Request) => {
|
|
309
|
+
// Hint endpoint — the build:watch process POSTs `{ type, path, hash }`
|
|
310
|
+
// after a rebuild; we forward it to all WS clients. Body is opaque; we
|
|
311
|
+
// just emit on the bus and ws.ts handles broadcast.
|
|
312
|
+
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
313
|
+
const body = await readJson<{ type: string; path?: string; hash?: number }>(req);
|
|
314
|
+
if (!body || typeof body.type !== 'string') return new Response('bad body', { status: 400 });
|
|
315
|
+
ctx.bus.emit('hmr', body);
|
|
316
|
+
return new Response(null, { status: 204 });
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
'/': () => serveFile(join(CLIENT_DIR, 'index.html')),
|
|
320
|
+
'/index.html': () => serveFile(join(CLIENT_DIR, 'index.html')),
|
|
321
|
+
} satisfies Record<string, (req: Request) => Response | Promise<Response>>;
|
|
322
|
+
|
|
323
|
+
async function fetch(req: Request): Promise<Response> {
|
|
324
|
+
try {
|
|
325
|
+
const url = new URL(req.url);
|
|
326
|
+
const pathname = url.pathname;
|
|
327
|
+
|
|
328
|
+
// Bundled client assets (preferred path — bundle from dist/).
|
|
329
|
+
if (pathname.startsWith('/_client/')) {
|
|
330
|
+
const rel = decodeURIComponent(pathname.slice('/_client/'.length));
|
|
331
|
+
if (rel.includes('..')) return new Response('Forbidden', { status: 403 });
|
|
332
|
+
// Try dist/ first (built bundle + styles), fall back to client/ (raw source files).
|
|
333
|
+
const distHit = join(DIST_DIR, rel);
|
|
334
|
+
if (await Bun.file(distHit).exists()) return serveFile(distHit);
|
|
335
|
+
const srcHit = join(CLIENT_DIR, rel);
|
|
336
|
+
return serveFile(srcHit);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// React 19 runtime bundles for TSX canvases. The browser pulls these
|
|
340
|
+
// through the importmap in _canvas-shell.html — each bundle is a single
|
|
341
|
+
// package (react, react-dom/client, jsx-runtime, jsx-dev-runtime),
|
|
342
|
+
// built once on first request, cached in-process for the session.
|
|
343
|
+
if (pathname.startsWith('/_canvas-runtime/')) {
|
|
344
|
+
const slugWithExt = decodeURIComponent(pathname.slice('/_canvas-runtime/'.length));
|
|
345
|
+
const pkg = packageForSlug(slugWithExt);
|
|
346
|
+
if (!pkg) return new Response('Not found', { status: 404 });
|
|
347
|
+
try {
|
|
348
|
+
const bundle = await getRuntimeBundle(pkg);
|
|
349
|
+
const ifNoneMatch = req.headers.get('if-none-match');
|
|
350
|
+
if (ifNoneMatch === bundle.etag) {
|
|
351
|
+
return new Response(null, {
|
|
352
|
+
status: 304,
|
|
353
|
+
headers: { ETag: bundle.etag, 'Cache-Control': 'no-cache' },
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return new Response(bundle.js, {
|
|
357
|
+
status: 200,
|
|
358
|
+
headers: {
|
|
359
|
+
'Content-Type': 'application/javascript; charset=utf-8',
|
|
360
|
+
ETag: bundle.etag,
|
|
361
|
+
'Cache-Control': 'no-cache',
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
} catch (err) {
|
|
365
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
366
|
+
return new Response(`Runtime bundle error: ${msg}`, { status: 500 });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Canvas mount harness — served for iframes pointing at a .tsx canvas.
|
|
371
|
+
// Static template ships under plugins/design/templates/_shell.html.
|
|
372
|
+
// Query parameter ?canvas=<path-relative-to-designRoot> tells the shell
|
|
373
|
+
// which canvas to import + mount. See plugins/design/templates/_shell.html.
|
|
374
|
+
if (pathname === '/_canvas-shell.html' || pathname === '/_canvas-shell') {
|
|
375
|
+
const shellHtml = await Bun.file(join(TEMPLATES_DIR, '_shell.html')).text();
|
|
376
|
+
// Inject inspector overlay — Cmd+Click selection + Shift/C+Click
|
|
377
|
+
// add-comment flow. Without this, TSX canvases mount fine but lose
|
|
378
|
+
// every interactive devtool.
|
|
379
|
+
const injected = inspect.injectInspector(shellHtml);
|
|
380
|
+
return new Response(injected, {
|
|
381
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' },
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Fall-through: serve user repo files (designRoot + everything under repoRoot).
|
|
386
|
+
const fp = safePathUnderRoot(req.url, ctx.paths.repoRoot);
|
|
387
|
+
if (!fp) return new Response('Forbidden', { status: 403 });
|
|
388
|
+
|
|
389
|
+
const file = Bun.file(fp);
|
|
390
|
+
const exists = await file.exists();
|
|
391
|
+
if (!exists) return new Response('Not found', { status: 404 });
|
|
392
|
+
|
|
393
|
+
const e = ext(fp);
|
|
394
|
+
const underDesignRoot = `${fp}/`.startsWith(`${ctx.paths.designRoot}/`);
|
|
395
|
+
// .tsx under designRoot is a canvas — transpile + emit locator, return JS.
|
|
396
|
+
if (e === '.tsx' && underDesignRoot) {
|
|
397
|
+
return serveCanvasTsx(fp, req, ctx, join(ctx.paths.designRoot, '_locator.json'));
|
|
398
|
+
}
|
|
399
|
+
// Bun.file streams transparently for binary content.
|
|
400
|
+
return new Response(file, {
|
|
401
|
+
headers: {
|
|
402
|
+
'Content-Type': MIME[e] || 'application/octet-stream',
|
|
403
|
+
'Cache-Control': 'no-store',
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
} catch (err) {
|
|
407
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
408
|
+
return new Response(`Server error: ${msg}`, { status: 500 });
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return { routes, fetch };
|
|
413
|
+
}
|