@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.
- 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/add-preset.d.ts +4 -0
- package/dist/add-preset.js +74 -0
- package/dist/api/config.d.ts +8 -5
- package/dist/api/shared/display-adapters/sdl.js +1 -1
- package/dist/api/shared/display-profile.d.ts +11 -0
- package/dist/api/shared/display-profile.js +3 -0
- package/dist/api/shared/framework-manifest.d.ts +61 -504
- package/dist/api/shared/hal-op-ir.d.ts +19 -0
- package/dist/api/shared/toolchain-types.d.ts +0 -1
- package/dist/cli.js +31 -9
- package/dist/config-loader.d.ts +0 -2
- package/dist/config-loader.js +13 -6
- package/dist/config-schema.d.ts +17 -230
- package/dist/config-schema.js +1 -3
- 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/debug-artifacts.d.ts +20 -0
- package/dist/create/debug-artifacts.js +69 -0
- 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 +9 -6
- package/dist/create/index.js +6 -4
- package/dist/create/{init-scaffold.d.ts → scaffold.d.ts} +5 -4
- package/dist/create/{init-scaffold.js → scaffold.js} +38 -2
- package/dist/create/templates.d.ts +29 -0
- package/dist/create/{init-templates.js → templates.js} +15 -2
- package/dist/create/wizard.d.ts +8 -0
- package/dist/create/{init-wizard.js → wizard.js} +11 -4
- package/dist/emit/compliance/rules.js +18 -4
- package/dist/emit/emitters/function-emitter-impl.js +7 -1
- package/dist/emit/emitters/line-appender.js +6 -0
- package/dist/emit/emitters/setup.js +22 -0
- package/dist/emit/emitters/ui-emitter.js +40 -15
- package/dist/emit/route-hal-op.js +55 -1
- package/dist/emit/statement-renderer.js +5 -2
- package/dist/ir/build-ir.js +46 -4
- package/dist/ir/expression-to-ir.js +17 -0
- package/dist/ir/hal/hal-emitter.js +23 -5
- package/dist/ir/hal/hal-plugins.js +11 -0
- package/dist/ir/pin-mode-validation.js +32 -9
- package/dist/ir/pin-state-tracking.d.ts +58 -0
- package/dist/ir/pin-state-tracking.js +182 -0
- package/dist/ir/program-analysis.d.ts +6 -0
- package/dist/ir/program-analysis.js +38 -0
- package/dist/ir/statement-to-ir.js +14 -0
- package/dist/ir/transformers/control-flow.js +29 -0
- package/dist/ir/transformers/ui-call-resolver.js +105 -1
- package/dist/ir/ui-element-auto-wire.js +7 -4
- 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.d.ts +4 -1
- package/dist/orchestrator/graph-builder.js +23 -1
- package/dist/preview/api-shared-shim.d.ts +1 -0
- package/dist/preview/api-shared-shim.js +7 -0
- package/dist/preview/client.js +235 -2
- package/dist/preview/server.js +154 -62
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +1 -1
- package/dist/theme-tokens.d.ts +22 -0
- package/dist/theme-tokens.js +172 -0
- package/dist/transpile.js +90 -5
- package/dist/types.d.ts +24 -0
- package/dist/ui-hook.d.ts +7 -0
- package/dist/utils/cli.d.ts +2 -2
- package/dist/utils/cli.js +77 -3
- package/dist/utils/ui.d.ts +5 -0
- package/dist/utils/ui.js +7 -0
- package/package.json +18 -9
- package/dist/create/init-templates.d.ts +0 -28
- package/dist/create/init-wizard.d.ts +0 -8
|
@@ -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
|
+
}
|
|
@@ -15,4 +15,7 @@ export declare function topologicalSortFiles(files: string[], dependencies: Map<
|
|
|
15
15
|
* @param boardPackage When provided, `@typecad/board` imports resolve to this
|
|
16
16
|
* board package (e.g. `'@typecad/board-arduino-uno'`).
|
|
17
17
|
*/
|
|
18
|
-
export declare function collectTranspileGraph(entryFile: string, boardPackage?: string
|
|
18
|
+
export declare function collectTranspileGraph(entryFile: string, boardPackage?: string, imageDecodeOpts?: {
|
|
19
|
+
maxW?: number;
|
|
20
|
+
maxH?: number;
|
|
21
|
+
}): Promise<TranspileGraphResult>;
|
|
@@ -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
|
|
@@ -68,7 +69,7 @@ export function topologicalSortFiles(files, dependencies) {
|
|
|
68
69
|
* @param boardPackage When provided, `@typecad/board` imports resolve to this
|
|
69
70
|
* board package (e.g. `'@typecad/board-arduino-uno'`).
|
|
70
71
|
*/
|
|
71
|
-
export function collectTranspileGraph(entryFile, boardPackage) {
|
|
72
|
+
export async function collectTranspileGraph(entryFile, boardPackage, imageDecodeOpts) {
|
|
72
73
|
const ordered = [];
|
|
73
74
|
const pending = [path.resolve(entryFile)];
|
|
74
75
|
const visited = new Set();
|
|
@@ -102,6 +103,10 @@ export function collectTranspileGraph(entryFile, boardPackage) {
|
|
|
102
103
|
const parts = ui.splitUiFile(sourceText);
|
|
103
104
|
// Register the template as a UI module at <file>.ui.html (synthetic path).
|
|
104
105
|
const uiHtmlPath = filePath + ".html";
|
|
106
|
+
// Prime the image-conversion cache before the (synchronous) module
|
|
107
|
+
// load — <img src="*.png|jpg|ico|…"> decodes here, and the module's
|
|
108
|
+
// asset reader + natural-size layout pull from the cache.
|
|
109
|
+
await ui.warmUpImageDecoding(parts.html, path.dirname(filePath), imageDecodeOpts ?? {});
|
|
105
110
|
ui.loadUIModuleFromText(uiHtmlPath, parts.html, parts.style, filePath);
|
|
106
111
|
uiModules.add(uiHtmlPath);
|
|
107
112
|
// Use the <script> as the TS source for import-graph walking. Inject an
|
|
@@ -121,6 +126,14 @@ export function collectTranspileGraph(entryFile, boardPackage) {
|
|
|
121
126
|
}
|
|
122
127
|
if (!moduleSpecifier)
|
|
123
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;
|
|
124
137
|
if (moduleSpecifier === "@typecad/expect" || moduleSpecifier === "@typecad/ui" || moduleSpecifier === "@typecad/safety")
|
|
125
138
|
continue;
|
|
126
139
|
if (moduleSpecifier.startsWith("@typecad/"))
|
|
@@ -154,6 +167,13 @@ export function collectTranspileGraph(entryFile, boardPackage) {
|
|
|
154
167
|
nativeModules.set(moduleSpecifier, nativeModule);
|
|
155
168
|
continue; // Don't try to resolve as TypeScript
|
|
156
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
|
+
}
|
|
157
177
|
// Skip @typecad/expect — it provides type-level stubs only.
|
|
158
178
|
// The AST preprocessor rewrites all expect calls before transpilation.
|
|
159
179
|
if (moduleSpecifier === "@typecad/expect") {
|
|
@@ -189,6 +209,8 @@ export function collectTranspileGraph(entryFile, boardPackage) {
|
|
|
189
209
|
// .ui.html modules: load into the UI registry, record the path, and don't
|
|
190
210
|
// push onto `pending` (they are never parsed as TypeScript).
|
|
191
211
|
if (resolved?.uiModule) {
|
|
212
|
+
const uiHtmlText = readText(resolved.sourcePath);
|
|
213
|
+
await requireUIHook().warmUpImageDecoding(uiHtmlText, path.dirname(resolved.sourcePath), imageDecodeOpts ?? {});
|
|
192
214
|
requireUIHook().loadUIModule(resolved.sourcePath);
|
|
193
215
|
uiModules.add(resolved.sourcePath);
|
|
194
216
|
// Track the dependency edge so topological sort orders the importer
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { resolveScrollConfig } from "../api/shared/display-profile.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Browser shim served to the preview page. The host runtime imports
|
|
2
|
+
// resolveScrollConfig from "@typecad/ui/..."→"@typecad/cuttlefish/api/shared",
|
|
3
|
+
// whose barrel index re-exports node-only modules (fs, zod) that cannot load
|
|
4
|
+
// in a browser. The preview's import map points the bare specifier at this
|
|
5
|
+
// file, which re-exports just the browser-safe piece the runtime needs, from
|
|
6
|
+
// the module that actually defines it.
|
|
7
|
+
export { resolveScrollConfig } from "../api/shared/display-profile.js";
|
package/dist/preview/client.js
CHANGED
|
@@ -5,6 +5,172 @@ let runtime;
|
|
|
5
5
|
let imageData;
|
|
6
6
|
let pendingPointerMove;
|
|
7
7
|
let pendingPointerFrame = 0;
|
|
8
|
+
let debugModes = { boxes: false, clips: false, dirty: false, inspect: false };
|
|
9
|
+
let debugOverlay;
|
|
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;
|
|
14
|
+
let dirtyFlashAlpha = 0;
|
|
15
|
+
/** CSS px per logical display px — the overlay backing store runs at this
|
|
16
|
+
* resolution so debug strokes are 1 CSS px thin (a 1-logical-px stroke on
|
|
17
|
+
* the app canvas's own backing store would upscale 3x thick and bury the
|
|
18
|
+
* content under investigation). */
|
|
19
|
+
let debugScale = 1;
|
|
20
|
+
// Stroke color per node kind: a screenshot should be self-describing.
|
|
21
|
+
// views cyan · text yellow · interactive magenta · img/canvas orange ·
|
|
22
|
+
// list green · screen transparent (skip; it's the whole panel).
|
|
23
|
+
function debugColor(tag, kind) {
|
|
24
|
+
if (tag === "screen")
|
|
25
|
+
return undefined;
|
|
26
|
+
if (tag === "img" || tag === "canvas")
|
|
27
|
+
return "#ff9a3c";
|
|
28
|
+
if (tag === "list")
|
|
29
|
+
return "#3ddc84";
|
|
30
|
+
if (["button", "input", "select", "check", "radio", "range", "progress", "drawer"].includes(tag))
|
|
31
|
+
return "#ff4fd8";
|
|
32
|
+
if (kind === "text")
|
|
33
|
+
return "#ffe14d";
|
|
34
|
+
return "#3cc8ff";
|
|
35
|
+
}
|
|
36
|
+
function applyDebugCapture() {
|
|
37
|
+
runtime?.setDebugCapture?.(debugModes.boxes || debugModes.clips || debugModes.dirty);
|
|
38
|
+
const state = document.getElementById("debugState");
|
|
39
|
+
if (state) {
|
|
40
|
+
const on = ["boxes", "clips", "dirty", "inspect"].filter((m) => debugModes[m]);
|
|
41
|
+
state.textContent = on.length === 0
|
|
42
|
+
? "overlay: off — check a box or press D"
|
|
43
|
+
: `overlay: ${on.join("+")}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function drawDebugOverlay(width, height) {
|
|
47
|
+
if (!debugOverlay || !debugCtx)
|
|
48
|
+
return;
|
|
49
|
+
const ctx = debugCtx;
|
|
50
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
51
|
+
ctx.clearRect(0, 0, debugOverlay.width, debugOverlay.height);
|
|
52
|
+
ctx.setTransform(debugScale, 0, 0, debugScale, 0, 0);
|
|
53
|
+
const anyVisual = debugModes.boxes || debugModes.clips || debugModes.dirty;
|
|
54
|
+
if (!anyVisual || !runtime?.debugInfo)
|
|
55
|
+
return;
|
|
56
|
+
const info = runtime.debugInfo();
|
|
57
|
+
if (debugModes.dirty) {
|
|
58
|
+
// Flash the regions repainted this frame; fades until the next repaint.
|
|
59
|
+
dirtyFlashAlpha = Math.min(0.55, dirtyFlashAlpha + 0.35);
|
|
60
|
+
ctx.fillStyle = `rgba(255, 225, 77, ${dirtyFlashAlpha.toFixed(2)})`;
|
|
61
|
+
for (const r of info.painted)
|
|
62
|
+
ctx.fillRect(r.x, r.y, r.w, r.h);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
dirtyFlashAlpha = 0;
|
|
66
|
+
}
|
|
67
|
+
// 1 CSS px regardless of the app canvas's logical upscaling — thicker
|
|
68
|
+
// strokes bury the content being inspected.
|
|
69
|
+
ctx.lineWidth = Math.max(1 / debugScale, 0.5);
|
|
70
|
+
if (debugModes.clips) {
|
|
71
|
+
// Viewport tint guarantees visibility at any stroke sampling; the dashed
|
|
72
|
+
// outline marks the exact clip edges.
|
|
73
|
+
for (const c of info.clips) {
|
|
74
|
+
ctx.fillStyle = "rgba(61, 220, 132, 0.08)";
|
|
75
|
+
ctx.fillRect(c.x, c.y, c.w, c.h);
|
|
76
|
+
}
|
|
77
|
+
ctx.strokeStyle = "#3ddc84";
|
|
78
|
+
ctx.setLineDash([3, 2]);
|
|
79
|
+
for (const c of info.clips)
|
|
80
|
+
ctx.strokeRect(c.x, c.y, c.w - 1, c.h - 1);
|
|
81
|
+
ctx.setLineDash([]);
|
|
82
|
+
}
|
|
83
|
+
if (debugModes.boxes) {
|
|
84
|
+
for (const n of info.nodes) {
|
|
85
|
+
const color = debugColor(n.tag, n.kind);
|
|
86
|
+
if (!color)
|
|
87
|
+
continue;
|
|
88
|
+
ctx.strokeStyle = color;
|
|
89
|
+
ctx.strokeRect(n.x + 0.5, n.y + 0.5, n.w - 1, n.h - 1);
|
|
90
|
+
// Untappable interactive elements get a red corner mark — the
|
|
91
|
+
// id-less-element trap (the preview wires by id).
|
|
92
|
+
if (!n.tappable && ["button", "input", "select", "check", "radio", "list"].includes(n.tag)) {
|
|
93
|
+
ctx.strokeStyle = "#ff5252";
|
|
94
|
+
ctx.beginPath();
|
|
95
|
+
ctx.moveTo(n.x, n.y);
|
|
96
|
+
ctx.lineTo(n.x + 6, n.y);
|
|
97
|
+
ctx.moveTo(n.x, n.y);
|
|
98
|
+
ctx.lineTo(n.x, n.y + 6);
|
|
99
|
+
ctx.stroke();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function inspectAt(px, py, width, height, report) {
|
|
105
|
+
if (!runtime?.debugHit)
|
|
106
|
+
return false;
|
|
107
|
+
const i = runtime.debugHit(px, py);
|
|
108
|
+
if (i < 0) {
|
|
109
|
+
report(`inspect (${px},${py}): empty space`);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
const info = runtime.debugInfo();
|
|
113
|
+
const n = info.nodes.find((x) => x.i === i);
|
|
114
|
+
if (!n)
|
|
115
|
+
return false;
|
|
116
|
+
report(`inspect #${i}${n.id ? ` '${n.id}'` : ""} <${n.tag}> kind=${n.kind} box=${n.x},${n.y} ${n.w}x${n.h} tappable=${n.tappable}`);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
function wireDebugControls(canvas, width, height, report, redraw) {
|
|
120
|
+
const modesBox = document.getElementById("debugModes");
|
|
121
|
+
if (modesBox) {
|
|
122
|
+
// ?debug=boxes,clips,dirty,inspect seeds the initial state.
|
|
123
|
+
const params = new URLSearchParams(location.search);
|
|
124
|
+
const seeded = (params.get("debug") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
125
|
+
for (const input of Array.from(modesBox.querySelectorAll("input[data-mode]"))) {
|
|
126
|
+
const mode = input.dataset.mode;
|
|
127
|
+
if (seeded.includes(mode))
|
|
128
|
+
debugModes[mode] = true;
|
|
129
|
+
input.checked = debugModes[mode];
|
|
130
|
+
input.addEventListener("change", () => {
|
|
131
|
+
debugModes[mode] = input.checked;
|
|
132
|
+
applyDebugCapture();
|
|
133
|
+
redraw();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// D cycles boxes → clips → dirty → off (inspect stays manual — it blocks
|
|
138
|
+
// normal input and shouldn't be cycled into by accident).
|
|
139
|
+
window.addEventListener("keydown", (e) => {
|
|
140
|
+
if (e.key !== "d" && e.key !== "D")
|
|
141
|
+
return;
|
|
142
|
+
const on = debugModes.boxes || debugModes.clips || debugModes.dirty;
|
|
143
|
+
if (!on)
|
|
144
|
+
debugModes = { ...debugModes, boxes: true };
|
|
145
|
+
else if (debugModes.boxes)
|
|
146
|
+
debugModes = { ...debugModes, boxes: false, clips: true };
|
|
147
|
+
else if (debugModes.clips)
|
|
148
|
+
debugModes = { ...debugModes, clips: false, dirty: true };
|
|
149
|
+
else
|
|
150
|
+
debugModes = { ...debugModes, dirty: false };
|
|
151
|
+
for (const input of Array.from((modesBox ?? document).querySelectorAll("input[data-mode]"))) {
|
|
152
|
+
const mode = input.dataset.mode;
|
|
153
|
+
input.checked = debugModes[mode];
|
|
154
|
+
}
|
|
155
|
+
applyDebugCapture();
|
|
156
|
+
redraw();
|
|
157
|
+
});
|
|
158
|
+
// Inspect taps: while inspect mode is on, taps report instead of
|
|
159
|
+
// interacting. Ctrl-click always inspects without entering the mode.
|
|
160
|
+
const inspectPointer = (event, force) => {
|
|
161
|
+
if (!force && !debugModes.inspect)
|
|
162
|
+
return false;
|
|
163
|
+
const p = canvasPoint(canvas, event, width, height);
|
|
164
|
+
inspectAt(p.x, p.y, width, height, report);
|
|
165
|
+
return true;
|
|
166
|
+
};
|
|
167
|
+
canvas.addEventListener("pointerdown", (event) => {
|
|
168
|
+
if (inspectPointer(event, event.ctrlKey || event.metaKey)) {
|
|
169
|
+
event.stopImmediatePropagation();
|
|
170
|
+
event.preventDefault();
|
|
171
|
+
}
|
|
172
|
+
}, true);
|
|
173
|
+
}
|
|
8
174
|
function byId(id) {
|
|
9
175
|
const el = document.getElementById(id);
|
|
10
176
|
if (!el)
|
|
@@ -74,9 +240,48 @@ async function start() {
|
|
|
74
240
|
if (!ctx)
|
|
75
241
|
throw new Error("2D canvas context unavailable");
|
|
76
242
|
ctx.imageSmoothingEnabled = false;
|
|
243
|
+
// Debug overlay: same backing resolution + CSS as the app canvas, stacked
|
|
244
|
+
// exactly on top; pointer-events pass through to the app canvas below.
|
|
245
|
+
debugOverlay?.remove();
|
|
246
|
+
debugOverlay = document.createElement("canvas");
|
|
247
|
+
debugOverlay.id = "debugOverlay";
|
|
248
|
+
debugOverlay.width = snapshot.program.width;
|
|
249
|
+
debugOverlay.height = snapshot.program.height;
|
|
250
|
+
canvas.parentElement?.appendChild(debugOverlay);
|
|
251
|
+
debugCtx = debugOverlay.getContext("2d") ?? undefined;
|
|
252
|
+
const syncOverlayBox = () => {
|
|
253
|
+
if (!debugOverlay)
|
|
254
|
+
return;
|
|
255
|
+
const rect = canvas.getBoundingClientRect();
|
|
256
|
+
const parent = canvas.parentElement;
|
|
257
|
+
const prect = parent.getBoundingClientRect();
|
|
258
|
+
debugOverlay.style.left = `${rect.left - prect.left}px`;
|
|
259
|
+
debugOverlay.style.top = `${rect.top - prect.top}px`;
|
|
260
|
+
debugOverlay.style.width = `${rect.width}px`;
|
|
261
|
+
debugOverlay.style.height = `${rect.height}px`;
|
|
262
|
+
// Backing store at CSS-pixel resolution: strokes draw at device-crisp
|
|
263
|
+
// 1 CSS px regardless of the app canvas's logical upscaling.
|
|
264
|
+
const dpr = window.devicePixelRatio || 1;
|
|
265
|
+
debugOverlay.width = Math.max(1, Math.round(rect.width * dpr));
|
|
266
|
+
debugOverlay.height = Math.max(1, Math.round(rect.height * dpr));
|
|
267
|
+
debugScale = (rect.width * dpr) / snapshot.program.width;
|
|
268
|
+
};
|
|
77
269
|
// Set the canvas CSS aspect-ratio to match the display (e.g. 128:64 for
|
|
78
|
-
// 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.
|
|
79
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
|
+
}
|
|
80
285
|
const status = byId("status");
|
|
81
286
|
status.textContent = `${snapshot.profileName ?? snapshot.program.display?.driver ?? "display"} ${snapshot.program.width}x${snapshot.program.height} ${snapshot.program.colorFormat}`;
|
|
82
287
|
const extraDiagnostics = [];
|
|
@@ -92,16 +297,39 @@ async function start() {
|
|
|
92
297
|
return;
|
|
93
298
|
imageData.data.set(rgba);
|
|
94
299
|
ctx.putImageData(imageData, 0, 0);
|
|
300
|
+
drawDebugOverlay(snapshot.program.width, snapshot.program.height);
|
|
95
301
|
},
|
|
96
302
|
onDiagnostics: (message) => {
|
|
97
303
|
extraDiagnostics.push(message);
|
|
98
304
|
renderDiagnostics(snapshot, extraDiagnostics);
|
|
99
305
|
},
|
|
100
306
|
});
|
|
307
|
+
// Mouse wheel scrolls the scroll owner under the cursor — same hit-scan
|
|
308
|
+
// as a drag (lists, scroll containers). Without this, wheel-scrolling a
|
|
309
|
+
// canvas preview does nothing.
|
|
310
|
+
canvas.onwheel = (event) => {
|
|
311
|
+
if (!runtime)
|
|
312
|
+
return;
|
|
313
|
+
event.preventDefault();
|
|
314
|
+
const p = canvasPoint(canvas, event, snapshot.program.width, snapshot.program.height);
|
|
315
|
+
// deltaMode: 0 = pixels, 1 = lines, 2 = pages.
|
|
316
|
+
const scale = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? 100 : 1;
|
|
317
|
+
runtime.wheel(p.x, p.y, event.deltaY * scale);
|
|
318
|
+
};
|
|
101
319
|
canvas.onpointerdown = (event) => {
|
|
102
320
|
if (!runtime)
|
|
103
321
|
return;
|
|
104
|
-
canvas
|
|
322
|
+
// Capture keeps drags flowing when the pointer leaves the canvas, but
|
|
323
|
+
// synthetic pointers (CDP/browser automation) have no capturable
|
|
324
|
+
// pointerId — setPointerCapture throws InvalidPointerId for them and
|
|
325
|
+
// would kill the tap before runtime.pointerDown runs. Losing capture
|
|
326
|
+
// only degrades off-canvas drags, so swallow the failure.
|
|
327
|
+
try {
|
|
328
|
+
canvas.setPointerCapture(event.pointerId);
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
/* not a capturable pointer — canvas-local move/up still arrive */
|
|
332
|
+
}
|
|
105
333
|
const p = canvasPoint(canvas, event, snapshot.program.width, snapshot.program.height);
|
|
106
334
|
runtime.pointerDown(p.x, p.y);
|
|
107
335
|
};
|
|
@@ -123,6 +351,11 @@ async function start() {
|
|
|
123
351
|
canvas.onpointercancel = pointerUp;
|
|
124
352
|
renderPinControls(snapshot.pinControls);
|
|
125
353
|
renderDiagnostics(snapshot);
|
|
354
|
+
wireDebugControls(canvas, snapshot.program.width, snapshot.program.height, (message) => {
|
|
355
|
+
extraDiagnostics.push(message);
|
|
356
|
+
renderDiagnostics(snapshot, extraDiagnostics);
|
|
357
|
+
}, () => drawDebugOverlay(snapshot.program.width, snapshot.program.height));
|
|
358
|
+
applyDebugCapture();
|
|
126
359
|
runtime.start();
|
|
127
360
|
}
|
|
128
361
|
void start().catch((error) => {
|