@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.
Files changed (211) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +166 -0
  3. package/cli/bin/maude.exe +15 -0
  4. package/cli/bin/maude.mjs +45 -0
  5. package/cli/bin/mdcc.exe +10 -0
  6. package/cli/bin/mdcc.mjs +7 -0
  7. package/cli/cli-wrapper.cjs +67 -0
  8. package/cli/commands/config.mjs +94 -0
  9. package/cli/commands/design.mjs +386 -0
  10. package/cli/commands/help.mjs +57 -0
  11. package/cli/commands/init.mjs +178 -0
  12. package/cli/commands/version.mjs +7 -0
  13. package/cli/install.cjs +113 -0
  14. package/cli/lib/argv.mjs +37 -0
  15. package/cli/lib/argv.test.mjs +46 -0
  16. package/cli/lib/copy-tree.mjs +78 -0
  17. package/package.json +94 -0
  18. package/plugins/design/dev-server/annotations-context-toolbar.tsx +397 -0
  19. package/plugins/design/dev-server/annotations-layer.tsx +1717 -0
  20. package/plugins/design/dev-server/api.ts +674 -0
  21. package/plugins/design/dev-server/bin/_screenshot-playwright.mjs +50 -0
  22. package/plugins/design/dev-server/bin/bootstrap-check.sh +83 -0
  23. package/plugins/design/dev-server/bin/canvas-edit.sh +48 -0
  24. package/plugins/design/dev-server/bin/handoff.sh +27 -0
  25. package/plugins/design/dev-server/bin/screenshot.sh +232 -0
  26. package/plugins/design/dev-server/bin/server-up.sh +135 -0
  27. package/plugins/design/dev-server/bin/slug.sh +22 -0
  28. package/plugins/design/dev-server/bin/smoke.sh +272 -0
  29. package/plugins/design/dev-server/build.ts +267 -0
  30. package/plugins/design/dev-server/canvas-build.ts +219 -0
  31. package/plugins/design/dev-server/canvas-edit.ts +388 -0
  32. package/plugins/design/dev-server/canvas-header.ts +165 -0
  33. package/plugins/design/dev-server/canvas-icons.tsx +131 -0
  34. package/plugins/design/dev-server/canvas-lib-inline.ts +260 -0
  35. package/plugins/design/dev-server/canvas-lib-resolver.ts +85 -0
  36. package/plugins/design/dev-server/canvas-lib.tsx +1995 -0
  37. package/plugins/design/dev-server/canvas-meta.schema.json +181 -0
  38. package/plugins/design/dev-server/canvas-pipeline.ts +270 -0
  39. package/plugins/design/dev-server/canvas-shell.tsx +813 -0
  40. package/plugins/design/dev-server/client/app.jsx +2027 -0
  41. package/plugins/design/dev-server/client/hmr.mjs +85 -0
  42. package/plugins/design/dev-server/client/iframe-lazy.mjs +121 -0
  43. package/plugins/design/dev-server/client/index.html +15 -0
  44. package/plugins/design/dev-server/client/styles/0-reset.css +18 -0
  45. package/plugins/design/dev-server/client/styles/1-tokens.css +297 -0
  46. package/plugins/design/dev-server/client/styles/2-layout.css +35 -0
  47. package/plugins/design/dev-server/client/styles/3-shell.css +906 -0
  48. package/plugins/design/dev-server/client/styles/4-components.css +1268 -0
  49. package/plugins/design/dev-server/client/styles/5-utilities.css +4 -0
  50. package/plugins/design/dev-server/client/styles/_index.css +24 -0
  51. package/plugins/design/dev-server/client/styles.css +1419 -0
  52. package/plugins/design/dev-server/config.schema.json +147 -0
  53. package/plugins/design/dev-server/context-menu.tsx +343 -0
  54. package/plugins/design/dev-server/context.ts +173 -0
  55. package/plugins/design/dev-server/dist/client.bundle.js +20323 -0
  56. package/plugins/design/dev-server/dist/styles.css +2875 -0
  57. package/plugins/design/dev-server/examples/README.md +9 -0
  58. package/plugins/design/dev-server/examples/perf-100-artboards.tsx +113 -0
  59. package/plugins/design/dev-server/fs-watch.ts +63 -0
  60. package/plugins/design/dev-server/handoff.ts +721 -0
  61. package/plugins/design/dev-server/history.ts +125 -0
  62. package/plugins/design/dev-server/hmr-broadcast.ts +114 -0
  63. package/plugins/design/dev-server/http.ts +413 -0
  64. package/plugins/design/dev-server/input-router.tsx +485 -0
  65. package/plugins/design/dev-server/inspect.ts +365 -0
  66. package/plugins/design/dev-server/locator.ts +159 -0
  67. package/plugins/design/dev-server/mem.ts +97 -0
  68. package/plugins/design/dev-server/runtime-bundle.ts +235 -0
  69. package/plugins/design/dev-server/server.mjs +1246 -0
  70. package/plugins/design/dev-server/server.ts +131 -0
  71. package/plugins/design/dev-server/test/_helpers.ts +81 -0
  72. package/plugins/design/dev-server/test/active-state.test.ts +145 -0
  73. package/plugins/design/dev-server/test/annotations-api.test.ts +146 -0
  74. package/plugins/design/dev-server/test/annotations-layer.test.ts +419 -0
  75. package/plugins/design/dev-server/test/binary-smoke.test.ts +47 -0
  76. package/plugins/design/dev-server/test/bundle-smoke.test.ts +29 -0
  77. package/plugins/design/dev-server/test/canvas-build.test.ts +78 -0
  78. package/plugins/design/dev-server/test/canvas-edit.test.ts +139 -0
  79. package/plugins/design/dev-server/test/canvas-header.test.ts +127 -0
  80. package/plugins/design/dev-server/test/canvas-lib-inline.test.ts +146 -0
  81. package/plugins/design/dev-server/test/canvas-lib-resolver.test.ts +112 -0
  82. package/plugins/design/dev-server/test/canvas-meta-api.test.ts +236 -0
  83. package/plugins/design/dev-server/test/canvas-pipeline.test.ts +180 -0
  84. package/plugins/design/dev-server/test/canvas-route.test.ts +176 -0
  85. package/plugins/design/dev-server/test/fs-watch.test.ts +41 -0
  86. package/plugins/design/dev-server/test/handoff-static-frames.test.ts +215 -0
  87. package/plugins/design/dev-server/test/handoff.test.ts +281 -0
  88. package/plugins/design/dev-server/test/history-rollback.test.ts +62 -0
  89. package/plugins/design/dev-server/test/hmr-broadcast.test.ts +108 -0
  90. package/plugins/design/dev-server/test/input-router.test.ts +316 -0
  91. package/plugins/design/dev-server/test/locator.test.ts +214 -0
  92. package/plugins/design/dev-server/test/perf-harness.ts +193 -0
  93. package/plugins/design/dev-server/test/phase-3.6-smoke.test.ts +77 -0
  94. package/plugins/design/dev-server/test/runtime-bundle.test.ts +69 -0
  95. package/plugins/design/dev-server/test/server-lifecycle.test.ts +28 -0
  96. package/plugins/design/dev-server/test/tool-palette.test.tsx +55 -0
  97. package/plugins/design/dev-server/test/use-annotation-selection.test.tsx +77 -0
  98. package/plugins/design/dev-server/test/use-artboard-drag.test.ts +325 -0
  99. package/plugins/design/dev-server/test/use-selection-set.test.tsx +166 -0
  100. package/plugins/design/dev-server/test/use-snap-guides.test.ts +190 -0
  101. package/plugins/design/dev-server/test/use-tool-mode.test.tsx +93 -0
  102. package/plugins/design/dev-server/test/ws-handshake.test.ts +33 -0
  103. package/plugins/design/dev-server/tool-palette.tsx +278 -0
  104. package/plugins/design/dev-server/tsconfig.json +26 -0
  105. package/plugins/design/dev-server/use-annotation-selection.tsx +92 -0
  106. package/plugins/design/dev-server/use-annotations-visibility.tsx +43 -0
  107. package/plugins/design/dev-server/use-artboard-drag.tsx +445 -0
  108. package/plugins/design/dev-server/use-selection-set.tsx +224 -0
  109. package/plugins/design/dev-server/use-snap-guides.tsx +215 -0
  110. package/plugins/design/dev-server/use-tool-mode.tsx +114 -0
  111. package/plugins/design/dev-server/ws.ts +90 -0
  112. package/plugins/design/templates/_shell.html +177 -0
  113. package/plugins/design/templates/canvas.tsx.template +54 -0
  114. package/plugins/design/templates/design-system-inspiration/_MAPPING.md +277 -0
  115. package/plugins/design/templates/design-system-inspiration/_README.md +71 -0
  116. package/plugins/design/templates/design-system-inspiration/audience-consumer/components-banner.html +68 -0
  117. package/plugins/design/templates/design-system-inspiration/audience-consumer/components-empty-state-generous.html +39 -0
  118. package/plugins/design/templates/design-system-inspiration/audience-consumer/components-feature-grid.html +62 -0
  119. package/plugins/design/templates/design-system-inspiration/audience-consumer/components-marketing-card.html +63 -0
  120. package/plugins/design/templates/design-system-inspiration/audience-consumer/components-testimonial.html +80 -0
  121. package/plugins/design/templates/design-system-inspiration/audience-developer/components-code-block.html +71 -0
  122. package/plugins/design/templates/design-system-inspiration/audience-developer/components-diff-view.html +65 -0
  123. package/plugins/design/templates/design-system-inspiration/audience-developer/components-log-stream.html +62 -0
  124. package/plugins/design/templates/design-system-inspiration/audience-developer/components-monospace-table.html +57 -0
  125. package/plugins/design/templates/design-system-inspiration/audience-developer/components-terminal-pane.html +67 -0
  126. package/plugins/design/templates/design-system-inspiration/audience-developer/type-mono.html +57 -0
  127. package/plugins/design/templates/design-system-inspiration/audience-pro/colors-presence.html +74 -0
  128. package/plugins/design/templates/design-system-inspiration/audience-pro/components-command-palette.html +90 -0
  129. package/plugins/design/templates/design-system-inspiration/audience-pro/components-keyboard.html +51 -0
  130. package/plugins/design/templates/design-system-inspiration/audience-pro/components-list.html +89 -0
  131. package/plugins/design/templates/design-system-inspiration/audience-pro/components-shortcuts-overlay.html +85 -0
  132. package/plugins/design/templates/design-system-inspiration/audience-pro/components-toast-menu.html +80 -0
  133. package/plugins/design/templates/design-system-inspiration/core/INDEX.md.tpl +25 -0
  134. package/plugins/design/templates/design-system-inspiration/core/README.orchestration.md.tpl +71 -0
  135. package/plugins/design/templates/design-system-inspiration/core/README.philosophy.md.tpl +77 -0
  136. package/plugins/design/templates/design-system-inspiration/core/SKILL.md.tpl +50 -0
  137. package/plugins/design/templates/design-system-inspiration/core/colors_and_type.css.tpl +111 -0
  138. package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +28 -0
  139. package/plugins/design/templates/design-system-inspiration/core/preview/_layout.css +113 -0
  140. package/plugins/design/templates/design-system-inspiration/core/preview/colors-accent.html +48 -0
  141. package/plugins/design/templates/design-system-inspiration/core/preview/colors-surfaces.html +49 -0
  142. package/plugins/design/templates/design-system-inspiration/core/preview/colors-text.html +52 -0
  143. package/plugins/design/templates/design-system-inspiration/core/preview/components-buttons.html +83 -0
  144. package/plugins/design/templates/design-system-inspiration/core/preview/components-cards.html +79 -0
  145. package/plugins/design/templates/design-system-inspiration/core/preview/components-inputs.html +82 -0
  146. package/plugins/design/templates/design-system-inspiration/core/preview/motion.html +66 -0
  147. package/plugins/design/templates/design-system-inspiration/core/preview/spacing-scale.html +53 -0
  148. package/plugins/design/templates/design-system-inspiration/core/preview/type-scale.html +62 -0
  149. package/plugins/design/templates/design-system-inspiration/foundations/borders.html +39 -0
  150. package/plugins/design/templates/design-system-inspiration/foundations/elevation.html +46 -0
  151. package/plugins/design/templates/design-system-inspiration/foundations/focus.html +48 -0
  152. package/plugins/design/templates/design-system-inspiration/foundations/grid.html +51 -0
  153. package/plugins/design/templates/design-system-inspiration/foundations/iconography.html +73 -0
  154. package/plugins/design/templates/design-system-inspiration/foundations/opacity.html +45 -0
  155. package/plugins/design/templates/design-system-inspiration/foundations/radii.html +40 -0
  156. package/plugins/design/templates/design-system-inspiration/foundations/selection.html +45 -0
  157. package/plugins/design/templates/design-system-inspiration/meta/accessibility.html +62 -0
  158. package/plugins/design/templates/design-system-inspiration/meta/i18n.html +73 -0
  159. package/plugins/design/templates/design-system-inspiration/meta/presence-multiplayer.html +71 -0
  160. package/plugins/design/templates/design-system-inspiration/meta/tokens-index.html +80 -0
  161. package/plugins/design/templates/design-system-inspiration/patterns/patterns-auth.html +78 -0
  162. package/plugins/design/templates/design-system-inspiration/patterns/patterns-data-density.html +61 -0
  163. package/plugins/design/templates/design-system-inspiration/patterns/patterns-error-pages.html +70 -0
  164. package/plugins/design/templates/design-system-inspiration/patterns/patterns-form-layouts.html +70 -0
  165. package/plugins/design/templates/design-system-inspiration/patterns/patterns-onboarding.html +71 -0
  166. package/plugins/design/templates/design-system-inspiration/patterns/patterns-pricing.html +83 -0
  167. package/plugins/design/templates/design-system-inspiration/platform-desktop/components-resize-panels.html +63 -0
  168. package/plugins/design/templates/design-system-inspiration/platform-desktop/ui_kits-desktop-index.html +55 -0
  169. package/plugins/design/templates/design-system-inspiration/platform-desktop/ui_kits-desktop-showcase.html +302 -0
  170. package/plugins/design/templates/design-system-inspiration/platform-mobile/components-bottom-sheet.html +63 -0
  171. package/plugins/design/templates/design-system-inspiration/platform-mobile/components-pull-to-refresh.html +74 -0
  172. package/plugins/design/templates/design-system-inspiration/platform-mobile/components-segmented-control.html +51 -0
  173. package/plugins/design/templates/design-system-inspiration/platform-mobile/components-tab-bar.html +57 -0
  174. package/plugins/design/templates/design-system-inspiration/platform-mobile/ui_kits-mobile-index.html +58 -0
  175. package/plugins/design/templates/design-system-inspiration/platform-mobile/ui_kits-mobile-showcase.html +237 -0
  176. package/plugins/design/templates/design-system-inspiration/status/colors-status.html +49 -0
  177. package/plugins/design/templates/design-system-inspiration/status/components-status.html +63 -0
  178. package/plugins/design/templates/design-system-inspiration/status/skeletons.html +74 -0
  179. package/plugins/design/templates/design-system-inspiration/theme-both/colors-themes-side-by-side.html +59 -0
  180. package/plugins/design/templates/design-system-inspiration/universal/components-callout.html +74 -0
  181. package/plugins/design/templates/design-system-inspiration/universal/components-dialogs.html +81 -0
  182. package/plugins/design/templates/design-system-inspiration/universal/components-tables.html +101 -0
  183. package/plugins/design/templates/design-system-inspiration/universal/components-toggles.html +74 -0
  184. package/plugins/design/templates/design-system-inspiration/universal/components-tooltips.html +74 -0
  185. package/plugins/design/templates/design-system-inspiration/universal/empty-state.html.tpl +34 -0
  186. package/plugins/design/templates/design-system-inspiration/universal/logo.html +42 -0
  187. package/plugins/design/templates/ds-specimen.tsx.template +59 -0
  188. package/plugins/flow/.claude-plugin/config.schema.json +398 -0
  189. package/plugins/flow/README.md +45 -0
  190. package/plugins/flow/templates/ai-skeleton/INDEX.md +50 -0
  191. package/plugins/flow/templates/ai-skeleton/README.md +46 -0
  192. package/plugins/flow/templates/ai-skeleton/browser/har/.gitkeep +0 -0
  193. package/plugins/flow/templates/ai-skeleton/browser/snapshots/.gitkeep +0 -0
  194. package/plugins/flow/templates/ai-skeleton/business/README.md +12 -0
  195. package/plugins/flow/templates/ai-skeleton/context/README.md +12 -0
  196. package/plugins/flow/templates/ai-skeleton/decisions/README.md +20 -0
  197. package/plugins/flow/templates/ai-skeleton/design-import/.gitkeep +0 -0
  198. package/plugins/flow/templates/ai-skeleton/dev-logs/.gitkeep +0 -0
  199. package/plugins/flow/templates/ai-skeleton/device/.gitkeep +0 -0
  200. package/plugins/flow/templates/ai-skeleton/docs/README.md +5 -0
  201. package/plugins/flow/templates/ai-skeleton/logs/.gitkeep +0 -0
  202. package/plugins/flow/templates/ai-skeleton/logs/README.md +13 -0
  203. package/plugins/flow/templates/ai-skeleton/plans/README.md +16 -0
  204. package/plugins/flow/templates/ai-skeleton/plans/archive/.gitkeep +0 -0
  205. package/plugins/flow/templates/ai-skeleton/release-guide.md +35 -0
  206. package/plugins/flow/templates/ai-skeleton/reviews/README.md +15 -0
  207. package/plugins/flow/templates/ai-skeleton/scenarios/README.md +26 -0
  208. package/plugins/flow/templates/ai-skeleton/scenarios/_lib/.gitkeep +0 -0
  209. package/plugins/flow/templates/ai-skeleton/state/STATE.md +25 -0
  210. package/plugins/flow/templates/ai-skeleton/templates/HANDOFF.md +32 -0
  211. package/plugins/flow/templates/ai-skeleton/workflows.config.json +74 -0
