@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,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, '"').replace(/[<>]/g, (c) => (c === '<' ? '<' : '>'));
|
|
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
|
+
}
|