@typecad/cuttlefish 1.0.0-alpha.13 → 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 (64) 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/api/config.d.ts +8 -1
  16. package/dist/api/shared/framework-manifest.d.ts +61 -504
  17. package/dist/cli.js +17 -6
  18. package/dist/config-loader.js +3 -1
  19. package/dist/config-schema.d.ts +17 -223
  20. package/dist/config-schema.js +1 -0
  21. package/dist/contract/contract-parser.d.ts +9 -127
  22. package/dist/create/board-checklist.js +1 -1
  23. package/dist/create/board-codegen.js +4 -4
  24. package/dist/create/board-spec.d.ts +74 -530
  25. package/dist/create/editor-integration.d.ts +30 -0
  26. package/dist/create/editor-integration.js +218 -0
  27. package/dist/create/eslint-rules-template.d.ts +2 -2
  28. package/dist/create/framework-catalog.d.ts +8 -0
  29. package/dist/create/framework-catalog.js +28 -5
  30. package/dist/create/index.d.ts +7 -6
  31. package/dist/create/index.js +5 -4
  32. package/dist/create/{init-scaffold.d.ts → scaffold.d.ts} +4 -4
  33. package/dist/create/{init-scaffold.js → scaffold.js} +33 -2
  34. package/dist/create/templates.d.ts +29 -0
  35. package/dist/create/{init-templates.js → templates.js} +15 -0
  36. package/dist/create/wizard.d.ts +8 -0
  37. package/dist/create/{init-wizard.js → wizard.js} +11 -4
  38. package/dist/emit/emitters/setup.js +22 -0
  39. package/dist/ir/build-ir.js +24 -3
  40. package/dist/library/catalog.d.ts +35 -0
  41. package/dist/library/catalog.js +67 -0
  42. package/dist/library/cli.d.ts +2 -0
  43. package/dist/library/cli.js +168 -0
  44. package/dist/library/init.d.ts +25 -0
  45. package/dist/library/init.js +397 -0
  46. package/dist/library/install.d.ts +9 -0
  47. package/dist/library/install.js +80 -0
  48. package/dist/library/registry-search.d.ts +34 -0
  49. package/dist/library/registry-search.js +50 -0
  50. package/dist/library/validate.d.ts +17 -0
  51. package/dist/library/validate.js +175 -0
  52. package/dist/library-packages.d.ts +95 -0
  53. package/dist/library-packages.js +275 -0
  54. package/dist/orchestrator/graph-builder.js +16 -0
  55. package/dist/preview/client.js +17 -3
  56. package/dist/testing.d.ts +2 -2
  57. package/dist/testing.js +1 -1
  58. package/dist/transpile.js +55 -0
  59. package/dist/types.d.ts +19 -0
  60. package/dist/utils/cli.d.ts +2 -2
  61. package/dist/utils/cli.js +68 -3
  62. package/package.json +16 -8
  63. package/dist/create/init-templates.d.ts +0 -28
  64. package/dist/create/init-wizard.d.ts +0 -8