@@ -0,0 +1,219 @@
1
+ // Browser-loadable canvas bundle (DDR-019, Phase 3.6 Task 6).
2
+ //
3
+ // The pre-3.6 pipeline (canvas-pipeline.ts) parses the TSX, injects data-cd-id,
4
+ // then lowers JSX via Bun.Transpiler. Output is JS but uses Bun-runtime-internal
5
+ // `jsxDEV_<hash>` symbol names — not browser-loadable.
6
+ //
7
+ // This module wraps that pipeline with a second pass through Bun.build, which
8
+ // resolves the JSX runtime against the canonical "react/jsx-dev-runtime" import
9
+ // and externalises React + ReactDOM. Output is standard ES module text. The
10
+ // browser loads it via the runtime importmap declared in _shell.html.
11
+ //
12
+ // The two-stage approach is deliberate:
13
+ //
14
+ // 1. canvas-pipeline.ts (oxc + magic-string) — owns identity. Pre-orders JSX
15
+ // elements, injects data-cd-id, writes _locator.json. Pure + fast.
16
+ // 2. canvas-build.ts (Bun.build) — owns module shape. Feeds the post-pass-1
17
+ // source to Bun.build via a virtual-loader plugin, so the bundler
18
+ // processes our edited source (not the on-disk file). Externalises every
19
+ // runtime package the importmap covers.
20
+ //
21
+ // Caller (http.ts) reads the canvas source, calls buildCanvasModule(), then
22
+ // serves the resulting JS at /<designRel>/ui/<slug>.tsx with Content-Type
23
+ // `application/javascript`.
24
+
25
+ import { existsSync } from 'node:fs';
26
+ import path from 'node:path';
27
+
28
+ import { canvasLibPath, canvasLibResolver } from './canvas-lib-resolver.ts';
29
+ import { transpileCanvasSource } from './canvas-pipeline.ts';
30
+ import type { LocatorMap } from './locator.ts';
31
+ import { RUNTIME_PACKAGES } from './runtime-bundle.ts';
32
+
33
+ // Sanity-check the dev-server-bundled canvas-lib once per process boot. If the
34
+ // install is corrupt (file missing), surface that before the first canvas build
35
+ // — Bun.build's plugin throws collapse to a useless "Bundle failed" string.
36
+ let canvasLibPresenceVerified = false;
37
+ function verifyCanvasLibPresence(): void {
38
+ if (canvasLibPresenceVerified) return;
39
+ const target = canvasLibPath();
40
+ if (!existsSync(target)) {
41
+ throw new Error(
42
+ `[@maude/canvas-lib] canvas library missing at ${target} — dev-server install is corrupt; re-install @1agh/maude.`
43
+ );
44
+ }
45
+ canvasLibPresenceVerified = true;
46
+ }
47
+
48
+ // Per DDR-025, canvas-lib now ships with the dev-server. Downstream projects
49
+ // with a legacy `<designRoot>/_lib/canvas-lib.tsx` from a pre-4.0.5 setup get a
50
+ // single deprecation warning per dev-server process — the project file is
51
+ // ignored, the dev-server-bundled lib is authoritative.
52
+ const loggedLegacyForRoot = new Set<string>();
53
+ function warnLegacyDesignLib(designRoot: string): void {
54
+ if (loggedLegacyForRoot.has(designRoot)) return;
55
+ loggedLegacyForRoot.add(designRoot);
56
+ const legacy = path.join(designRoot, '_lib', 'canvas-lib.tsx');
57
+ if (existsSync(legacy)) {
58
+ console.warn(
59
+ `[canvas-lib] Legacy ${legacy} detected. As of v0.15.0, canvas-lib ships with the dev-server install — the project file is ignored and can be deleted. See DDR-025 for the migration rationale.`
60
+ );
61
+ }
62
+ }
63
+
64
+ export interface CanvasBundleResult {
65
+ /** Browser-loadable ES module text. */
66
+ js: string;
67
+ /** data-cd-id → source location map (same as the pipeline emits). */
68
+ locator: LocatorMap;
69
+ /** Content-derived hash; suitable as HTTP ETag. */
70
+ etag: string;
71
+ }
72
+
73
+ export interface BuildCanvasOptions {
74
+ /**
75
+ * Absolute path to the design root. Per DDR-025 canvas-lib is dev-server
76
+ * bundled, so this value is no longer required to resolve `@maude/canvas-lib`
77
+ * — it's accepted for back-compat and used only to emit a one-shot
78
+ * deprecation warning when a legacy `<designRoot>/_lib/canvas-lib.tsx` is
79
+ * detected.
80
+ */
81
+ designRoot?: string;
82
+ }
83
+
84
+ /**
85
+ * Build a single canvas TSX file end-to-end. Identity pass is from
86
+ * canvas-pipeline.ts; the module pass is Bun.build with React externalised
87
+ * and `@maude/canvas-lib` resolved to the dev-server-bundled canvas-lib.
88
+ */
89
+ export async function buildCanvasModule(
90
+ canvasAbsPath: string,
91
+ source: string,
92
+ options: BuildCanvasOptions = {}
93
+ ): Promise<CanvasBundleResult> {
94
+ // Pass 1: inject data-cd-id, capture the locator.
95
+ const pass1 = transpileCanvasSource(canvasAbsPath, source);
96
+
97
+ // Sanity-check the dev-server-bundled canvas-lib once per process — if the
98
+ // install is corrupt, fail loud before Bun.build collapses plugin throws.
99
+ if (/@maude\/canvas-lib/.test(source)) {
100
+ verifyCanvasLibPresence();
101
+ }
102
+
103
+ // Non-destructive deprecation warning for projects still carrying a legacy
104
+ // `<designRoot>/_lib/canvas-lib.tsx`. The dev-server-bundled lib is
105
+ // authoritative; this just nudges the project owner to clean up.
106
+ if (options.designRoot) {
107
+ warnLegacyDesignLib(options.designRoot);
108
+ }
109
+
110
+ // Pass 2: Bun.build with a virtual loader that resolves canvasAbsPath to the
111
+ // post-pass-1 TSX. Every other import (npm packages, relative imports of
112
+ // sibling canvas components) goes through Bun's default resolver. The four
113
+ // runtime packages are externalised so they resolve through the importmap.
114
+ // Phase 5.1 — `react-dom` is now its own runtime package (so createPortal is
115
+ // in the bundle). The flatMap legacy alias for `react-dom` is no longer
116
+ // needed; RUNTIME_PACKAGES already lists every specifier the importmap covers.
117
+ const externalSpecifiers = new Set<string>(RUNTIME_PACKAGES);
118
+
119
+ const built = await Bun.build({
120
+ entrypoints: [canvasAbsPath],
121
+ target: 'browser',
122
+ format: 'esm',
123
+ minify: false,
124
+ splitting: false,
125
+ define: {
126
+ // Match runtime-bundle.ts: production React. Without this the canvas's
127
+ // `import { jsxDEV } from "react/jsx-dev-runtime"` resolves against a
128
+ // bundle that fails to load due to a Bun.build naming collision in the
129
+ // dev variant. Both halves of the system MUST agree on the JSX runtime
130
+ // flavour.
131
+ 'process.env.NODE_ENV': '"production"',
132
+ },
133
+ plugins: [
134
+ // Resolve `@maude/canvas-lib` BEFORE exact-externals — we want the bare
135
+ // specifier to map to the dev-server-bundled lib, not get marked external.
136
+ canvasLibResolver(),
137
+ {
138
+ name: 'canvas-virtual-source',
139
+ setup(builder) {
140
+ builder.onLoad({ filter: filterForExactPath(canvasAbsPath) }, () => ({
141
+ contents: pass1.withIds,
142
+ loader: 'tsx',
143
+ }));
144
+ },
145
+ },
146
+ {
147
+ name: 'exact-externals',
148
+ setup(builder) {
149
+ builder.onResolve({ filter: /.*/ }, (args) => {
150
+ if (externalSpecifiers.has(args.path)) {
151
+ return { path: args.path, external: true };
152
+ }
153
+ return null;
154
+ });
155
+ },
156
+ },
157
+ ],
158
+ });
159
+
160
+ if (!built.success) {
161
+ const msg = built.logs.map((l) => l.message).join('\n');
162
+ throw new Error(`Bun.build failed on ${canvasAbsPath}:\n${msg}`);
163
+ }
164
+ const entry = built.outputs.find((o) => o.kind === 'entry-point');
165
+ if (!entry) {
166
+ throw new Error(`Bun.build produced no entry-point output for ${canvasAbsPath}`);
167
+ }
168
+ let js = await entry.text();
169
+
170
+ // Gather any sibling CSS the bundler produced from `import "./<slug>.css"`
171
+ // statements in the canvas/specimen TSX. Bun.build extracts those into a
172
+ // separate `kind: "asset"` CSS file. Browser-loaded ESM doesn't process
173
+ // `import "*.css"` natively, so we inline the CSS via a `<style>` tag
174
+ // injection at module-init time — keeps each canvas self-contained without
175
+ // needing a parallel `<link>` request.
176
+ const cssAssets = built.outputs.filter((o) => o.kind === 'asset' && o.path.endsWith('.css'));
177
+ if (cssAssets.length > 0) {
178
+ let css = '';
179
+ for (const a of cssAssets) css += await a.text();
180
+ if (css.trim().length > 0) {
181
+ const slug = canvasAbsPath.split('/').pop() ?? 'canvas';
182
+ js = buildCssInjector(slug, css) + js;
183
+ }
184
+ }
185
+
186
+ const etag = Bun.hash(js).toString(16);
187
+ return { js, locator: pass1.locator, etag };
188
+ }
189
+
190
+ /**
191
+ * Synthesize a module-init prologue that creates a `<style data-canvas-css>`
192
+ * tag with the bundled CSS text. Idempotent per-slug — duplicate mounts of
193
+ * the same canvas don't re-inject. Run at top-level so it executes before
194
+ * the React component does its first render.
195
+ */
196
+ function buildCssInjector(slug: string, css: string): string {
197
+ // JSON-encode the CSS text so we don't need to worry about backticks,
198
+ // backslashes, or embedded `</style>` (which would break a raw template).
199
+ const enc = JSON.stringify(css);
200
+ const id = `canvas-css-${slug.replace(/[^a-zA-Z0-9-]/g, '_')}`;
201
+ return `// canvas-build: inject bundled sibling CSS so the canvas is self-contained.\n(function(){if(typeof document==="undefined")return;if(document.getElementById(${JSON.stringify(id)}))return;var s=document.createElement("style");s.id=${JSON.stringify(id)};s.dataset.canvasCss="bundled";s.textContent=${enc};document.head.appendChild(s);})();\n`;
202
+ }
203
+
204
+ function filterForExactPath(absPath: string): RegExp {
205
+ // Bun.build hands onLoad the resolved on-disk path. On macOS /tmp is a
206
+ // symlink to /private/tmp, so an entrypoint of "/tmp/foo.tsx" arrives in
207
+ // onLoad as "/private/tmp/foo.tsx". Match on the *trailing* path
208
+ // segments — same filename + parent dir — which is enough to uniquely
209
+ // identify a canvas file path while tolerating prefix normalisations.
210
+ // We require the last two segments to match so a canvas with the same
211
+ // filename in a different directory doesn't collide.
212
+ const parts = absPath.split('/');
213
+ const tail = parts.slice(-2).join('/');
214
+ return new RegExp(`/${escapeRegex(tail)}$`);
215
+ }
216
+
217
+ function escapeRegex(s: string): string {
218
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
219
+ }
@@ -0,0 +1,388 @@
1
+ // AST-aware single-element edits for the `/design:edit` Step 3a fast path
2
+ // (DDR-019, Phase 3.6 Task 5).
3
+ //
4
+ // Caller hands us (canvasAbsPath, dataCdId, attr, value); we parse the TSX,
5
+ // re-walk it with the same component/jsxIndex bookkeeping as canvas-pipeline.ts,
6
+ // find the JSX element whose ID matches, and rewrite a single attribute via
7
+ // magic-string. The two-pass-transform contract (DDR-019) is the only thing
8
+ // that keeps source-DOM identity stable across edits — so the editor lives
9
+ // next to the transpiler and shares its toolchain (oxc-parser + magic-string).
10
+ //
11
+ // Supported `attr` syntaxes:
12
+ // - "className" → swap the value of the `className` JSX attribute
13
+ // (insert one if missing). Value form: bare string.
14
+ // - "style.<prop>" → swap (or insert) a single CSS-property key inside
15
+ // the inline `style={{ ... }}` object. Value form:
16
+ // literal text inserted between `:` and `,` — pass a
17
+ // JS expression (string with quotes for strings, raw
18
+ // number for numbers).
19
+ // - "<any other name>" → swap (or insert) the value of a plain string
20
+ // attribute (aria-label, role, title, ...).
21
+ //
22
+ // All edits preserve every other attribute byte-for-byte. The `data-cd-id`
23
+ // attribute is intentionally NOT writable through this path — the pipeline
24
+ // owns those.
25
+ //
26
+ // Concurrent edits against the same canvas serialise behind a per-file mutex
27
+ // (matches the locator.ts pattern). Two parallel edits against different
28
+ // canvases run in parallel.
29
+
30
+ import MagicString from 'magic-string';
31
+ import { parseSync } from 'oxc-parser';
32
+
33
+ export class CanvasEditError extends Error {
34
+ readonly canvas: string;
35
+ readonly id: string;
36
+ constructor(message: string, info: { canvas: string; id: string }) {
37
+ super(message);
38
+ this.name = 'CanvasEditError';
39
+ this.canvas = info.canvas;
40
+ this.id = info.id;
41
+ }
42
+ }
43
+
44
+ const PASCAL_CASE = /^[A-Z][A-Za-z0-9_]*$/;
45
+ // biome-ignore lint/suspicious/noExplicitAny: oxc-parser AST nodes are heterogeneous.
46
+ type AnyNode = any;
47
+
48
+ function isPascalIdent(name: unknown): name is string {
49
+ return typeof name === 'string' && PASCAL_CASE.test(name);
50
+ }
51
+
52
+ function componentNameOf(node: AnyNode): string | null {
53
+ if (!node || typeof node !== 'object') return null;
54
+ if (node.type === 'FunctionDeclaration' && isPascalIdent(node.id?.name)) return node.id.name;
55
+ if (node.type === 'VariableDeclarator' && isPascalIdent(node.id?.name)) {
56
+ const init = node.init;
57
+ if (init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')) {
58
+ return node.id.name;
59
+ }
60
+ }
61
+ if (node.type === 'FunctionExpression' && isPascalIdent(node.id?.name)) return node.id.name;
62
+ return null;
63
+ }
64
+
65
+ function computeId(componentName: string, idx: number): string {
66
+ return Bun.hash(`${componentName}:${idx}`).toString(16).padStart(16, '0').slice(0, 8);
67
+ }
68
+
69
+ interface OpeningHit {
70
+ opening: AnyNode;
71
+ }
72
+
73
+ /**
74
+ * Find the openingElement of the JSX element whose pipeline-computed ID matches
75
+ * `targetId`. Walks pre-order with the same component+jsxIndex bookkeeping the
76
+ * pipeline uses, so the ID arithmetic stays in lockstep. Returns null if no
77
+ * match.
78
+ */
79
+ function findOpening(program: AnyNode, targetId: string): OpeningHit | null {
80
+ interface Frame {
81
+ componentName: string;
82
+ jsxIndex: number;
83
+ }
84
+ const stack: Frame[] = [{ componentName: '', jsxIndex: 0 }];
85
+ let hit: OpeningHit | null = null;
86
+
87
+ function visit(node: AnyNode): void {
88
+ if (hit || !node || typeof node !== 'object') return;
89
+ if (Array.isArray(node)) {
90
+ for (const c of node) {
91
+ if (hit) return;
92
+ visit(c);
93
+ }
94
+ return;
95
+ }
96
+ if (typeof node.type !== 'string') return;
97
+
98
+ const newComp = componentNameOf(node);
99
+ let pushed = false;
100
+ if (newComp !== null) {
101
+ stack.push({ componentName: newComp, jsxIndex: 0 });
102
+ pushed = true;
103
+ }
104
+
105
+ if (node.type === 'JSXElement') {
106
+ const frame = stack[stack.length - 1] as Frame;
107
+ const idx = frame.jsxIndex;
108
+ frame.jsxIndex += 1;
109
+ const id = computeId(frame.componentName, idx);
110
+ if (id === targetId) {
111
+ hit = { opening: node.openingElement };
112
+ }
113
+ if (!hit) {
114
+ if (node.openingElement) visit(node.openingElement.attributes);
115
+ visit(node.children);
116
+ }
117
+ if (pushed) stack.pop();
118
+ return;
119
+ }
120
+
121
+ for (const k of Object.keys(node)) {
122
+ if (k === 'loc' || k === 'range' || k === 'start' || k === 'end' || k === 'type') continue;
123
+ visit(node[k]);
124
+ }
125
+
126
+ if (pushed) stack.pop();
127
+ }
128
+
129
+ visit(program);
130
+ return hit;
131
+ }
132
+
133
+ function findAttribute(opening: AnyNode, name: string): AnyNode | null {
134
+ const attrs = opening?.attributes;
135
+ if (!Array.isArray(attrs)) return null;
136
+ for (const a of attrs) {
137
+ if (a?.type === 'JSXAttribute' && a.name?.type === 'JSXIdentifier' && a.name.name === name) {
138
+ return a;
139
+ }
140
+ }
141
+ return null;
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Per-canvas mutex. Mirrors locator.ts; two concurrent edits against the same
146
+ // .tsx file serialise so they can't race read-modify-write.
147
+
148
+ const locks = new Map<string, Promise<void>>();
149
+ function withLock<T>(filePath: string, fn: () => Promise<T>): Promise<T> {
150
+ const prev = locks.get(filePath) ?? Promise.resolve();
151
+ let release!: () => void;
152
+ const gate = new Promise<void>((res) => {
153
+ release = res;
154
+ });
155
+ const next = prev.then(() => gate);
156
+ locks.set(filePath, next);
157
+ return prev.then(fn).finally(() => {
158
+ release();
159
+ if (locks.get(filePath) === next) locks.delete(filePath);
160
+ });
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Public API.
165
+
166
+ export interface EditResult {
167
+ /** The post-edit source (also written to disk by editAttribute()). */
168
+ source: string;
169
+ /** Number of bytes the edit changed (positive = grew, negative = shrunk). */
170
+ delta: number;
171
+ }
172
+
173
+ /**
174
+ * Apply a single-attribute edit to the JSX element with the given `data-cd-id`.
175
+ * Reads the canvas, rewrites in memory, writes atomically (via Bun.write to a
176
+ * tmp + rename) so a concurrent reader never sees a partial file.
177
+ */
178
+ export async function editAttribute(
179
+ canvasAbsPath: string,
180
+ id: string,
181
+ attr: string,
182
+ value: string
183
+ ): Promise<EditResult> {
184
+ return withLock(canvasAbsPath, async () => {
185
+ const file = Bun.file(canvasAbsPath);
186
+ if (!(await file.exists())) {
187
+ throw new CanvasEditError(`Canvas not found: ${canvasAbsPath}`, {
188
+ canvas: canvasAbsPath,
189
+ id,
190
+ });
191
+ }
192
+ const source = await file.text();
193
+ const next = applyEdit(canvasAbsPath, source, id, attr, value);
194
+ if (next.source === source) return { source, delta: 0 };
195
+ const tmp = `${canvasAbsPath}.tmp.${Math.random().toString(36).slice(2, 10)}`;
196
+ await Bun.write(tmp, next.source);
197
+ const { rename } = await import('node:fs/promises');
198
+ await rename(tmp, canvasAbsPath);
199
+ return next;
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Pure variant — exposed for tests + in-memory pipelines. Caller owns
205
+ * persistence. Throws CanvasEditError if the ID isn't found or the edit shape
206
+ * isn't representable.
207
+ */
208
+ export function applyEdit(
209
+ canvasAbsPath: string,
210
+ source: string,
211
+ id: string,
212
+ attr: string,
213
+ value: string
214
+ ): EditResult {
215
+ const parsed = parseSync(canvasAbsPath, source, { sourceType: 'module' });
216
+ if (parsed.errors && parsed.errors.length > 0) {
217
+ const first = parsed.errors[0];
218
+ throw new CanvasEditError(
219
+ `oxc-parser failed on ${canvasAbsPath}: ${first?.message ?? 'unknown'}`,
220
+ { canvas: canvasAbsPath, id }
221
+ );
222
+ }
223
+
224
+ const hit = findOpening(parsed.program, id);
225
+ if (!hit) {
226
+ throw new CanvasEditError(`data-cd-id "${id}" not found in ${canvasAbsPath}`, {
227
+ canvas: canvasAbsPath,
228
+ id,
229
+ });
230
+ }
231
+
232
+ const s = new MagicString(source);
233
+ if (attr.startsWith('style.')) {
234
+ editStyleProp(s, hit.opening, attr.slice('style.'.length), value, canvasAbsPath, id);
235
+ } else if (attr === 'data-cd-id') {
236
+ throw new CanvasEditError('data-cd-id is owned by the pipeline; cannot be edited', {
237
+ canvas: canvasAbsPath,
238
+ id,
239
+ });
240
+ } else {
241
+ editStringAttr(s, hit.opening, attr, value);
242
+ }
243
+
244
+ const out = s.toString();
245
+ return { source: out, delta: out.length - source.length };
246
+ }
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // Edit shapes.
250
+
251
+ function editStringAttr(s: MagicString, opening: AnyNode, name: string, value: string): void {
252
+ const attr = findAttribute(opening, name);
253
+ if (attr) {
254
+ // Replace existing value. JSX attribute value forms we handle:
255
+ // - <Tag name="literal" /> → replace inside the quotes
256
+ // - <Tag name={'literal'} /> → wrap quotes around new value
257
+ // - <Tag name={expr} /> → replace the whole expression text
258
+ // - <Tag name /> → no value node; add `="..."`
259
+ const v = attr.value;
260
+ if (!v) {
261
+ // <Tag name /> → <Tag name="value" />
262
+ s.appendLeft(attr.end, `="${escapeAttr(value)}"`);
263
+ return;
264
+ }
265
+ if (v.type === 'Literal' || v.type === 'StringLiteral') {
266
+ // Replace just the value text, keeping surrounding quotes.
267
+ s.overwrite(v.start, v.end, JSON.stringify(value));
268
+ return;
269
+ }
270
+ if (v.type === 'JSXExpressionContainer') {
271
+ // Replace the whole `{...}` with a plain quoted literal — keeps the
272
+ // resulting JSX readable, no escaping gymnastics.
273
+ s.overwrite(v.start, v.end, JSON.stringify(value));
274
+ return;
275
+ }
276
+ // Unknown shape — refuse rather than corrupt.
277
+ throw new Error(`Unsupported JSX attribute value shape: ${v.type}`);
278
+ }
279
+ // Attribute missing — insert right after the tag name (mirrors pipeline's
280
+ // injection point so attribute order stays predictable).
281
+ const insertAt: number | undefined = opening?.name?.end;
282
+ if (typeof insertAt !== 'number') {
283
+ throw new Error('Opening element has no resolvable name range');
284
+ }
285
+ s.appendLeft(insertAt, ` ${name}="${escapeAttr(value)}"`);
286
+ }
287
+
288
+ function escapeAttr(value: string): string {
289
+ return value.replace(/"/g, '&quot;').replace(/[<>]/g, (c) => (c === '<' ? '&lt;' : '&gt;'));
290
+ }
291
+
292
+ function editStyleProp(
293
+ s: MagicString,
294
+ opening: AnyNode,
295
+ prop: string,
296
+ value: string,
297
+ canvasAbsPath: string,
298
+ id: string
299
+ ): void {
300
+ const attr = findAttribute(opening, 'style');
301
+ if (!attr) {
302
+ // No style prop yet — insert one with a single key.
303
+ const insertAt: number | undefined = opening?.name?.end;
304
+ if (typeof insertAt !== 'number') {
305
+ throw new Error('Opening element has no resolvable name range');
306
+ }
307
+ s.appendLeft(insertAt, ` style={{ ${jsKey(prop)}: ${value} }}`);
308
+ return;
309
+ }
310
+ const v = attr.value;
311
+ if (!v || v.type !== 'JSXExpressionContainer') {
312
+ throw new CanvasEditError(
313
+ `style attribute on ${id} is not a {{...}} expression — refusing to edit`,
314
+ { canvas: canvasAbsPath, id }
315
+ );
316
+ }
317
+ const obj = v.expression;
318
+ if (!obj || obj.type !== 'ObjectExpression') {
319
+ throw new CanvasEditError(
320
+ `style={...} on ${id} is not an inline ObjectExpression — refusing to edit`,
321
+ { canvas: canvasAbsPath, id }
322
+ );
323
+ }
324
+
325
+ // Search for an existing property with the same key. JSX styles permit
326
+ // camelCase identifiers (paddingTop) AND quoted strings ("padding-top").
327
+ // Compare both forms.
328
+ const propCamel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
329
+ for (const p of obj.properties as AnyNode[]) {
330
+ if (p?.type !== 'Property' && p?.type !== 'ObjectProperty') continue;
331
+ const k = p.key;
332
+ if (!k) continue;
333
+ const kname = k.type === 'Identifier' ? k.name : k.type === 'Literal' ? String(k.value) : null;
334
+ if (kname === prop || kname === propCamel) {
335
+ s.overwrite(p.value.start, p.value.end, value);
336
+ return;
337
+ }
338
+ }
339
+
340
+ // Key missing — append before the closing `}`. ObjectExpression's last char
341
+ // is the `}`; magic-string appendLeft at end keeps the brace in place.
342
+ const props = obj.properties as AnyNode[];
343
+ const tail = props.length > 0 ? (props[props.length - 1].end as number) : obj.start + 1;
344
+ const sep = props.length > 0 ? ', ' : ' ';
345
+ // The object's textual end is `obj.end - 1` for `}` after the chars.
346
+ // appendLeft at obj.end -1 puts new text before the `}`.
347
+ s.appendLeft(obj.end - 1, `${sep}${jsKey(prop)}: ${value} `);
348
+ // Suppress unused-var lint without bypassing TS:
349
+ void tail;
350
+ }
351
+
352
+ /**
353
+ * Render a JS object key — bare identifier when the prop is camelCase + valid
354
+ * JS id, quoted otherwise. Mirrors how authors write JSX styles.
355
+ */
356
+ function jsKey(prop: string): string {
357
+ if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(prop)) return prop;
358
+ return JSON.stringify(prop);
359
+ }
360
+
361
+ // ---------------------------------------------------------------------------
362
+ // CLI entry. Invoked by bin/canvas-edit.sh — keeps Bun startup off the hot
363
+ // path of /design:edit when the orchestrator shells out.
364
+ //
365
+ // Layout: bun run canvas-edit.ts --invoke <canvas> <id> <attr> <value>
366
+
367
+ if (import.meta.main) {
368
+ const argv = process.argv.slice(2);
369
+ if (argv[0] === '--invoke' && argv.length === 5) {
370
+ const [, canvas, id, attr, value] = argv;
371
+ if (!canvas || !id || !attr || value === undefined) {
372
+ console.error('canvas-edit: --invoke needs <canvas> <id> <attr> <value>');
373
+ process.exit(2);
374
+ }
375
+ try {
376
+ const r = await editAttribute(canvas, id, attr, value);
377
+ console.log(JSON.stringify({ canvas, id, delta: r.delta }));
378
+ process.exit(0);
379
+ } catch (err) {
380
+ const msg = err instanceof Error ? err.message : String(err);
381
+ console.error(`canvas-edit: ${msg}`);
382
+ process.exit(2);
383
+ }
384
+ } else {
385
+ console.error('Usage: bun run canvas-edit.ts --invoke <canvas> <id> <attr> <value>');
386
+ process.exit(2);
387
+ }
388
+ }