@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.
- package/assets/editor-extensions/typecad-debug/README.md +142 -0
- package/assets/editor-extensions/typecad-debug/out/extension.js +347 -0
- package/assets/editor-extensions/typecad-debug/out/extension.js.map +1 -0
- package/assets/editor-extensions/typecad-debug/package.json +76 -0
- package/assets/editor-extensions/typecad-ui/LICENSE +27 -0
- package/assets/editor-extensions/typecad-ui/README.md +52 -0
- package/assets/editor-extensions/typecad-ui/file-icon-dark.png +0 -0
- package/assets/editor-extensions/typecad-ui/file-icon-light.png +0 -0
- package/assets/editor-extensions/typecad-ui/icon.png +0 -0
- package/assets/editor-extensions/typecad-ui/language-configuration.json +43 -0
- package/assets/editor-extensions/typecad-ui/package.json +54 -0
- package/assets/editor-extensions/typecad-ui/snippets/typecad-ui.json +80 -0
- package/assets/editor-extensions/typecad-ui/syntaxes/markdown-ui.json +45 -0
- package/assets/editor-extensions/typecad-ui/syntaxes/typecad-ui.tmLanguage.json +1269 -0
- package/dist/api/config.d.ts +8 -1
- package/dist/api/shared/framework-manifest.d.ts +61 -504
- package/dist/cli.js +17 -6
- package/dist/config-loader.js +3 -1
- package/dist/config-schema.d.ts +17 -223
- package/dist/config-schema.js +1 -0
- package/dist/contract/contract-parser.d.ts +9 -127
- package/dist/create/board-checklist.js +1 -1
- package/dist/create/board-codegen.js +4 -4
- package/dist/create/board-spec.d.ts +74 -530
- package/dist/create/editor-integration.d.ts +30 -0
- package/dist/create/editor-integration.js +218 -0
- package/dist/create/eslint-rules-template.d.ts +2 -2
- package/dist/create/framework-catalog.d.ts +8 -0
- package/dist/create/framework-catalog.js +28 -5
- package/dist/create/index.d.ts +7 -6
- package/dist/create/index.js +5 -4
- package/dist/create/{init-scaffold.d.ts → scaffold.d.ts} +4 -4
- package/dist/create/{init-scaffold.js → scaffold.js} +33 -2
- package/dist/create/templates.d.ts +29 -0
- package/dist/create/{init-templates.js → templates.js} +15 -0
- package/dist/create/wizard.d.ts +8 -0
- package/dist/create/{init-wizard.js → wizard.js} +11 -4
- package/dist/emit/emitters/setup.js +22 -0
- package/dist/ir/build-ir.js +24 -3
- package/dist/library/catalog.d.ts +35 -0
- package/dist/library/catalog.js +67 -0
- package/dist/library/cli.d.ts +2 -0
- package/dist/library/cli.js +168 -0
- package/dist/library/init.d.ts +25 -0
- package/dist/library/init.js +397 -0
- package/dist/library/install.d.ts +9 -0
- package/dist/library/install.js +80 -0
- package/dist/library/registry-search.d.ts +34 -0
- package/dist/library/registry-search.js +50 -0
- package/dist/library/validate.d.ts +17 -0
- package/dist/library/validate.js +175 -0
- package/dist/library-packages.d.ts +95 -0
- package/dist/library-packages.js +275 -0
- package/dist/orchestrator/graph-builder.js +16 -0
- package/dist/preview/client.js +17 -3
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +1 -1
- package/dist/transpile.js +55 -0
- package/dist/types.d.ts +19 -0
- package/dist/utils/cli.d.ts +2 -2
- package/dist/utils/cli.js +68 -3
- package/package.json +16 -8
- package/dist/create/init-templates.d.ts +0 -28
- 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") {
|
package/dist/preview/client.js
CHANGED
|
@@ -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)
|
|
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,
|
|
23
|
-
export type {
|
|
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,
|
|
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";
|