@typecad/cuttlefish 1.0.0-alpha.12 → 1.0.0-alpha.14

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 (101) hide show
  1. package/assets/editor-extensions/typecad-debug/README.md +142 -0
  2. package/assets/editor-extensions/typecad-debug/out/extension.js +347 -0
  3. package/assets/editor-extensions/typecad-debug/out/extension.js.map +1 -0
  4. package/assets/editor-extensions/typecad-debug/package.json +76 -0
  5. package/assets/editor-extensions/typecad-ui/LICENSE +27 -0
  6. package/assets/editor-extensions/typecad-ui/README.md +52 -0
  7. package/assets/editor-extensions/typecad-ui/file-icon-dark.png +0 -0
  8. package/assets/editor-extensions/typecad-ui/file-icon-light.png +0 -0
  9. package/assets/editor-extensions/typecad-ui/icon.png +0 -0
  10. package/assets/editor-extensions/typecad-ui/language-configuration.json +43 -0
  11. package/assets/editor-extensions/typecad-ui/package.json +54 -0
  12. package/assets/editor-extensions/typecad-ui/snippets/typecad-ui.json +80 -0
  13. package/assets/editor-extensions/typecad-ui/syntaxes/markdown-ui.json +45 -0
  14. package/assets/editor-extensions/typecad-ui/syntaxes/typecad-ui.tmLanguage.json +1269 -0
  15. package/dist/add-preset.d.ts +4 -0
  16. package/dist/add-preset.js +74 -0
  17. package/dist/api/config.d.ts +8 -5
  18. package/dist/api/shared/display-adapters/sdl.js +1 -1
  19. package/dist/api/shared/display-profile.d.ts +11 -0
  20. package/dist/api/shared/display-profile.js +3 -0
  21. package/dist/api/shared/framework-manifest.d.ts +61 -504
  22. package/dist/api/shared/hal-op-ir.d.ts +19 -0
  23. package/dist/api/shared/toolchain-types.d.ts +0 -1
  24. package/dist/cli.js +31 -9
  25. package/dist/config-loader.d.ts +0 -2
  26. package/dist/config-loader.js +13 -6
  27. package/dist/config-schema.d.ts +17 -230
  28. package/dist/config-schema.js +1 -3
  29. package/dist/contract/contract-parser.d.ts +9 -127
  30. package/dist/create/board-checklist.js +1 -1
  31. package/dist/create/board-codegen.js +4 -4
  32. package/dist/create/board-spec.d.ts +74 -530
  33. package/dist/create/debug-artifacts.d.ts +20 -0
  34. package/dist/create/debug-artifacts.js +69 -0
  35. package/dist/create/editor-integration.d.ts +30 -0
  36. package/dist/create/editor-integration.js +218 -0
  37. package/dist/create/eslint-rules-template.d.ts +2 -2
  38. package/dist/create/framework-catalog.d.ts +8 -0
  39. package/dist/create/framework-catalog.js +28 -5
  40. package/dist/create/index.d.ts +9 -6
  41. package/dist/create/index.js +6 -4
  42. package/dist/create/{init-scaffold.d.ts → scaffold.d.ts} +5 -4
  43. package/dist/create/{init-scaffold.js → scaffold.js} +38 -2
  44. package/dist/create/templates.d.ts +29 -0
  45. package/dist/create/{init-templates.js → templates.js} +15 -2
  46. package/dist/create/wizard.d.ts +8 -0
  47. package/dist/create/{init-wizard.js → wizard.js} +11 -4
  48. package/dist/emit/compliance/rules.js +18 -4
  49. package/dist/emit/emitters/function-emitter-impl.js +7 -1
  50. package/dist/emit/emitters/line-appender.js +6 -0
  51. package/dist/emit/emitters/setup.js +22 -0
  52. package/dist/emit/emitters/ui-emitter.js +40 -15
  53. package/dist/emit/route-hal-op.js +55 -1
  54. package/dist/emit/statement-renderer.js +5 -2
  55. package/dist/ir/build-ir.js +46 -4
  56. package/dist/ir/expression-to-ir.js +17 -0
  57. package/dist/ir/hal/hal-emitter.js +23 -5
  58. package/dist/ir/hal/hal-plugins.js +11 -0
  59. package/dist/ir/pin-mode-validation.js +32 -9
  60. package/dist/ir/pin-state-tracking.d.ts +58 -0
  61. package/dist/ir/pin-state-tracking.js +182 -0
  62. package/dist/ir/program-analysis.d.ts +6 -0
  63. package/dist/ir/program-analysis.js +38 -0
  64. package/dist/ir/statement-to-ir.js +14 -0
  65. package/dist/ir/transformers/control-flow.js +29 -0
  66. package/dist/ir/transformers/ui-call-resolver.js +105 -1
  67. package/dist/ir/ui-element-auto-wire.js +7 -4
  68. package/dist/library/catalog.d.ts +35 -0
  69. package/dist/library/catalog.js +67 -0
  70. package/dist/library/cli.d.ts +2 -0
  71. package/dist/library/cli.js +168 -0
  72. package/dist/library/init.d.ts +25 -0
  73. package/dist/library/init.js +397 -0
  74. package/dist/library/install.d.ts +9 -0
  75. package/dist/library/install.js +80 -0
  76. package/dist/library/registry-search.d.ts +34 -0
  77. package/dist/library/registry-search.js +50 -0
  78. package/dist/library/validate.d.ts +17 -0
  79. package/dist/library/validate.js +175 -0
  80. package/dist/library-packages.d.ts +95 -0
  81. package/dist/library-packages.js +275 -0
  82. package/dist/orchestrator/graph-builder.d.ts +4 -1
  83. package/dist/orchestrator/graph-builder.js +23 -1
  84. package/dist/preview/api-shared-shim.d.ts +1 -0
  85. package/dist/preview/api-shared-shim.js +7 -0
  86. package/dist/preview/client.js +235 -2
  87. package/dist/preview/server.js +154 -62
  88. package/dist/testing.d.ts +2 -2
  89. package/dist/testing.js +1 -1
  90. package/dist/theme-tokens.d.ts +22 -0
  91. package/dist/theme-tokens.js +172 -0
  92. package/dist/transpile.js +90 -5
  93. package/dist/types.d.ts +24 -0
  94. package/dist/ui-hook.d.ts +7 -0
  95. package/dist/utils/cli.d.ts +2 -2
  96. package/dist/utils/cli.js +77 -3
  97. package/dist/utils/ui.d.ts +5 -0
  98. package/dist/utils/ui.js +7 -0
  99. package/package.json +18 -9
  100. package/dist/create/init-templates.d.ts +0 -28
  101. package/dist/create/init-wizard.d.ts +0 -8