@@ -0,0 +1,175 @@
1
+ // ---------------------------------------------------------------------------
2
+ // `cuttlefish library validate` — the standalone library-package validator.
3
+ //
4
+ // Before this, manifest errors only surfaced at import time inside a
5
+ // transpile. This runs the same checks (and more) on a package directory:
6
+ // manifest schema, file existence, include/gateToken consistency, npm
7
+ // keyword consistency, and an AUTOSAR --strict pass over the shim bytes —
8
+ // the same self-check the generated application runs over them.
9
+ // ---------------------------------------------------------------------------
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { ComplianceContext, runSelfCheck } from "../emit/compliance/index.js";
13
+ import { LIBRARY_MARKER_KEYWORD, libraryCategory } from "./catalog.js";
14
+ function readJson(filePath) {
15
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
16
+ }
17
+ export function validateLibraryPackage(dir) {
18
+ const errors = [];
19
+ const warnings = [];
20
+ const root = path.resolve(dir);
21
+ const fail = (message) => {
22
+ errors.push(message);
23
+ };
24
+ // ── Manifest ───────────────────────────────────────────────────────────
25
+ const manifestPath = path.join(root, "cuttlefish.library.json");
26
+ let manifest;
27
+ if (!fs.existsSync(manifestPath)) {
28
+ fail("cuttlefish.library.json not found — is this a library package directory?");
29
+ }
30
+ else {
31
+ try {
32
+ manifest = readJson(manifestPath);
33
+ }
34
+ catch (e) {
35
+ fail(`cuttlefish.library.json is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
36
+ }
37
+ }
38
+ if (manifest) {
39
+ for (const field of ["id", "module", "framework", "include", "gateToken"]) {
40
+ if (typeof manifest[field] !== "string" || manifest[field].length === 0) {
41
+ fail(`Manifest field '${field}' must be a non-empty string.`);
42
+ }
43
+ }
44
+ if (!Array.isArray(manifest.shims) || manifest.shims.length === 0) {
45
+ fail("Manifest 'shims' must be a non-empty array.");
46
+ }
47
+ const include = typeof manifest.include === "string" ? manifest.include : "";
48
+ const gateToken = typeof manifest.gateToken === "string" ? manifest.gateToken : "";
49
+ if (include.length > 0 && gateToken.length > 0 && !include.includes(gateToken)) {
50
+ fail(`The gate token '${gateToken}' does not appear in the include '${include}' — ` +
51
+ `the include line is the only guaranteed emission, so contributions would never gate on.`);
52
+ }
53
+ // ── Shim files ───────────────────────────────────────────────────────
54
+ if (Array.isArray(manifest.shims)) {
55
+ for (const entry of manifest.shims) {
56
+ if (typeof entry !== "object" ||
57
+ entry === null ||
58
+ typeof entry.path !== "string" ||
59
+ typeof entry.outName !== "string") {
60
+ fail("Every 'shims' entry needs string 'path' and 'outName' fields.");
61
+ continue;
62
+ }
63
+ const shimPath = path.join(root, entry.path);
64
+ if (!fs.existsSync(shimPath)) {
65
+ fail(`Shim file listed in the manifest is missing: ${entry.path}`);
66
+ }
67
+ }
68
+ }
69
+ // ── Overlay fragment ─────────────────────────────────────────────────
70
+ if (typeof manifest.overlay === "string" && manifest.overlay.length > 0) {
71
+ const overlayPath = path.join(root, manifest.overlay);
72
+ if (!fs.existsSync(overlayPath)) {
73
+ fail(`Overlay fragment listed in the manifest is missing: ${manifest.overlay}`);
74
+ }
75
+ }
76
+ }
77
+ // ── package.json consistency ───────────────────────────────────────────
78
+ const pkgJsonPath = path.join(root, "package.json");
79
+ let pkgName;
80
+ let pkgKeywords = [];
81
+ if (!fs.existsSync(pkgJsonPath)) {
82
+ warnings.push("No package.json — nothing published, so keyword checks are skipped.");
83
+ }
84
+ else {
85
+ try {
86
+ const pkg = readJson(pkgJsonPath);
87
+ if (typeof pkg.name === "string") {
88
+ pkgName = pkg.name;
89
+ }
90
+ if (Array.isArray(pkg.keywords)) {
91
+ pkgKeywords = pkg.keywords.filter((k) => typeof k === "string");
92
+ }
93
+ if (Array.isArray(pkg.files) && !pkg.files.includes("cuttlefish.library.json")) {
94
+ fail("package.json 'files' must include 'cuttlefish.library.json' or the manifest will not ship.");
95
+ }
96
+ }
97
+ catch (e) {
98
+ fail(`package.json is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
99
+ }
100
+ }
101
+ if (pkgKeywords.length > 0 && !pkgKeywords.map((k) => k.toLowerCase()).includes(LIBRARY_MARKER_KEYWORD)) {
102
+ fail(`package.json keywords must include the marker '${LIBRARY_MARKER_KEYWORD}' — it is what library search scans for.`);
103
+ }
104
+ const categoryKeywords = pkgKeywords.filter((k) => k.startsWith("cuttlefish-") && k !== LIBRARY_MARKER_KEYWORD);
105
+ if (pkgKeywords.length > 0 && categoryKeywords.length === 0) {
106
+ warnings.push(`No category keyword in package.json — add one of ${LIBRARY_MARKER_KEYWORD}'s siblings (e.g. cuttlefish-led) so the library is browsable by category.`);
107
+ }
108
+ for (const kw of categoryKeywords) {
109
+ const id = kw.slice("cuttlefish-".length);
110
+ if (!libraryCategory(id)) {
111
+ warnings.push(`Keyword '${kw}' is not a known category keyword (cuttlefish-<id>).`);
112
+ }
113
+ }
114
+ if (categoryKeywords.length > 1) {
115
+ warnings.push(`Multiple category keywords (${categoryKeywords.join(", ")}) — search results show the first taxonomy match only.`);
116
+ }
117
+ if (manifest && pkgName !== undefined && typeof manifest.module === "string" && manifest.module !== pkgName) {
118
+ fail(`Manifest 'module' (${manifest.module}) must equal the package.json name (${pkgName}) — ` +
119
+ `the import specifier users write resolves against the package name.`);
120
+ }
121
+ // ── AUTOSAR strict over the shim bytes ─────────────────────────────────
122
+ const autosarFindings = [];
123
+ if (manifest && Array.isArray(manifest.shims)) {
124
+ const headerLines = [];
125
+ const sourceLines = [];
126
+ let anyReadable = false;
127
+ for (const entry of manifest.shims) {
128
+ if (typeof entry !== "object" || entry === null)
129
+ continue;
130
+ const rel = entry.path;
131
+ if (typeof rel !== "string")
132
+ continue;
133
+ const abs = path.join(root, rel);
134
+ if (!fs.existsSync(abs))
135
+ continue;
136
+ anyReadable = true;
137
+ const content = fs.readFileSync(abs, "utf8").split("\n");
138
+ if (abs.endsWith(".h") || abs.endsWith(".hpp")) {
139
+ headerLines.push(...content, "");
140
+ }
141
+ else {
142
+ sourceLines.push(...content, "");
143
+ }
144
+ }
145
+ if (anyReadable) {
146
+ const ctx = new ComplianceContext("strict");
147
+ autosarFindings.push(...runSelfCheck(ctx, sourceLines, headerLines));
148
+ }
149
+ }
150
+ for (const finding of autosarFindings) {
151
+ fail(`AUTOSAR ${finding.ruleId} (${finding.file} line ${finding.line}): ${finding.snippet}`);
152
+ }
153
+ return {
154
+ valid: errors.length === 0,
155
+ dir: root,
156
+ errors,
157
+ warnings,
158
+ autosarFindings,
159
+ };
160
+ }
161
+ /** Render the human-readable report. Returns the process exit code (0/1). */
162
+ export function printValidationReport(report) {
163
+ for (const warning of report.warnings) {
164
+ console.log(` ! ${warning}`);
165
+ }
166
+ for (const error of report.errors) {
167
+ console.log(` x ${error}`);
168
+ }
169
+ if (report.valid) {
170
+ console.log(` Valid library package${report.warnings.length > 0 ? ` (${report.warnings.length} warning${report.warnings.length === 1 ? "" : "s"})` : ""}.`);
171
+ return 0;
172
+ }
173
+ console.log(` ${report.errors.length} error${report.errors.length === 1 ? "" : "s"}.`);
174
+ return 1;
175
+ }
@@ -0,0 +1,95 @@
1
+ import type { LibraryDefinition } from "./types.js";
2
+ /** A native source file a library contributes to the emitted sources. */
3
+ export interface CuttlefishLibraryShim {
4
+ /** Path of the shim file, relative to the library package root. */
5
+ path: string;
6
+ /** File name the shim is written as, in the emitted source directory. */
7
+ outName: string;
8
+ }
9
+ /** The parsed `cuttlefish.library.json` manifest. */
10
+ export interface CuttlefishLibraryManifest {
11
+ /** Stable library id (diagnostics / sidecar records). */
12
+ id: string;
13
+ /** The import specifier this library resolves (e.g. '@typecad/zephyr-esp32s3-rgb'). */
14
+ module: string;
15
+ /** The framework (strategy id) this library requires, e.g. 'zephyr'. */
16
+ framework: string;
17
+ /** Build-target prefixes this library supports (matched against buildTarget). */
18
+ targets?: string[];
19
+ /** C++ include directive content for the import, e.g. '"__tc_rgbled.h"'. */
20
+ include: string;
21
+ /** Token whose presence in emitted sources gates the library's artifacts. */
22
+ gateToken: string;
23
+ /** Native shim files written into the emitted source directory. */
24
+ shims: CuttlefishLibraryShim[];
25
+ /** CONFIG_ lines contributed to the framework's prj.conf. */
26
+ kconfig?: string[];
27
+ /** Path of a devicetree overlay fragment, relative to the package root. */
28
+ overlay?: string;
29
+ }
30
+ /** A registered library: manifest + resolved package root. */
31
+ export interface RegisteredCuttlefishLibrary {
32
+ manifest: CuttlefishLibraryManifest;
33
+ packageRoot: string;
34
+ }
35
+ /** One record of the `libraries.json` sidecar (build-time contribution set). */
36
+ export interface LibrarySidecarEntry {
37
+ id: string;
38
+ module: string;
39
+ framework: string;
40
+ gateToken: string;
41
+ kconfig: string[];
42
+ /** Absolute path of the overlay fragment, or null. */
43
+ overlay: string | null;
44
+ /** Shim file names written into the emitted source directory. */
45
+ shims: string[];
46
+ }
47
+ /** Sidecar file name — written next to the emitted sources. */
48
+ export declare const LIBRARY_SIDECAR_NAME = "libraries.json";
49
+ /** Clear all registrations. Called once per transpilation, before graph build. */
50
+ export declare function resetCuttlefishLibraries(): void;
51
+ /** All libraries registered during this transpilation (import-driven). */
52
+ export declare function getRegisteredCuttlefishLibraries(): RegisteredCuttlefishLibrary[];
53
+ /**
54
+ * Whether the specifier resolves to a library registered in this
55
+ * transpilation. Library packages are exempt from the @typecad/* "SDK
56
+ * imports are type-level only" include skip — their import IS the include
57
+ * of the shim header.
58
+ */
59
+ export declare function isRegisteredCuttlefishLibrary(moduleSpecifier: string): boolean;
60
+ /**
61
+ * Resolve the package root for a bare npm specifier from the importing file,
62
+ * then register the package as a cuttlefish library if it ships a
63
+ * `cuttlefish.library.json`. Returns true when the specifier resolved to a
64
+ * registered library (the import must then be skipped by the graph builder).
65
+ */
66
+ export declare function registerCuttlefishLibraryFromSpecifier(fromFile: string, moduleSpecifier: string): boolean;
67
+ /** Libdef entries for the registry: import → shim include. */
68
+ export declare function cuttlefishLibraryLibdefs(): LibraryDefinition[];
69
+ /**
70
+ * Validate registered libraries against the loaded framework (and, when
71
+ * known, the build target). Throws with an actionable message when a library
72
+ * cannot work in this project — far earlier and clearer than the native
73
+ * compiler would.
74
+ */
75
+ export declare function validateCuttlefishLibraries(frameworkId: string, buildTarget?: string): void;
76
+ /**
77
+ * Emit the library artifacts for the libraries actually used in this
78
+ * transpilation, and write the `libraries.json` sidecar:
79
+ *
80
+ * 1. scan the emitted sources for each library's gate token;
81
+ * 2. write (write-if-changed) the shims of every gated library into the
82
+ * emitted source directory — the framework scaffold's CMake/sketch
83
+ * regen picks the sources up automatically;
84
+ * 3. delete shim files recorded in the previous sidecar that are no longer
85
+ * used, so removing an import removes its native code;
86
+ * 4. persist the sidecar (even when empty — that clears stale state).
87
+ */
88
+ export declare function writeCuttlefishLibraryArtifacts(outDir: string, srcDir: string): void;
89
+ /**
90
+ * Read the `libraries.json` sidecar for build-time consumers (framework
91
+ * toolchains). Tolerant of a missing or corrupt file (returns []).
92
+ */
93
+ export declare function readCuttlefishLibrarySidecar(outDir: string): LibrarySidecarEntry[];
94
+ /** Re-exported for transpile.ts libdef injection convenience. */
95
+ export declare function libraryDefinitionKey(module: string): string;
@@ -0,0 +1,275 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Cuttlefish library packages
3
+ //
4
+ // A library package is an npm package whose root carries a
5
+ // `cuttlefish.library.json` manifest. It contributes native artifacts to the
6
+ // generated firmware application:
7
+ //
8
+ // - an include mapping (the import emits `#include` of the shim header)
9
+ // - shim files (native C++ written into the emitted source directory)
10
+ // - framework build contributions (Kconfig lines, a devicetree overlay
11
+ // fragment) consumed by the framework toolchain via the sidecar
12
+ //
13
+ // Discovery is import-driven: when the transpile graph builder resolves an
14
+ // import whose package ships the manifest, the package is registered at that
15
+ // moment and its TypeScript is NOT transpiled — the package's own types are
16
+ // the compile-time contract, and the native shim is the implementation.
17
+ // Nothing scans node_modules; an installed-but-never-imported library
18
+ // contributes nothing.
19
+ //
20
+ // The transpiler writes a `libraries.json` sidecar next to the emitted
21
+ // sources (the board-constants.json convention) recording the libraries that
22
+ // were actually used, so build-time consumers (framework toolchains) can
23
+ // re-derive their contributions idempotently from disk.
24
+ // ---------------------------------------------------------------------------
25
+ import fs from "node:fs";
26
+ import path from "node:path";
27
+ import { createRequire } from "node:module";
28
+ import { toModuleKey } from "./utils/strings.js";
29
+ /** Sidecar file name — written next to the emitted sources. */
30
+ export const LIBRARY_SIDECAR_NAME = "libraries.json";
31
+ const registered = new Map();
32
+ /** Specifiers known NOT to be library packages (avoids repeated resolve attempts). */
33
+ const knownNonLibraries = new Set();
34
+ /** Clear all registrations. Called once per transpilation, before graph build. */
35
+ export function resetCuttlefishLibraries() {
36
+ registered.clear();
37
+ knownNonLibraries.clear();
38
+ }
39
+ /** All libraries registered during this transpilation (import-driven). */
40
+ export function getRegisteredCuttlefishLibraries() {
41
+ return [...registered.values()];
42
+ }
43
+ /**
44
+ * Whether the specifier resolves to a library registered in this
45
+ * transpilation. Library packages are exempt from the @typecad/* "SDK
46
+ * imports are type-level only" include skip — their import IS the include
47
+ * of the shim header.
48
+ */
49
+ export function isRegisteredCuttlefishLibrary(moduleSpecifier) {
50
+ return registered.has(moduleSpecifier.toLowerCase());
51
+ }
52
+ function parseManifest(packageRoot) {
53
+ const manifestPath = path.join(packageRoot, "cuttlefish.library.json");
54
+ let raw;
55
+ try {
56
+ raw = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
57
+ }
58
+ catch {
59
+ return undefined;
60
+ }
61
+ if (typeof raw !== "object" || raw === null)
62
+ return undefined;
63
+ const m = raw;
64
+ if (typeof m.id !== "string" ||
65
+ typeof m.module !== "string" ||
66
+ typeof m.framework !== "string" ||
67
+ typeof m.include !== "string" ||
68
+ typeof m.gateToken !== "string" ||
69
+ !Array.isArray(m.shims) ||
70
+ m.shims.length === 0) {
71
+ return undefined;
72
+ }
73
+ for (const shim of m.shims) {
74
+ if (typeof shim?.path !== "string" || typeof shim?.outName !== "string") {
75
+ return undefined;
76
+ }
77
+ }
78
+ return {
79
+ id: m.id,
80
+ module: m.module,
81
+ framework: m.framework,
82
+ targets: m.targets,
83
+ include: m.include,
84
+ gateToken: m.gateToken,
85
+ shims: m.shims,
86
+ kconfig: Array.isArray(m.kconfig) ? m.kconfig.filter((k) => typeof k === "string") : [],
87
+ overlay: typeof m.overlay === "string" ? m.overlay : undefined,
88
+ };
89
+ }
90
+ /**
91
+ * Resolve the package root for a bare npm specifier from the importing file,
92
+ * then register the package as a cuttlefish library if it ships a
93
+ * `cuttlefish.library.json`. Returns true when the specifier resolved to a
94
+ * registered library (the import must then be skipped by the graph builder).
95
+ */
96
+ export function registerCuttlefishLibraryFromSpecifier(fromFile, moduleSpecifier) {
97
+ // Only bare npm specifiers ('pkg' or '@scope/pkg'[/subpath]) — relative
98
+ // imports and node builtins cannot be library packages.
99
+ if (moduleSpecifier.startsWith(".") || moduleSpecifier.startsWith("node:")) {
100
+ return false;
101
+ }
102
+ const cacheKey = moduleSpecifier.toLowerCase();
103
+ if (knownNonLibraries.has(cacheKey))
104
+ return false;
105
+ if (registered.has(cacheKey))
106
+ return true;
107
+ let packageRoot;
108
+ try {
109
+ const req = createRequire(path.resolve(fromFile));
110
+ // Resolve the package entry, then walk up to the nearest package.json —
111
+ // the package root. (Resolving the manifest subpath directly would be
112
+ // blocked by exports encapsulation; the entry point always resolves.)
113
+ const entry = req.resolve(moduleSpecifier);
114
+ let dir = path.dirname(entry);
115
+ while (dir !== path.dirname(dir)) {
116
+ if (fs.existsSync(path.join(dir, "package.json")))
117
+ break;
118
+ dir = path.dirname(dir);
119
+ }
120
+ if (dir !== path.dirname(dir))
121
+ packageRoot = dir;
122
+ }
123
+ catch {
124
+ packageRoot = undefined;
125
+ }
126
+ if (packageRoot === undefined) {
127
+ knownNonLibraries.add(cacheKey);
128
+ return false;
129
+ }
130
+ const manifest = parseManifest(packageRoot);
131
+ if (manifest === undefined) {
132
+ knownNonLibraries.add(cacheKey);
133
+ return false;
134
+ }
135
+ registered.set(cacheKey, { manifest, packageRoot });
136
+ return true;
137
+ }
138
+ /** Libdef entries for the registry: import → shim include. */
139
+ export function cuttlefishLibraryLibdefs() {
140
+ return getRegisteredCuttlefishLibraries().map(({ manifest }) => ({
141
+ module: manifest.module,
142
+ include: manifest.include,
143
+ }));
144
+ }
145
+ /**
146
+ * Validate registered libraries against the loaded framework (and, when
147
+ * known, the build target). Throws with an actionable message when a library
148
+ * cannot work in this project — far earlier and clearer than the native
149
+ * compiler would.
150
+ */
151
+ export function validateCuttlefishLibraries(frameworkId, buildTarget) {
152
+ for (const { manifest } of getRegisteredCuttlefishLibraries()) {
153
+ if (manifest.framework !== frameworkId) {
154
+ throw new Error(`Library ${manifest.module} requires the '${manifest.framework}' framework, ` +
155
+ `but this project uses '${frameworkId}'. ` +
156
+ `Install the matching framework or a '${frameworkId}' counterpart of this library.`);
157
+ }
158
+ if (buildTarget !== undefined &&
159
+ manifest.targets !== undefined &&
160
+ manifest.targets.length > 0 &&
161
+ !manifest.targets.some((t) => buildTarget.toLowerCase().startsWith(t.toLowerCase()))) {
162
+ throw new Error(`Library ${manifest.module} supports board targets ` +
163
+ `[${manifest.targets.join(", ")}], but this project builds for '${buildTarget}'.`);
164
+ }
165
+ }
166
+ }
167
+ function writeIfChanged(filePath, content) {
168
+ try {
169
+ if (fs.existsSync(filePath) && fs.readFileSync(filePath, "utf8") === content)
170
+ return;
171
+ }
172
+ catch {
173
+ /* fall through and write */
174
+ }
175
+ fs.writeFileSync(filePath, content);
176
+ }
177
+ function readEmittedTokens(srcDir) {
178
+ let out = "";
179
+ let names = [];
180
+ try {
181
+ names = fs.readdirSync(srcDir);
182
+ }
183
+ catch {
184
+ return "";
185
+ }
186
+ for (const name of names) {
187
+ if (!/\.(cpp|cc|c|h|hpp|ino)$/.test(name))
188
+ continue;
189
+ try {
190
+ out += fs.readFileSync(path.join(srcDir, name), "utf8");
191
+ }
192
+ catch {
193
+ /* best-effort */
194
+ }
195
+ }
196
+ return out;
197
+ }
198
+ function readSidecar(sidecarPath) {
199
+ try {
200
+ const raw = JSON.parse(fs.readFileSync(sidecarPath, "utf8"));
201
+ if (!Array.isArray(raw))
202
+ return [];
203
+ return raw.filter((e) => typeof e === "object" && e !== null && typeof e.module === "string");
204
+ }
205
+ catch {
206
+ return [];
207
+ }
208
+ }
209
+ /**
210
+ * Emit the library artifacts for the libraries actually used in this
211
+ * transpilation, and write the `libraries.json` sidecar:
212
+ *
213
+ * 1. scan the emitted sources for each library's gate token;
214
+ * 2. write (write-if-changed) the shims of every gated library into the
215
+ * emitted source directory — the framework scaffold's CMake/sketch
216
+ * regen picks the sources up automatically;
217
+ * 3. delete shim files recorded in the previous sidecar that are no longer
218
+ * used, so removing an import removes its native code;
219
+ * 4. persist the sidecar (even when empty — that clears stale state).
220
+ */
221
+ export function writeCuttlefishLibraryArtifacts(outDir, srcDir) {
222
+ const sidecarPath = path.join(outDir, LIBRARY_SIDECAR_NAME);
223
+ const previous = readSidecar(sidecarPath);
224
+ const previousShims = new Set(previous.flatMap((e) => e.shims));
225
+ const emitted = readEmittedTokens(srcDir);
226
+ const usedShims = new Set();
227
+ const entries = [];
228
+ for (const { manifest, packageRoot } of getRegisteredCuttlefishLibraries()) {
229
+ if (!emitted.includes(manifest.gateToken))
230
+ continue;
231
+ for (const shim of manifest.shims) {
232
+ let content;
233
+ try {
234
+ content = fs.readFileSync(path.join(packageRoot, shim.path), "utf8");
235
+ }
236
+ catch {
237
+ continue; // broken library packaging — the missing header surfaces at compile
238
+ }
239
+ writeIfChanged(path.join(srcDir, shim.outName), content);
240
+ usedShims.add(shim.outName);
241
+ }
242
+ entries.push({
243
+ id: manifest.id,
244
+ module: manifest.module,
245
+ framework: manifest.framework,
246
+ gateToken: manifest.gateToken,
247
+ kconfig: manifest.kconfig ?? [],
248
+ overlay: manifest.overlay ? path.join(packageRoot, manifest.overlay) : null,
249
+ shims: manifest.shims.map((s) => s.outName).filter((n) => usedShims.has(n)),
250
+ });
251
+ }
252
+ // Remove shims from a previous build whose library is no longer used.
253
+ for (const name of previousShims) {
254
+ if (usedShims.has(name))
255
+ continue;
256
+ try {
257
+ fs.rmSync(path.join(srcDir, name), { force: true });
258
+ }
259
+ catch {
260
+ /* best-effort */
261
+ }
262
+ }
263
+ writeIfChanged(sidecarPath, JSON.stringify(entries, null, 2) + "\n");
264
+ }
265
+ /**
266
+ * Read the `libraries.json` sidecar for build-time consumers (framework
267
+ * toolchains). Tolerant of a missing or corrupt file (returns []).
268
+ */
269
+ export function readCuttlefishLibrarySidecar(outDir) {
270
+ return readSidecar(path.join(outDir, LIBRARY_SIDECAR_NAME));
271
+ }
272
+ /** Re-exported for transpile.ts libdef injection convenience. */
273
+ export function libraryDefinitionKey(module) {
274
+ return toModuleKey(module);
275
+ }
@@ -3,6 +3,7 @@ import ts from "typescript";
3
3
  import { readText } from "../utils/fs.js";
4
4
  import { detectNativeCppModule, getNpmPackageInfoForFile, isInNodeModules, resolveImport, isCuttlefishSDKPath, } from "../transpile/resolution.js";
5
5
  import { requireUIHook } from "../ui-hook.js";
6
+ import { registerCuttlefishLibraryFromSpecifier } from "../library-packages.js";
6
7
  /**
7
8
  * Sort files in dependency order using Kahn's algorithm.
8
9
  * Dependencies come before dependents so that include ordering is correct
@@ -125,6 +126,14 @@ export async function collectTranspileGraph(entryFile, boardPackage, imageDecode
125
126
  }
126
127
  if (!moduleSpecifier)
127
128
  continue;
129
+ // Cuttlefish library package: an import whose package ships a
130
+ // cuttlefish.library.json contributes native shims + build fragments
131
+ // instead of transpiling — its own TypeScript types are the contract.
132
+ // Must precede the @typecad/* skips: scoped libraries would otherwise
133
+ // be dropped as "SDK packages" here (the .ui script loop skips ALL
134
+ // @typecad/* specifiers, unlike the selective main-loop skip list).
135
+ if (registerCuttlefishLibraryFromSpecifier(filePath, moduleSpecifier))
136
+ continue;
128
137
  if (moduleSpecifier === "@typecad/expect" || moduleSpecifier === "@typecad/ui" || moduleSpecifier === "@typecad/safety")
129
138
  continue;
130
139
  if (moduleSpecifier.startsWith("@typecad/"))
@@ -158,6 +167,13 @@ export async function collectTranspileGraph(entryFile, boardPackage, imageDecode
158
167
  nativeModules.set(moduleSpecifier, nativeModule);
159
168
  continue; // Don't try to resolve as TypeScript
160
169
  }
170
+ // Cuttlefish library package: an import whose package ships a
171
+ // cuttlefish.library.json contributes native shims + build fragments
172
+ // instead of transpiling — its own TypeScript types are the contract.
173
+ // Checked before the @typecad/* skips so scoped libraries register.
174
+ if (registerCuttlefishLibraryFromSpecifier(filePath, moduleSpecifier)) {
175
+ continue;
176
+ }
161
177
  // Skip @typecad/expect — it provides type-level stubs only.
162
178
  // The AST preprocessor rewrites all expect calls before transpilation.
163
179
  if (moduleSpecifier === "@typecad/expect") {
@@ -8,6 +8,9 @@ let pendingPointerFrame = 0;
8
8
  let debugModes = { boxes: false, clips: false, dirty: false, inspect: false };
9
9
  let debugOverlay;
10
10
  let debugCtx;
11
+ /** Re-syncs the overlay to the app canvas whenever layout changes its box
12
+ * (aspect-ratio application, max-height, container reflow). */
13
+ let overlayResizeObserver;
11
14
  let dirtyFlashAlpha = 0;
12
15
  /** CSS px per logical display px — the overlay backing store runs at this
13
16
  * resolution so debug strokes are 1 CSS px thin (a 1-logical-px stroke on
@@ -263,11 +266,22 @@ async function start() {
263
266
  debugOverlay.height = Math.max(1, Math.round(rect.height * dpr));
264
267
  debugScale = (rect.width * dpr) / snapshot.program.width;
265
268
  };
266
- syncOverlayBox();
267
- window.addEventListener("resize", syncOverlayBox);
268
269
  // Set the canvas CSS aspect-ratio to match the display (e.g. 128:64 for
269
- // OLED, 320:240 for TFT) so the browser scales it proportionally.
270
+ // OLED, 320:240 for TFT) BEFORE syncing the overlay: the canvas is
271
+ // vertically centered (place-items: center), so applying the real aspect
272
+ // after the sync SHRANK it and moved it DOWN — the overlay stayed at the
273
+ // old 4/3-fallback position and every debug box sat that far above its
274
+ // element. A ResizeObserver re-syncs on any later layout-driven size
275
+ // change (max-height kicking in, container reflow), not just window
276
+ // resizes.
270
277
  document.documentElement.style.setProperty("--display-aspect", `${snapshot.program.width} / ${snapshot.program.height}`);
278
+ syncOverlayBox();
279
+ window.addEventListener("resize", syncOverlayBox);
280
+ if (typeof ResizeObserver !== "undefined") {
281
+ overlayResizeObserver?.disconnect();
282
+ overlayResizeObserver = new ResizeObserver(() => syncOverlayBox());
283
+ overlayResizeObserver.observe(canvas);
284
+ }
271
285
  const status = byId("status");
272
286
  status.textContent = `${snapshot.profileName ?? snapshot.program.display?.driver ?? "display"} ${snapshot.program.width}x${snapshot.program.height} ${snapshot.program.colorFormat}`;
273
287
  const extraDiagnostics = [];
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";