@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,131 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Dev-server entry. Bun.serve + native WebSocket + per-platform standalone
|
|
3
|
+
// binary distribution (DDR-009, DDR-013, DDR-015).
|
|
4
|
+
//
|
|
5
|
+
// Process layout:
|
|
6
|
+
// createContext() -> repo root, config, paths, pub/sub bus
|
|
7
|
+
// createApi(ctx) -> comments / canvas-state / index / system data
|
|
8
|
+
// createInspect() -> active-canvas tracking + HTML injection
|
|
9
|
+
// createWs() -> Bun.serve native WS handler
|
|
10
|
+
// createHttp() -> route table + fall-through fetch
|
|
11
|
+
// createFsWatch() -> recursive fs.watch -> bus -> WS broadcast
|
|
12
|
+
//
|
|
13
|
+
// Single Bun.serve instance owns both HTTP routes and the WS upgrade. Nothing
|
|
14
|
+
// else binds to a port. The orchestrator (slash commands) reads _server.json
|
|
15
|
+
// to detect a live instance and avoid duplicate boots.
|
|
16
|
+
|
|
17
|
+
import { spawn } from 'node:child_process';
|
|
18
|
+
|
|
19
|
+
import { createApi } from './api.ts';
|
|
20
|
+
import { createContext } from './context.ts';
|
|
21
|
+
import { createFsWatch } from './fs-watch.ts';
|
|
22
|
+
import { createHttp } from './http.ts';
|
|
23
|
+
import { createInspect } from './inspect.ts';
|
|
24
|
+
import { startHeapWatch } from './mem.ts';
|
|
25
|
+
import { createWs } from './ws.ts';
|
|
26
|
+
|
|
27
|
+
const ctx = createContext();
|
|
28
|
+
|
|
29
|
+
const api = createApi(ctx, async (file) => {
|
|
30
|
+
// After every comments mutation, re-broadcast the updated list.
|
|
31
|
+
const comments = await api.loadCommentsForFile(file);
|
|
32
|
+
ctx.bus.emit('comments', { file, comments });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const inspect = createInspect(ctx, (file) => api.loadCommentsForFile(file));
|
|
36
|
+
await inspect.load();
|
|
37
|
+
|
|
38
|
+
const ws = createWs(ctx, api, inspect);
|
|
39
|
+
const http = createHttp(ctx, api, inspect);
|
|
40
|
+
const fsWatch = createFsWatch(ctx);
|
|
41
|
+
|
|
42
|
+
// Port: --port arg > $PORT > $MDCC_DEV_PORT > 4399.
|
|
43
|
+
function resolvePort(): number {
|
|
44
|
+
const i = process.argv.indexOf('--port');
|
|
45
|
+
if (i !== -1 && process.argv[i + 1]) return Number(process.argv[i + 1]);
|
|
46
|
+
const env = process.env.PORT ?? process.env.MDCC_DEV_PORT;
|
|
47
|
+
if (env) return Number(env);
|
|
48
|
+
return 4399;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const PORT = resolvePort();
|
|
52
|
+
|
|
53
|
+
const server = Bun.serve<{ id: string; remote: string }, never>({
|
|
54
|
+
port: PORT,
|
|
55
|
+
hostname: '127.0.0.1',
|
|
56
|
+
development: process.env.NODE_ENV !== 'production',
|
|
57
|
+
routes: http.routes,
|
|
58
|
+
async fetch(req, srv) {
|
|
59
|
+
// WebSocket upgrade.
|
|
60
|
+
if (new URL(req.url).pathname.startsWith('/_ws')) {
|
|
61
|
+
const ok = srv.upgrade(req, {
|
|
62
|
+
data: {
|
|
63
|
+
id: crypto.randomUUID(),
|
|
64
|
+
remote: req.headers.get('x-forwarded-for') ?? '127.0.0.1',
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
if (ok) return undefined as unknown as Response;
|
|
68
|
+
return new Response('Upgrade failed', { status: 400 });
|
|
69
|
+
}
|
|
70
|
+
return http.fetch(req);
|
|
71
|
+
},
|
|
72
|
+
websocket: ws.handler,
|
|
73
|
+
error(e) {
|
|
74
|
+
console.error('[bun.serve error]', e);
|
|
75
|
+
return new Response('Server error', { status: 500 });
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await Bun.write(
|
|
80
|
+
ctx.paths.serverInfoFile,
|
|
81
|
+
JSON.stringify(
|
|
82
|
+
{
|
|
83
|
+
pid: process.pid,
|
|
84
|
+
port: server.port,
|
|
85
|
+
url: `http://localhost:${server.port}`,
|
|
86
|
+
started: new Date().toISOString(),
|
|
87
|
+
project: ctx.cfg.name,
|
|
88
|
+
config_source: ctx.cfg._source,
|
|
89
|
+
},
|
|
90
|
+
null,
|
|
91
|
+
2
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
fsWatch.start();
|
|
96
|
+
startHeapWatch();
|
|
97
|
+
|
|
98
|
+
const url = `http://localhost:${server.port}`;
|
|
99
|
+
console.log(`\n ${ctx.projectLabel} — local browser`);
|
|
100
|
+
console.log(' ─────────────────────────────');
|
|
101
|
+
console.log(` ${url}`);
|
|
102
|
+
console.log(` Project: ${ctx.cfg.name}`);
|
|
103
|
+
console.log(` Config: ${ctx.cfg._source}`);
|
|
104
|
+
console.log(` Design: ${ctx.paths.designRoot}`);
|
|
105
|
+
console.log(` Active: ${ctx.paths.activeFile}`);
|
|
106
|
+
console.log(' Press Ctrl+C to stop.\n');
|
|
107
|
+
|
|
108
|
+
if (!process.env.NO_OPEN) {
|
|
109
|
+
if (process.platform === 'darwin')
|
|
110
|
+
spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
|
|
111
|
+
else if (process.platform === 'linux')
|
|
112
|
+
spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function shutdown() {
|
|
116
|
+
console.log('\n Stopping…');
|
|
117
|
+
fsWatch.stop();
|
|
118
|
+
try {
|
|
119
|
+
await Bun.write(ctx.paths.serverInfoFile, '').catch(() => {});
|
|
120
|
+
// Remove the file by writing empty then unlinking.
|
|
121
|
+
const fs = await import('node:fs/promises');
|
|
122
|
+
await fs.unlink(ctx.paths.serverInfoFile).catch(() => {});
|
|
123
|
+
} catch {
|
|
124
|
+
/* ignore */
|
|
125
|
+
}
|
|
126
|
+
server.stop();
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
process.on('SIGINT', shutdown);
|
|
131
|
+
process.on('SIGTERM', shutdown);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Shared test scaffolding. Bun-test only.
|
|
2
|
+
|
|
3
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { type Subprocess, spawn } from 'bun';
|
|
7
|
+
|
|
8
|
+
export interface Sandbox {
|
|
9
|
+
root: string;
|
|
10
|
+
designRoot: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function makeSandbox(): Sandbox {
|
|
14
|
+
const root = mkdtempSync(join(tmpdir(), 'mdcc-test-'));
|
|
15
|
+
const designRoot = join(root, '.design');
|
|
16
|
+
mkdirSync(designRoot, { recursive: true });
|
|
17
|
+
writeFileSync(
|
|
18
|
+
join(designRoot, 'config.json'),
|
|
19
|
+
JSON.stringify(
|
|
20
|
+
{
|
|
21
|
+
name: 'test',
|
|
22
|
+
designRoot: '.design',
|
|
23
|
+
canvasGroups: [
|
|
24
|
+
{ label: 'System', path: 'system' },
|
|
25
|
+
{ label: 'UI', path: 'ui' },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
null,
|
|
29
|
+
2
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
mkdirSync(join(designRoot, 'ui'), { recursive: true });
|
|
33
|
+
writeFileSync(
|
|
34
|
+
join(designRoot, 'ui', 'fixture.html'),
|
|
35
|
+
'<!doctype html><html><head><title>fix</title></head><body><h1>fixture</h1></body></html>'
|
|
36
|
+
);
|
|
37
|
+
return { root, designRoot };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let portCounter = 4500;
|
|
41
|
+
export function nextPort(): number {
|
|
42
|
+
// Bump on every call so parallel tests don't collide. Bun.serve will throw
|
|
43
|
+
// EADDRINUSE if the host happened to bind one — caller retries with nextPort().
|
|
44
|
+
portCounter += 1;
|
|
45
|
+
return portCounter;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function bootServer(root: string, port: number): Promise<Subprocess> {
|
|
49
|
+
const serverPath = join(import.meta.dir, '..', 'server.ts');
|
|
50
|
+
const proc = spawn({
|
|
51
|
+
cmd: ['bun', 'run', serverPath, '--port', String(port), '--root', root],
|
|
52
|
+
cwd: join(import.meta.dir, '..'),
|
|
53
|
+
env: { ...process.env, NO_OPEN: '1', NODE_ENV: 'test' },
|
|
54
|
+
stdout: 'pipe',
|
|
55
|
+
stderr: 'pipe',
|
|
56
|
+
});
|
|
57
|
+
// Wait up to 3 s for the server to bind.
|
|
58
|
+
const start = Date.now();
|
|
59
|
+
while (Date.now() - start < 3000) {
|
|
60
|
+
try {
|
|
61
|
+
const r = await fetch(`http://localhost:${port}/_health`, {
|
|
62
|
+
signal: AbortSignal.timeout(200),
|
|
63
|
+
});
|
|
64
|
+
if (r.ok) return proc;
|
|
65
|
+
} catch {
|
|
66
|
+
/* not up yet */
|
|
67
|
+
}
|
|
68
|
+
await Bun.sleep(50);
|
|
69
|
+
}
|
|
70
|
+
proc.kill();
|
|
71
|
+
throw new Error(`server did not start on port ${port} within 3 s`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function killProc(proc: Subprocess) {
|
|
75
|
+
proc.kill();
|
|
76
|
+
try {
|
|
77
|
+
await proc.exited;
|
|
78
|
+
} catch {
|
|
79
|
+
/* ignore */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Smoke: WS `active` + `tabs` + `select` messages reflect into _active.json.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from 'bun:test';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { bootServer, killProc, makeSandbox, nextPort } from './_helpers.ts';
|
|
7
|
+
|
|
8
|
+
async function openWs(port: number): Promise<WebSocket> {
|
|
9
|
+
const ws = new WebSocket(`ws://localhost:${port}/_ws`);
|
|
10
|
+
await new Promise<void>((resolve, reject) => {
|
|
11
|
+
const t = setTimeout(() => reject(new Error('ws open timeout')), 2000);
|
|
12
|
+
ws.addEventListener('open', () => {
|
|
13
|
+
clearTimeout(t);
|
|
14
|
+
resolve();
|
|
15
|
+
});
|
|
16
|
+
ws.addEventListener('error', (e) => {
|
|
17
|
+
clearTimeout(t);
|
|
18
|
+
reject(e);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
return ws;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('_active.json round-trip', () => {
|
|
25
|
+
test('active + tabs + selected persist to disk', async () => {
|
|
26
|
+
const { root, designRoot } = makeSandbox();
|
|
27
|
+
const port = nextPort();
|
|
28
|
+
const proc = await bootServer(root, port);
|
|
29
|
+
try {
|
|
30
|
+
const ws = await openWs(port);
|
|
31
|
+
ws.send(JSON.stringify({ type: 'active', file: '.design/ui/fixture.html' }));
|
|
32
|
+
ws.send(JSON.stringify({ type: 'tabs', tabs: ['.design/ui/fixture.html'] }));
|
|
33
|
+
ws.send(
|
|
34
|
+
JSON.stringify({
|
|
35
|
+
type: 'select',
|
|
36
|
+
selection: {
|
|
37
|
+
file: '.design/ui/fixture.html',
|
|
38
|
+
selector: 'h1',
|
|
39
|
+
tag: 'h1',
|
|
40
|
+
classes: '',
|
|
41
|
+
text: 'fixture',
|
|
42
|
+
dom_path: ['html', 'body', 'h1'],
|
|
43
|
+
bounds: { x: 0, y: 0, w: 100, h: 24 },
|
|
44
|
+
html: '<h1>fixture</h1>',
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Allow time for the queued microtask save to land.
|
|
50
|
+
await Bun.sleep(150);
|
|
51
|
+
|
|
52
|
+
const state = (await Bun.file(join(designRoot, '_active.json')).json()) as {
|
|
53
|
+
active: string;
|
|
54
|
+
open_tabs: string[];
|
|
55
|
+
selected: { selector: string; tag: string; v?: number };
|
|
56
|
+
};
|
|
57
|
+
expect(state.active).toBe('.design/ui/fixture.html');
|
|
58
|
+
expect(state.open_tabs).toContain('.design/ui/fixture.html');
|
|
59
|
+
expect(state.selected.selector).toBe('h1');
|
|
60
|
+
expect(state.selected.tag).toBe('h1');
|
|
61
|
+
expect(state.selected.v).toBe(1);
|
|
62
|
+
ws.close();
|
|
63
|
+
} finally {
|
|
64
|
+
await killProc(proc);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('selection carrying data-cd-id stamps v=2 + canvas slug', async () => {
|
|
69
|
+
const { root, designRoot } = makeSandbox();
|
|
70
|
+
const port = nextPort();
|
|
71
|
+
const proc = await bootServer(root, port);
|
|
72
|
+
try {
|
|
73
|
+
const ws = await openWs(port);
|
|
74
|
+
ws.send(JSON.stringify({ type: 'active', file: '.design/ui/fixture.tsx' }));
|
|
75
|
+
ws.send(
|
|
76
|
+
JSON.stringify({
|
|
77
|
+
type: 'select',
|
|
78
|
+
selection: {
|
|
79
|
+
file: '.design/ui/fixture.tsx',
|
|
80
|
+
selector: 'div.btn',
|
|
81
|
+
tag: 'button',
|
|
82
|
+
classes: 'btn btn--ghost',
|
|
83
|
+
text: 'OK',
|
|
84
|
+
dom_path: ['html', 'body', 'button'],
|
|
85
|
+
bounds: { x: 10, y: 20, w: 80, h: 32 },
|
|
86
|
+
html: '<button class="btn btn--ghost">OK</button>',
|
|
87
|
+
id: 'a1b2c3d4',
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
await Bun.sleep(150);
|
|
92
|
+
|
|
93
|
+
const state = (await Bun.file(join(designRoot, '_active.json')).json()) as {
|
|
94
|
+
selected: { id?: string; canvas?: string; v: number; selector: string };
|
|
95
|
+
};
|
|
96
|
+
expect(state.selected.v).toBe(2);
|
|
97
|
+
expect(state.selected.id).toBe('a1b2c3d4');
|
|
98
|
+
// Canvas slug = designRoot-relative path, ext-less, POSIX. The fixture
|
|
99
|
+
// file is `.design/ui/fixture.tsx`; designRel = `.design` → slug =
|
|
100
|
+
// `ui/fixture`.
|
|
101
|
+
expect(state.selected.canvas).toBe('ui/fixture');
|
|
102
|
+
// v=2 still carries the v1 selector as a fallback (legacy readers).
|
|
103
|
+
expect(state.selected.selector).toBe('div.btn');
|
|
104
|
+
ws.close();
|
|
105
|
+
} finally {
|
|
106
|
+
await killProc(proc);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('selection without id stays v=1 (legacy HTML canvas / shell chrome click)', async () => {
|
|
111
|
+
const { root, designRoot } = makeSandbox();
|
|
112
|
+
const port = nextPort();
|
|
113
|
+
const proc = await bootServer(root, port);
|
|
114
|
+
try {
|
|
115
|
+
const ws = await openWs(port);
|
|
116
|
+
ws.send(JSON.stringify({ type: 'active', file: '.design/ui/fixture.html' }));
|
|
117
|
+
ws.send(
|
|
118
|
+
JSON.stringify({
|
|
119
|
+
type: 'select',
|
|
120
|
+
selection: {
|
|
121
|
+
file: '.design/ui/fixture.html',
|
|
122
|
+
selector: 'h1',
|
|
123
|
+
tag: 'h1',
|
|
124
|
+
classes: '',
|
|
125
|
+
text: '',
|
|
126
|
+
dom_path: ['html', 'body', 'h1'],
|
|
127
|
+
bounds: { x: 0, y: 0, w: 0, h: 0 },
|
|
128
|
+
html: '<h1/>',
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
await Bun.sleep(150);
|
|
133
|
+
|
|
134
|
+
const state = (await Bun.file(join(designRoot, '_active.json')).json()) as {
|
|
135
|
+
selected: { v: number; id?: string; canvas?: string };
|
|
136
|
+
};
|
|
137
|
+
expect(state.selected.v).toBe(1);
|
|
138
|
+
expect(state.selected.id).toBeUndefined();
|
|
139
|
+
expect(state.selected.canvas).toBeUndefined();
|
|
140
|
+
ws.close();
|
|
141
|
+
} finally {
|
|
142
|
+
await killProc(proc);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// /_api/annotations — Phase 5 GET / PUT round-trip + validation gates.
|
|
2
|
+
//
|
|
3
|
+
// Verifies:
|
|
4
|
+
// - GET returns empty body for a canvas with no annotations file
|
|
5
|
+
// - PUT writes `<designRoot>/<slug>.annotations.svg`
|
|
6
|
+
// - GET subsequently returns the saved SVG
|
|
7
|
+
// - PUT rejects non-SVG bodies (400)
|
|
8
|
+
// - PUT rejects oversized bodies (>1 MB)
|
|
9
|
+
// - Unknown method → 405
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { describe, expect, test } from 'bun:test';
|
|
15
|
+
|
|
16
|
+
import { bootServer, killProc, makeSandbox, nextPort } from './_helpers.ts';
|
|
17
|
+
|
|
18
|
+
const SVG_OK =
|
|
19
|
+
'<svg xmlns="http://www.w3.org/2000/svg" data-mdcc-annotations="1">' +
|
|
20
|
+
'<path data-id="p1" data-tool="pen" stroke="#d63b1f" stroke-width="2" fill="none" d="M0 0 L10 10"/>' +
|
|
21
|
+
'</svg>';
|
|
22
|
+
|
|
23
|
+
describe('/_api/annotations — GET/PUT', () => {
|
|
24
|
+
test('GET on a canvas with no annotations returns empty body (200)', async () => {
|
|
25
|
+
const { root, designRoot } = makeSandbox();
|
|
26
|
+
const port = nextPort();
|
|
27
|
+
const proc = await bootServer(root, port);
|
|
28
|
+
try {
|
|
29
|
+
mkdirSync(join(designRoot, 'ui'), { recursive: true });
|
|
30
|
+
writeFileSync(
|
|
31
|
+
join(designRoot, 'ui', 'Phase5.tsx'),
|
|
32
|
+
'export default function P(){return <main/>}\n'
|
|
33
|
+
);
|
|
34
|
+
const r = await fetch(
|
|
35
|
+
`http://localhost:${port}/_api/annotations?file=${encodeURIComponent(
|
|
36
|
+
'.design/ui/Phase5.tsx'
|
|
37
|
+
)}`
|
|
38
|
+
);
|
|
39
|
+
expect(r.status).toBe(200);
|
|
40
|
+
const body = await r.text();
|
|
41
|
+
expect(body).toBe('');
|
|
42
|
+
} finally {
|
|
43
|
+
await killProc(proc);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('PUT writes <designRoot>/<slug>.annotations.svg and GET round-trips', async () => {
|
|
48
|
+
const { root, designRoot } = makeSandbox();
|
|
49
|
+
const port = nextPort();
|
|
50
|
+
const proc = await bootServer(root, port);
|
|
51
|
+
try {
|
|
52
|
+
mkdirSync(join(designRoot, 'ui'), { recursive: true });
|
|
53
|
+
writeFileSync(
|
|
54
|
+
join(designRoot, 'ui', 'Round.tsx'),
|
|
55
|
+
'export default function P(){return <main/>}\n'
|
|
56
|
+
);
|
|
57
|
+
const put = await fetch(`http://localhost:${port}/_api/annotations`, {
|
|
58
|
+
method: 'PUT',
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
body: JSON.stringify({ file: '.design/ui/Round.tsx', svg: SVG_OK }),
|
|
61
|
+
});
|
|
62
|
+
expect(put.status).toBe(204);
|
|
63
|
+
|
|
64
|
+
// File slug derives from the path under designRoot: `ui-round`.
|
|
65
|
+
const written = join(designRoot, 'ui-round.annotations.svg');
|
|
66
|
+
expect(existsSync(written)).toBe(true);
|
|
67
|
+
expect(readFileSync(written, 'utf8')).toBe(SVG_OK);
|
|
68
|
+
|
|
69
|
+
const get = await fetch(
|
|
70
|
+
`http://localhost:${port}/_api/annotations?file=${encodeURIComponent(
|
|
71
|
+
'.design/ui/Round.tsx'
|
|
72
|
+
)}`
|
|
73
|
+
);
|
|
74
|
+
expect(get.status).toBe(200);
|
|
75
|
+
expect(get.headers.get('content-type')).toContain('image/svg+xml');
|
|
76
|
+
expect(await get.text()).toBe(SVG_OK);
|
|
77
|
+
} finally {
|
|
78
|
+
await killProc(proc);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('PUT rejects non-SVG bodies with 400', async () => {
|
|
83
|
+
const { root } = makeSandbox();
|
|
84
|
+
const port = nextPort();
|
|
85
|
+
const proc = await bootServer(root, port);
|
|
86
|
+
try {
|
|
87
|
+
const r = await fetch(`http://localhost:${port}/_api/annotations`, {
|
|
88
|
+
method: 'PUT',
|
|
89
|
+
headers: { 'Content-Type': 'application/json' },
|
|
90
|
+
body: JSON.stringify({ file: '.design/ui/X.tsx', svg: '<p>nope</p>' }),
|
|
91
|
+
});
|
|
92
|
+
expect(r.status).toBe(400);
|
|
93
|
+
} finally {
|
|
94
|
+
await killProc(proc);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('PUT rejects missing file', async () => {
|
|
99
|
+
const { root } = makeSandbox();
|
|
100
|
+
const port = nextPort();
|
|
101
|
+
const proc = await bootServer(root, port);
|
|
102
|
+
try {
|
|
103
|
+
const r = await fetch(`http://localhost:${port}/_api/annotations`, {
|
|
104
|
+
method: 'PUT',
|
|
105
|
+
headers: { 'Content-Type': 'application/json' },
|
|
106
|
+
body: JSON.stringify({ svg: SVG_OK }),
|
|
107
|
+
});
|
|
108
|
+
expect(r.status).toBe(400);
|
|
109
|
+
} finally {
|
|
110
|
+
await killProc(proc);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('PUT rejects bodies above the 1 MB ceiling', async () => {
|
|
115
|
+
const { root } = makeSandbox();
|
|
116
|
+
const port = nextPort();
|
|
117
|
+
const proc = await bootServer(root, port);
|
|
118
|
+
try {
|
|
119
|
+
const huge = `<svg>${'x'.repeat(1024 * 1024 + 100)}</svg>`;
|
|
120
|
+
const r = await fetch(`http://localhost:${port}/_api/annotations`, {
|
|
121
|
+
method: 'PUT',
|
|
122
|
+
headers: { 'Content-Type': 'application/json' },
|
|
123
|
+
body: JSON.stringify({ file: '.design/ui/X.tsx', svg: huge }),
|
|
124
|
+
});
|
|
125
|
+
// readJson throws on body too large → readJson returns null → 400.
|
|
126
|
+
// saveAnnotations would also reject on length, so either path yields 400.
|
|
127
|
+
expect(r.status).toBe(400);
|
|
128
|
+
} finally {
|
|
129
|
+
await killProc(proc);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('Unknown method (DELETE) returns 405', async () => {
|
|
134
|
+
const { root } = makeSandbox();
|
|
135
|
+
const port = nextPort();
|
|
136
|
+
const proc = await bootServer(root, port);
|
|
137
|
+
try {
|
|
138
|
+
const r = await fetch(`http://localhost:${port}/_api/annotations`, {
|
|
139
|
+
method: 'DELETE',
|
|
140
|
+
});
|
|
141
|
+
expect(r.status).toBe(405);
|
|
142
|
+
} finally {
|
|
143
|
+
await killProc(proc);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
});
|