@@ -1,55 +1,83 @@
1
1
  import fs from "node:fs";
2
2
  import http from "node:http";
3
3
  import path from "node:path";
4
- import { fileURLToPath } from "node:url";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { createRequire } from "node:module";
5
6
  import chalk from "chalk";
6
7
  import { findConfigFile, parseConfigFile } from "../config-loader.js";
7
- import { requireUIHook } from "../ui-hook.js";
8
- const HTML = `<!doctype html>
9
- <html lang="en">
10
- <head>
11
- <meta charset="utf-8">
12
- <meta name="viewport" content="width=device-width, initial-scale=1">
13
- <title>Cuttlefish Preview</title>
14
- <style>
15
- :root { color-scheme: dark; font-family: ui-sans-serif, system-ui, sans-serif; background: #111315; color: #e8ecef; }
16
- body { margin: 0; height: 100vh; overflow: hidden; display: grid; grid-template-columns: minmax(360px, 1fr) 280px; }
17
- main { display: grid; place-items: center; padding: 24px; background: #191d20; }
18
- canvas { image-rendering: pixelated; width: min(92vw, 960px); max-height: calc(100vh - 48px); aspect-ratio: var(--display-aspect, 4 / 3); background: #000; box-shadow: 0 12px 36px rgba(0,0,0,.35); }
19
- aside { border-left: 1px solid #2d3338; padding: 18px; display: flex; flex-direction: column; gap: 18px; min-height: 0; }
20
- aside > section:first-child, aside > section:nth-child(2) { flex-shrink: 0; } /* Display + GPIO stay visible */
21
- aside > section:last-child { min-height: 0; display: flex; flex-direction: column; }
22
- #diagnostics { overflow-y: auto; min-height: 0; }
23
- h1 { font-size: 15px; margin: 0 0 8px; font-weight: 650; }
24
- #status, #diagnostics, .empty { color: #aab3ba; font-size: 12px; line-height: 1.4; }
25
- #pins { display: grid; gap: 8px; }
26
- button { appearance: none; border: 1px solid #44505a; background: #252b30; color: #f3f6f8; border-radius: 6px; padding: 9px 10px; text-align: left; font: inherit; cursor: pointer; }
27
- button:hover { background: #303841; }
28
- @media (max-width: 760px) {
29
- body { height: auto; min-height: 100vh; overflow: auto; grid-template-rows: auto 1fr; grid-template-columns: 1fr; }
30
- aside { border-left: 0; border-top: 1px solid #2d3338; }
31
- canvas { width: min(94vw, 640px); }
32
- }
33
- </style>
34
- </head>
35
- <body>
36
- <main><canvas id="display" width="320" height="240"></canvas></main>
37
- <aside>
38
- <section>
39
- <h1>Display</h1>
40
- <div id="status"></div>
41
- </section>
42
- <section>
43
- <h1>GPIO</h1>
44
- <div id="pins"></div>
45
- </section>
46
- <section>
47
- <h1>Diagnostics</h1>
48
- <div id="diagnostics"></div>
49
- </section>
50
- </aside>
51
- <script type="module" src="/__cuttlefish/preview/client.js"></script>
52
- </body>
8
+ import { requireUIHook, hasUIHook } from "../ui-hook.js";
9
+ import { loadUIEngine } from "../ui/ui-bridge.js";
10
+ const HTML = `<!doctype html>
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="utf-8">
14
+ <meta name="viewport" content="width=device-width, initial-scale=1">
15
+ <title>Cuttlefish Preview</title>
16
+ <style>
17
+ :root { color-scheme: dark; font-family: ui-sans-serif, system-ui, sans-serif; background: #111315; color: #e8ecef; }
18
+ body { margin: 0; height: 100vh; overflow: hidden; display: grid; grid-template-columns: minmax(360px, 1fr) 280px; }
19
+ main { display: grid; place-items: center; padding: 24px; background: #191d20; }
20
+ canvas { image-rendering: pixelated; width: min(92vw, 960px); max-height: calc(100vh - 48px); aspect-ratio: var(--display-aspect, 4 / 3); background: #000; box-shadow: 0 12px 36px rgba(0,0,0,.35); }
21
+ #debugOverlay { position: absolute; pointer-events: none; image-rendering: pixelated; background: transparent; }
22
+ main { position: relative; }
23
+ #debugModes { display: grid; gap: 2px; }
24
+ #debugModes label { display: flex; align-items: center; gap: 8px; color: #aab3ba; font-size: 12px; cursor: pointer; padding: 5px 6px; border-radius: 5px; }
25
+ #debugModes label:hover { background: #232a30; color: #dfe6eb; }
26
+ #debugModes input[type="checkbox"] { width: 14px; height: 14px; margin: 0; }
27
+ #debugState { color: #7d8790; font-size: 11px; margin-top: 6px; }
28
+ aside { border-left: 1px solid #2d3338; padding: 18px; display: flex; flex-direction: column; gap: 18px; min-height: 0; }
29
+ aside > section:first-child, aside > section:nth-child(2) { flex-shrink: 0; } /* Display + GPIO stay visible */
30
+ aside > section:last-child { min-height: 0; display: flex; flex-direction: column; }
31
+ #diagnostics { overflow-y: auto; min-height: 0; }
32
+ h1 { font-size: 15px; margin: 0 0 8px; font-weight: 650; }
33
+ #status, #diagnostics, .empty { color: #aab3ba; font-size: 12px; line-height: 1.4; }
34
+ #pins { display: grid; gap: 8px; }
35
+ button { appearance: none; border: 1px solid #44505a; background: #252b30; color: #f3f6f8; border-radius: 6px; padding: 9px 10px; text-align: left; font: inherit; cursor: pointer; }
36
+ button:hover { background: #303841; }
37
+ @media (max-width: 760px) {
38
+ body { height: auto; min-height: 100vh; overflow: auto; grid-template-rows: auto 1fr; grid-template-columns: 1fr; }
39
+ aside { border-left: 0; border-top: 1px solid #2d3338; }
40
+ canvas { width: min(94vw, 640px); }
41
+ }
42
+ </style>
43
+ <script type="importmap">
44
+ {
45
+ "imports": {
46
+ "@typecad/ui/": "/__cuttlefish-ui/",
47
+ "@typecad/cuttlefish/api/shared": "/__cuttlefish/preview/api-shared-shim.js"
48
+ }
49
+ }
50
+ </script>
51
+ </head>
52
+ <body>
53
+ <main><canvas id="display" width="320" height="240"></canvas></main>
54
+ <aside>
55
+ <section>
56
+ <h1>Display</h1>
57
+ <div id="status"></div>
58
+ </section>
59
+ <section>
60
+ <h1>GPIO</h1>
61
+ <div id="pins"></div>
62
+ </section>
63
+ <section>
64
+ <h1>Debug overlay</h1>
65
+ <div id="debugModes">
66
+ <label><input type="checkbox" data-mode="boxes"> boxes (colors by kind)</label>
67
+ <label><input type="checkbox" data-mode="clips"> scroll clips (dashed)</label>
68
+ <label><input type="checkbox" data-mode="dirty"> dirty flash (repaints)</label>
69
+ <label><input type="checkbox" data-mode="inspect"> inspect taps (blocks input)</label>
70
+ </div>
71
+ <div id="debugState">overlay: off — check a box or press D</div>
72
+ <div style="color:#7d8790;font-size:11px;margin-top:4px">D cycles boxes/clips/dirty · Ctrl-click always inspects</div>
73
+ </section>
74
+ <section>
75
+ <h1>Diagnostics</h1>
76
+ <div id="diagnostics"></div>
77
+ </section>
78
+ </aside>
79
+ <script type="module" src="/__cuttlefish/preview/client.js"></script>
80
+ </body>
53
81
  </html>`;
54
82
  function contentType(filePath) {
55
83
  if (filePath.endsWith(".js"))
@@ -60,6 +88,42 @@ function contentType(filePath) {
60
88
  return "application/json; charset=utf-8";
61
89
  return "text/plain; charset=utf-8";
62
90
  }
91
+ /** Serve a file from a dist root, with ESM-specifier fallbacks: extensionless
92
+ * paths try +".js" (package-exports style "./preview/host-ui-runtime" →
93
+ * host-ui-runtime.js) and bare directories try +"/index.js". */
94
+ function serveFromRoot(res, root, relRaw) {
95
+ const rel = decodeURIComponent(relRaw);
96
+ const candidates = [rel, `${rel}.js`, `${rel}/index.js`];
97
+ for (const candidate of candidates) {
98
+ const filePath = path.resolve(root, candidate);
99
+ if (!filePath.startsWith(root))
100
+ break; // path traversal — 404 below
101
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
102
+ // no-store: these are compiled dist files that change between builds;
103
+ // heuristic browser caching serves stale modules after a rebuild and
104
+ // breaks the preview dev loop (and any debugging of it).
105
+ res.writeHead(200, { "content-type": contentType(filePath), "cache-control": "no-store" });
106
+ fs.createReadStream(filePath).pipe(res);
107
+ return;
108
+ }
109
+ }
110
+ writeText(res, 404, "Not found");
111
+ }
112
+ /** Resolve a dependency package's dist directory (served to the browser).
113
+ * Resolves through the package's exports map (subpath → dist file) and walks
114
+ * up to the enclosing "dist" — packages don't export "./package.json". */
115
+ function packageDistDir(specifier) {
116
+ const require = createRequire(import.meta.url);
117
+ const entry = require.resolve(specifier); // e.g. .../dist/engine.js
118
+ let dir = path.dirname(entry);
119
+ for (let i = 0; i < 4 && path.basename(dir) !== "dist"; i++) {
120
+ dir = path.dirname(dir);
121
+ }
122
+ if (path.basename(dir) !== "dist") {
123
+ throw new Error(`Could not locate the dist directory for ${specifier} (resolved ${entry}).`);
124
+ }
125
+ return dir;
126
+ }
63
127
  function writeText(res, status, text, type = "text/plain; charset=utf-8") {
64
128
  res.writeHead(status, { "content-type": type });
65
129
  res.end(text);
@@ -127,13 +191,28 @@ async function listen(server, preferredPort) {
127
191
  throw new Error(`No available preview port found starting at ${preferredPort}.`);
128
192
  }
129
193
  export async function runPreviewServer(options = {}) {
194
+ // The preview pipeline drives the UI engine directly (snapshot builds,
195
+ // type-decl generation) without going through transpileFile(), which is the
196
+ // only path that lazily registers the hook — so load it here first. Preview
197
+ // is a UI feature: when the engine is absent, fail with a clear message
198
+ // instead of the generic "hook is not registered" error.
199
+ await loadUIEngine();
200
+ if (!hasUIHook()) {
201
+ throw new Error(`cuttlefish preview requires the @typecad/ui package — install it in this project (npm install @typecad/ui).`);
202
+ }
130
203
  const configPath = resolveConfigPath(options.configPath);
131
204
  const config = parseConfigFile(configPath);
132
205
  if (!config)
133
206
  throw new Error(`Could not parse ${configPath}`);
207
+ // Snapshots re-read the config on every build (see /snapshot.json below);
208
+ // this holds the most recent config that parsed, for fallback mid-edit.
209
+ let lastGoodConfig = config;
134
210
  const projectRoot = path.dirname(configPath);
135
211
  requireUIHook().generateProjectUITypeDeclarations(projectRoot);
136
212
  const distRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
213
+ // The browser client imports the host runtime from @typecad/ui via the
214
+ // import map — resolve where that package's dist lives on this machine.
215
+ const uiDistRoot = packageDistDir("@typecad/ui/engine");
137
216
  const clients = new Set();
138
217
  const server = http.createServer(async (req, res) => {
139
218
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -144,13 +223,26 @@ export async function runPreviewServer(options = {}) {
144
223
  }
145
224
  if (url.pathname === "/snapshot.json") {
146
225
  requireUIHook().generateProjectUITypeDeclarations(projectRoot);
147
- // Routed through a variable (not a string literal) so tsc types this
148
- // as `any` and never resolves @typecad/ui's declaration files see
149
- // the comment in ui/ui-bridge.ts for why a literal specifier here
150
- // causes TS5055 on rebuilds where dist/ already exists.
151
- const buildProgramPath = "@typecad/ui/preview/build-program";
152
- const { buildPreviewSnapshot } = await import(buildProgramPath);
153
- const snapshot = await buildPreviewSnapshot({ config, projectRoot });
226
+ // Re-read the config for every snapshot build so edits (themeClass,
227
+ // entry, display) take effect on page refresh without restarting the
228
+ // preview server. A config that momentarily fails to parse (mid-edit)
229
+ // falls back to the last good one — the page keeps working; refresh
230
+ // again once the edit settles.
231
+ const latest = parseConfigFile(configPath) ?? lastGoodConfig;
232
+ lastGoodConfig = latest;
233
+ // Imported via a computed file:// URL (never a string-literal bare
234
+ // specifier) so tsc types this as `any` and never resolves
235
+ // @typecad/ui's declaration files — see the comment in ui/ui-bridge.ts
236
+ // for why a literal specifier causes TS5055 on rebuilds where dist/
237
+ // already exists. The ?t= cache-bust forces a fresh ESM load per
238
+ // snapshot build: without it the server caches the module graph from
239
+ // startup, and engine rebuilds (font planning, layout fixes, ...)
240
+ // never take effect until the server restarts — a recurring source of
241
+ // "fixed but the preview still shows it" confusion. Dev-only cost:
242
+ // re-evaluating the module graph per snapshot request.
243
+ const buildProgramUrl = pathToFileURL(path.join(uiDistRoot, "preview", "build-program.js")).href;
244
+ const { buildPreviewSnapshot } = await import(buildProgramUrl + "?t=" + Date.now());
245
+ const snapshot = await buildPreviewSnapshot({ config: latest, projectRoot });
154
246
  writeText(res, 200, JSON.stringify(snapshot), "application/json; charset=utf-8");
155
247
  return;
156
248
  }
@@ -165,15 +257,15 @@ export async function runPreviewServer(options = {}) {
165
257
  req.on("close", () => clients.delete(res));
166
258
  return;
167
259
  }
260
+ if (url.pathname.startsWith("/__cuttlefish-ui/")) {
261
+ // The @typecad/ui package's dist — the browser client resolves
262
+ // "@typecad/ui/..." bare specifiers onto this prefix via the import
263
+ // map (host runtime, gfx, and their relative engine imports).
264
+ serveFromRoot(res, uiDistRoot, url.pathname.slice("/__cuttlefish-ui/".length));
265
+ return;
266
+ }
168
267
  if (url.pathname.startsWith("/__cuttlefish/")) {
169
- const rel = decodeURIComponent(url.pathname.slice("/__cuttlefish/".length));
170
- const filePath = path.resolve(distRoot, rel);
171
- if (!filePath.startsWith(distRoot) || !fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
172
- writeText(res, 404, "Not found");
173
- return;
174
- }
175
- res.writeHead(200, { "content-type": contentType(filePath) });
176
- fs.createReadStream(filePath).pipe(res);
268
+ serveFromRoot(res, distRoot, url.pathname.slice("/__cuttlefish/".length));
177
269
  return;
178
270
  }
179
271
  writeText(res, 404, "Not found");
package/dist/testing.d.ts CHANGED
@@ -19,8 +19,8 @@ export { transpileFile } from "./transpile.js";
19
19
  export { resetUIEngine, __simulateUIAbsentForTest } from "./ui/ui-bridge.js";
20
20
  export { resetSafetyEngine, __simulateSafetyAbsentForTest } from "./safety/safety-bridge.js";
21
21
  export { setSafetyHook, hasSafetyHook } from "./safety-hook.js";
22
- export { scaffoldProject, normalizeProjectName, KNOWN_BOARDS, generateProjectPackageJson, generateProjectTsconfig, generateProjectConfig, generateProjectEnvDts, generateStarterSketch, generateStarterTest, generateStarterSim, generateGitignore, generateEslintConfig, runInitWizard, } from "./create/index.js";
23
- export type { InitProjectOptions } from "./create/index.js";
22
+ export { scaffoldProject, normalizeProjectName, KNOWN_BOARDS, generateProjectPackageJson, generateProjectTsconfig, generateProjectConfig, generateProjectEnvDts, generateStarterSketch, generateStarterTest, generateStarterSim, generateGitignore, generateEslintConfig, runCreateWizard, } from "./create/index.js";
23
+ export type { CreateProjectOptions } from "./create/index.js";
24
24
  export { scaffoldBoardPackages, parseBoardSpec, safeParseBoardSpec, stripJsonc, } from "./create/index.js";
25
25
  export type { BoardSpec, ScaffoldBoardResult } from "./create/index.js";
26
26
  export { BoardGenerators } from "./create/index.js";
package/dist/testing.js CHANGED
@@ -23,7 +23,7 @@ export { resetUIEngine, __simulateUIAbsentForTest } from "./ui/ui-bridge.js";
23
23
  export { resetSafetyEngine, __simulateSafetyAbsentForTest } from "./safety/safety-bridge.js";
24
24
  export { setSafetyHook, hasSafetyHook } from "./safety-hook.js";
25
25
  // ── Project scaffolding ──────────────────────────────────────────────────────
26
- export { scaffoldProject, normalizeProjectName, KNOWN_BOARDS, generateProjectPackageJson, generateProjectTsconfig, generateProjectConfig, generateProjectEnvDts, generateStarterSketch, generateStarterTest, generateStarterSim, generateGitignore, generateEslintConfig, runInitWizard, } from "./create/index.js";
26
+ export { scaffoldProject, normalizeProjectName, KNOWN_BOARDS, generateProjectPackageJson, generateProjectTsconfig, generateProjectConfig, generateProjectEnvDts, generateStarterSketch, generateStarterTest, generateStarterSim, generateGitignore, generateEslintConfig, runCreateWizard, } from "./create/index.js";
27
27
  // ── Board codegen (`cuttlefish board add`) ───────────────────────────────────
28
28
  export { scaffoldBoardPackages, parseBoardSpec, safeParseBoardSpec, stripJsonc, } from "./create/index.js";
29
29
  export { BoardGenerators } from "./create/index.js";
@@ -0,0 +1,22 @@
1
+ /** One declaration (`--token: value`), order-preserving. */
2
+ type Decl = [name: string, value: string];
3
+ export interface ShadcnTheme {
4
+ name: string;
5
+ light: Decl[];
6
+ dark: Decl[];
7
+ }
8
+ /** Where a project keeps its own themes: src/styles/themes/*.css next to
9
+ * the kit stylesheet. Project themes win over same-named package ones —
10
+ * pasting a theme is dropping a file into YOUR project, never node_modules. */
11
+ export declare function projectThemesDir(projectRoot: string): string;
12
+ /** Theme names, project-local (src/styles/themes/) first, then the ones
13
+ * shipped in the package (assets/shadcn/themes/). */
14
+ export declare function listShadcnThemes(projectRoot?: string): string[];
15
+ /** Load a theme by name — project-local directory first, then the package
16
+ * included set. Throws with the available names when missing. */
17
+ export declare function loadShadcnTheme(name: string, projectRoot?: string): ShadcnTheme;
18
+ /** Merge a theme's tokens into a kit stylesheet's :root/.dark blocks and
19
+ * return the rewritten text. Throws when a merged block is missing any
20
+ * token the kit's recipes reference. */
21
+ export declare function applyShadcnTheme(cssText: string, theme: ShadcnTheme): string;
22
+ export {};
@@ -0,0 +1,172 @@
1
+ // ---------------------------------------------------------------------------
2
+ // shadcn kit themes — the included token sets under assets/shadcn/themes/.
3
+ //
4
+ // A theme file is two CSS blocks (:root = light tokens, .dark = dark tokens)
5
+ // plus an optional header comment. Selecting a theme MERGES each block into
6
+ // the kit's stylesheet: declarations the theme defines override the kit's,
7
+ // and kit-specific extras the theme lacks (notably --destructive-background,
8
+ // which stock shadcn themes don't carry) keep their current values — so any
9
+ // stock theme from ui.shadcn.com or tweakcn pastes cleanly.
10
+ //
11
+ // Used by `cuttlefish add shadcn --theme <name>` (scaffold time) and
12
+ // `cuttlefish theme <name>` (swap an existing project's kit stylesheet).
13
+ // ---------------------------------------------------------------------------
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ const THEMES_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../assets/shadcn/themes");
18
+ /** The tokens the kit's recipes reference; every merged theme must cover them. */
19
+ const REQUIRED_TOKENS = [
20
+ "--background", "--foreground",
21
+ "--card", "--card-foreground",
22
+ "--primary", "--primary-foreground",
23
+ "--secondary", "--secondary-foreground",
24
+ "--muted", "--muted-foreground",
25
+ "--accent", "--accent-foreground",
26
+ "--destructive", "--destructive-foreground", "--destructive-background",
27
+ "--border", "--input", "--radius",
28
+ ];
29
+ /** Blank out /* … *​/ comment interiors (same length) so selector searches
30
+ * can't match inside a comment while byte offsets stay valid against the
31
+ * original text. The kit's header comment literally contains `.dark { ... }`
32
+ * as documentation — an unmasked search spliced tokens into the comment and
33
+ * left the real block untouched. */
34
+ function maskComments(css) {
35
+ return css.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length));
36
+ }
37
+ /** Locate `selector {` in css, skipping comment spans. Custom-property values
38
+ * contain no braces, so the block ends at the first `}` after the opening
39
+ * one. */
40
+ function blockRange(css, selector) {
41
+ const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
42
+ const m = new RegExp(escaped + "\\s*\\{").exec(maskComments(css));
43
+ if (!m)
44
+ return null;
45
+ const open = m.index + m[0].length;
46
+ const close = css.indexOf("}", open);
47
+ if (close < 0)
48
+ return null;
49
+ return { selStart: m.index, open, close };
50
+ }
51
+ /** Parse a token block's declarations (line-oriented; one decl per line, the
52
+ * format both the kit and stock shadcn themes use). */
53
+ function parseDecls(block) {
54
+ const out = [];
55
+ for (const line of block.split("\n")) {
56
+ const m = /^\s*(--[\w-]+)\s*:\s*([^;]+);/.exec(line);
57
+ if (m)
58
+ out.push([m[1], m[2].trim()]);
59
+ }
60
+ return out;
61
+ }
62
+ function emitBlock(selector, decls) {
63
+ const body = decls.map(([k, v]) => ` ${k}: ${v};`).join("\n");
64
+ return `${selector} {\n${body}\n}`;
65
+ }
66
+ /** Order-preserving override merge: base order, overrides in place, theme-only
67
+ * declarations appended. */
68
+ function mergeDecls(base, override) {
69
+ const out = base.map(([k, v]) => [k, v]);
70
+ const indexByName = new Map(out.map(([k], i) => [k, i]));
71
+ for (const [k, v] of override) {
72
+ const existing = indexByName.get(k);
73
+ if (existing !== undefined)
74
+ out[existing][1] = v;
75
+ else {
76
+ indexByName.set(k, out.length);
77
+ out.push([k, v]);
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+ /** Where a project keeps its own themes: src/styles/themes/*.css next to
83
+ * the kit stylesheet. Project themes win over same-named package ones —
84
+ * pasting a theme is dropping a file into YOUR project, never node_modules. */
85
+ export function projectThemesDir(projectRoot) {
86
+ return path.resolve(projectRoot, "src/styles/themes");
87
+ }
88
+ function themeNamesIn(dir) {
89
+ if (!fs.existsSync(dir))
90
+ return [];
91
+ return fs.readdirSync(dir)
92
+ .filter((f) => f.endsWith(".css") && !f.startsWith("_"))
93
+ .map((f) => f.replace(/\.css$/, ""))
94
+ .sort();
95
+ }
96
+ /** Theme names, project-local (src/styles/themes/) first, then the ones
97
+ * shipped in the package (assets/shadcn/themes/). */
98
+ export function listShadcnThemes(projectRoot) {
99
+ const local = projectRoot ? themeNamesIn(projectThemesDir(projectRoot)) : [];
100
+ const included = themeNamesIn(THEMES_DIR);
101
+ return [...new Set([...local, ...included])].sort();
102
+ }
103
+ function formatThemeList(projectRoot) {
104
+ const local = projectRoot ? themeNamesIn(projectThemesDir(projectRoot)) : [];
105
+ const included = themeNamesIn(THEMES_DIR);
106
+ const lines = [];
107
+ if (local.length > 0) {
108
+ lines.push("project (src/styles/themes/):");
109
+ for (const n of local)
110
+ lines.push(` ${n}`);
111
+ }
112
+ lines.push("included with cuttlefish:");
113
+ for (const n of included)
114
+ lines.push(` ${n}`);
115
+ lines.push("(paste your own: src/styles/themes/<name>.css in this project)");
116
+ return lines.join("\n");
117
+ }
118
+ /** Load a theme by name — project-local directory first, then the package
119
+ * included set. Throws with the available names when missing. */
120
+ export function loadShadcnTheme(name, projectRoot) {
121
+ if (!/^[a-z0-9-]+$/.test(name)) {
122
+ throw new Error(`Invalid theme name "${name}" — use letters, digits, and dashes.`);
123
+ }
124
+ const candidates = projectRoot
125
+ ? [path.join(projectThemesDir(projectRoot), `${name}.css`), path.join(THEMES_DIR, `${name}.css`)]
126
+ : [path.join(THEMES_DIR, `${name}.css`)];
127
+ const file = candidates.find((c) => fs.existsSync(c));
128
+ if (!file) {
129
+ throw new Error(`Unknown theme "${name}". Available themes:\n${formatThemeList(projectRoot)}`);
130
+ }
131
+ const text = fs.readFileSync(file, "utf-8");
132
+ const rootRange = blockRange(text, ":root");
133
+ const darkRange = blockRange(text, ".dark");
134
+ if (!rootRange || !darkRange) {
135
+ throw new Error(`Theme "${name}" must define both a :root and a .dark token block.`);
136
+ }
137
+ const light = parseDecls(text.slice(rootRange.open, rootRange.close));
138
+ const dark = parseDecls(text.slice(darkRange.open, darkRange.close));
139
+ if (light.length === 0 || dark.length === 0) {
140
+ throw new Error(`Theme "${name}" has empty token blocks — paste the theme's :root and .dark declarations.`);
141
+ }
142
+ return { name, light, dark };
143
+ }
144
+ /** Merge a theme's tokens into a kit stylesheet's :root/.dark blocks and
145
+ * return the rewritten text. Throws when a merged block is missing any
146
+ * token the kit's recipes reference. */
147
+ export function applyShadcnTheme(cssText, theme) {
148
+ let out = cssText;
149
+ for (const [selector, themeDecls] of [[".dark", theme.dark], [":root", theme.light]]) {
150
+ const range = blockRange(out, selector);
151
+ if (!range) {
152
+ throw new Error(`The kit stylesheet has no ${selector} token block to replace.`);
153
+ }
154
+ const merged = mergeDecls(parseDecls(out.slice(range.open, range.close)), themeDecls);
155
+ out = out.slice(0, range.selStart) + emitBlock(selector, merged) + out.slice(range.close + 1);
156
+ }
157
+ // Validate AFTER both merges (a token can live in either block).
158
+ const rootRange = blockRange(out, ":root");
159
+ const darkRange = blockRange(out, ".dark");
160
+ const names = new Set();
161
+ if (rootRange)
162
+ for (const [k] of parseDecls(out.slice(rootRange.open, rootRange.close)))
163
+ names.add(k);
164
+ if (darkRange)
165
+ for (const [k] of parseDecls(out.slice(darkRange.open, darkRange.close)))
166
+ names.add(k);
167
+ const missing = REQUIRED_TOKENS.filter((t) => !names.has(t));
168
+ if (missing.length > 0) {
169
+ throw new Error(`Theme "${theme.name}" leaves required tokens unset: ${missing.join(", ")}`);
170
+ }
171
+ return out;
172
+ }
package/dist/transpile.js CHANGED
@@ -26,12 +26,14 @@ import { requireUIHook, hasUIHook } from "./ui-hook.js";
26
26
  import { loadUIEngine } from "./ui/ui-bridge.js";
27
27
  import { hasSafetyHook, requireSafetyHook } from "./safety-hook.js";
28
28
  import { loadSafetyEngine } from "./safety/safety-bridge.js";
29
- import { setDisplayProfile, resetDisplayProfile } from "./stores/display-profile-store.js";
29
+ import { setDisplayProfile, resetDisplayProfile, getDisplayProfile } from "./stores/display-profile-store.js";
30
30
  import { setThemeCss, resetThemeCss, setThemeClass } from "./stores/theme-store.js";
31
31
  import { emitCpp, registerAllEnumNames } from "./emit/cpp-emitter.js";
32
32
  import { readText, writeText, resetWrittenFiles, wasWrittenThisRun } from "./utils/fs.js";
33
33
  import { debug as logDebug, info } from "./utils/logger.js";
34
+ import { printDebugStrategy } from "./utils/ui.js";
34
35
  import { loadLibraryDefinitions, generateLibdefStubs } from "./libdef/registry.js";
36
+ import { resetCuttlefishLibraries, validateCuttlefishLibraries, cuttlefishLibraryLibdefs, libraryDefinitionKey, writeCuttlefishLibraryArtifacts, } from "./library-packages.js";
35
37
  import { buildCallGraph } from "./ir/call-graph.js";
36
38
  import { clearCaches, } from "./cache.js";
37
39
  import { detectEntryPoints, detectExportedEntryPoints } from "./ir/entry-points.js";
@@ -201,6 +203,15 @@ import { loadFrameworkPackage } from "./framework-package.js";
201
203
  export { loadFrameworkPackage };
202
204
  export { getLoadedFramework, hasLoadedFramework } from "./framework-registry.js";
203
205
  import { getLoadedFramework, hasLoadedFramework } from "./framework-registry.js";
206
+ /** Under --strict-css, UI CSS-compatibility warnings (code css-*: ignored
207
+ * alpha, quantized font sizes, unsupported display/position values, ...) are
208
+ * upgraded to errors so the build fails instead of approximating silently. */
209
+ function upgradeStrictCss(d, strict) {
210
+ if (strict && d.severity === "warning" && typeof d.code === "string" && d.code.startsWith("css-")) {
211
+ return { ...d, severity: "error" };
212
+ }
213
+ return d;
214
+ }
204
215
  function formatFatalDiagnostics(entries) {
205
216
  const errors = entries.filter(({ diagnostic }) => diagnostic.severity === "error");
206
217
  const lines = [
@@ -260,7 +271,11 @@ function loadPlatformStrategy(frameworkPackage, _boardPackage, fromDir, debug) {
260
271
  if (hasLoadedFramework()) {
261
272
  const { strategy } = getLoadedFramework();
262
273
  if (debug) {
263
- logDebug(`Loaded FrameworkStrategy from ${frameworkPackage}`, true);
274
+ // Styled like the other step lines (cyan ⇉) says what the debug
275
+ // build is actually doing, in user terms. Falls back to the package
276
+ // name when the strategy carries no id.
277
+ const frameworkName = strategy.id || frameworkPackage.replace(/^@typecad\/framework-/, "");
278
+ printDebugStrategy(frameworkName);
264
279
  }
265
280
  return strategy;
266
281
  }
@@ -351,8 +366,26 @@ export async function transpileFile(options) {
351
366
  // see incremental-cache.ts — so we always transpile the full graph.)
352
367
  cleanOutput(entryDir, outDir);
353
368
  profiler.startTimer("graph:collect");
354
- const graphResult = collectTranspileGraph(entryFile, options.boardPackage);
369
+ // Image-conversion cap: never decode larger than the physical panel —
370
+ // converted <img> assets downscale to fit (no 24MB C arrays from photos).
371
+ const imageDecodeMax = await (async () => {
372
+ try {
373
+ const { effectiveDisplaySize } = await import("./api/shared/display-profile.js");
374
+ const size = effectiveDisplaySize(getDisplayProfile());
375
+ return { maxW: size.width, maxH: size.height };
376
+ }
377
+ catch {
378
+ return {};
379
+ }
380
+ })();
381
+ resetCuttlefishLibraries();
382
+ const graphResult = await collectTranspileGraph(entryFile, options.boardPackage, imageDecodeMax);
355
383
  profiler.endTimer("graph:collect");
384
+ // Cuttlefish library packages registered during the graph walk — validate
385
+ // them against the loaded framework (and build target, when known) before
386
+ // any codegen. A framework/target mismatch is a hard error here, far
387
+ // clearer than the native compiler's take on a missing header or node.
388
+ validateCuttlefishLibraries(strategy.id, options.platformContext?.frameworkData?.buildTarget);
356
389
  const transpileFiles = graphResult.files;
357
390
  // ── Type-check all files before transpiling ────────────────────────────────
358
391
  // Skip type-checking if explicitly disabled
@@ -425,6 +458,15 @@ export async function transpileFile(options) {
425
458
  }
426
459
  const npmPackages = graphResult.npmPackages;
427
460
  const definitions = loadLibraryDefinitions(sourceDir);
461
+ // Cuttlefish library packages: the import resolves to the library's shim
462
+ // include (e.g. '"__tc_rgbled.h"') instead of a transpiled module header.
463
+ // Project-local .libdef.json files keep precedence.
464
+ for (const libdef of cuttlefishLibraryLibdefs()) {
465
+ const key = libraryDefinitionKey(libdef.module);
466
+ if (!definitions.has(key)) {
467
+ definitions.set(key, libdef);
468
+ }
469
+ }
428
470
  let entryOutputs;
429
471
  const diagnostics = [];
430
472
  const allRemovedSymbols = [];
@@ -449,10 +491,12 @@ export async function transpileFile(options) {
449
491
  // parser warnings (unknown CSS properties, unknown HTML tags) here so the
450
492
  // author sees typos and unsupported features instead of silent drops.
451
493
  // Guarded: @typecad/ui is optional, so there may be no UI engine loaded.
494
+ // --strict-css upgrades the css-* compatibility warnings to errors.
452
495
  if (hasUIHook()) {
453
496
  for (const mod of requireUIHook().allUIModules()) {
454
497
  for (const d of mod.diagnostics) {
455
- diagnostics.push({ ...d, filePath: d.filePath ?? path.basename(mod.htmlPath) });
498
+ const upgraded = upgradeStrictCss(d, options.strictCss);
499
+ diagnostics.push({ ...upgraded, filePath: d.filePath ?? path.basename(mod.htmlPath) });
456
500
  }
457
501
  }
458
502
  }
@@ -582,10 +626,12 @@ export async function transpileFile(options) {
582
626
  profiler.endTimer("ir:build-all");
583
627
  // ── UI mount-time warnings (scroll memory budget, etc.) ─────────────────
584
628
  // Guarded: @typecad/ui is optional; no engine means no UI modules.
629
+ // --strict-css upgrades the css-* compatibility warnings to errors.
585
630
  if (hasUIHook()) {
586
631
  for (const mod of requireUIHook().allUIModules()) {
587
632
  for (const d of mod.mountDiagnostics) {
588
- diagnostics.push({ ...d, filePath: d.filePath ?? path.basename(mod.htmlPath) });
633
+ const upgraded = upgradeStrictCss(d, options.strictCss);
634
+ diagnostics.push({ ...upgraded, filePath: d.filePath ?? path.basename(mod.htmlPath) });
589
635
  }
590
636
  }
591
637
  }
@@ -905,6 +951,45 @@ export async function transpileFile(options) {
905
951
  });
906
952
  }
907
953
  }
954
+ // ── Persist the resolved board constants next to the emitted source ─────
955
+ // Framework toolchains re-read this at compile time to rebuild their chip
956
+ // descriptors (framework-zephyr's resolveChipFromBoard). Board packages
957
+ // carry chip data — controller splits, ADC channel maps, PWM specs — that
958
+ // lives only in the board package, so without this file the toolchain's
959
+ // registry fallback silently resolves board-derived targets (rpi_pico,
960
+ // esp32c3/c6, blackpill) to the XIAO default descriptor.
961
+ try {
962
+ // Read from the built program IRs — the IR build context (and its
963
+ // current-board-constants slot) is already closed at this point. Any
964
+ // file's IR may carry the constants (the board import is traversed while
965
+ // building whichever file imports it first), so scan for a populated one.
966
+ let boardConstants;
967
+ for (const pb of preBuilt.values()) {
968
+ const bc = pb.programIR.boardConstants;
969
+ if (bc && bc.size > 0) {
970
+ boardConstants = bc;
971
+ break;
972
+ }
973
+ }
974
+ if (boardConstants && boardConstants.size > 0) {
975
+ fs.writeFileSync(path.join(outDir, "board-constants.json"), JSON.stringify(Object.fromEntries(boardConstants), null, 2));
976
+ }
977
+ }
978
+ catch {
979
+ // Best-effort persistence; the toolchain falls back to its registry.
980
+ }
981
+ // ── Cuttlefish library packages: shims + libraries.json sidecar ─────────
982
+ // Shims are written next to the emitted sources (the framework scaffold's
983
+ // CMake/sketch regen compiles them); the sidecar records the used
984
+ // libraries' build contributions (Kconfig lines, overlay fragments) for
985
+ // the framework toolchain — the board-constants.json convention.
986
+ try {
987
+ const emittedSrcDir = entryOutputs ? path.dirname(entryOutputs.sourcePath) : outDir;
988
+ writeCuttlefishLibraryArtifacts(outDir, emittedSrcDir);
989
+ }
990
+ catch {
991
+ // Best-effort; a missing shim surfaces at native compile time.
992
+ }
908
993
  return {
909
994
  ...entryOutputs,
910
995
  diagnostics,