@typecad/cuttlefish 1.0.0-alpha.6 → 1.0.0-alpha.7
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/dist/api/shared/display-adapter.d.ts +2 -1
- package/dist/api/shared/display-adapter.js +8 -1
- package/dist/api/shared/display-adapters/sdl.js +9 -2
- package/dist/api/shared/display-profile.d.ts +20 -3
- package/dist/api/shared/display-profile.js +21 -6
- package/dist/api/shared/framework-manifest-registry.d.ts +9 -0
- package/dist/api/shared/framework-manifest-registry.js +25 -0
- package/dist/api/shared/framework-manifest.d.ts +462 -0
- package/dist/api/shared/framework-manifest.js +149 -0
- package/dist/api/shared/glcdfont.d.ts +12 -0
- package/dist/api/shared/glcdfont.js +124 -0
- package/dist/api/shared/graphics-strategy.d.ts +28 -0
- package/dist/api/shared/hal-op-ir.d.ts +418 -1
- package/dist/api/shared/hal-op-ir.js +95 -1
- package/dist/api/shared/index.d.ts +11 -1
- package/dist/api/shared/index.js +14 -0
- package/dist/api/shared/native-display-op-resolver.d.ts +10 -0
- package/dist/api/shared/native-display-op-resolver.js +64 -0
- package/dist/api/shared/platform-strategy.d.ts +9 -0
- package/dist/api/shared/promise-runtime.js +78 -0
- package/dist/api/shared/validate-framework-manifest.d.ts +28 -0
- package/dist/api/shared/validate-framework-manifest.js +417 -0
- package/dist/cli.js +109 -4
- package/dist/config-loader.js +20 -1
- package/dist/config-schema.d.ts +36 -36
- package/dist/create/board-spec.d.ts +4 -4
- package/dist/create/init-scaffold.d.ts +3 -0
- package/dist/create/init-scaffold.js +48 -0
- package/dist/create/init-templates.d.ts +2 -0
- package/dist/create/init-templates.js +28 -8
- package/dist/create/init-wizard.js +20 -1
- package/dist/emit/cpp-emitter.js +4 -3
- package/dist/emit/emitters/emitter-context.d.ts +5 -0
- package/dist/emit/emitters/function-emitter-impl.js +69 -59
- package/dist/emit/emitters/output-finalizer.d.ts +6 -0
- package/dist/emit/emitters/output-finalizer.js +21 -11
- package/dist/emit/emitters/setup.js +40 -0
- package/dist/emit/emitters/top-level-prep.js +2 -0
- package/dist/emit/emitters/ui-emitter.js +23 -8
- package/dist/emit/expression-renderer.js +10 -1
- package/dist/emit/snprintf-helpers.js +8 -0
- package/dist/emit/statement-renderer.js +13 -0
- package/dist/emit/utils/async-state-machine.js +185 -116
- package/dist/emit/utils/hal-op-cpp-type.d.ts +6 -0
- package/dist/emit/utils/hal-op-cpp-type.js +40 -0
- package/dist/ir/build-ir.js +5 -1
- package/dist/ir/expression-to-ir.js +14 -0
- package/dist/ir/feature-registry.js +7 -25
- package/dist/ir/hal/hal-emitter.d.ts +5 -2
- package/dist/ir/hal/hal-emitter.js +40 -12
- package/dist/ir/hal/hal-parser.d.ts +6 -0
- package/dist/ir/hal/hal-parser.js +74 -0
- package/dist/ir/hal/hal-plugins.js +561 -0
- package/dist/ir/identifier-collector.js +18 -0
- package/dist/ir/network-validation.d.ts +4 -0
- package/dist/ir/network-validation.js +184 -0
- package/dist/ir/ownership-analysis.js +14 -1
- package/dist/ir/program-analysis.d.ts +23 -0
- package/dist/ir/program-analysis.js +81 -2
- package/dist/ir/timing-validation.d.ts +6 -1
- package/dist/ir/timing-validation.js +50 -12
- package/dist/ir/transformers/expressions.js +58 -0
- package/dist/ir/transformers/hal-emit-helpers.js +1 -1
- package/dist/ir/transformers/variables.js +41 -3
- package/dist/ir/validation-orchestrator.js +3 -1
- package/dist/libdef/c-to-decl.d.ts +27 -0
- package/dist/libdef/c-to-decl.js +397 -0
- package/dist/libdef/component-decls.d.ts +2 -0
- package/dist/libdef/component-decls.js +6 -0
- package/dist/libdef/component-discovery.d.ts +43 -0
- package/dist/libdef/component-discovery.js +83 -0
- package/dist/libdef/cpp-to-decl.d.ts +9 -0
- package/dist/libdef/cpp-to-decl.js +72 -0
- package/dist/libdef/idf-discovery.d.ts +7 -0
- package/dist/libdef/idf-discovery.js +59 -0
- package/dist/libdef/registry.js +5 -2
- package/dist/lint-cache.d.ts +59 -0
- package/dist/lint-cache.js +257 -0
- package/dist/orchestrator/graph-builder.js +6 -2
- package/dist/stores/display-profile-store.d.ts +1 -0
- package/dist/stores/display-profile-store.js +1 -0
- package/dist/testing.d.ts +3 -1
- package/dist/testing.js +3 -1
- package/dist/transpile.d.ts +3 -0
- package/dist/transpile.js +72 -27
- package/dist/ui-hook.d.ts +17 -1
- package/dist/utils/cli.js +27 -1
- package/dist/utils/fs.d.ts +13 -0
- package/dist/utils/fs.js +50 -0
- package/package.json +8 -4
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover an ESP-IDF install root by checking $IDF_PATH first, then
|
|
3
|
+
* well-known install locations. Returns the absolute path if found and
|
|
4
|
+
* the `components/` subdir exists (the only thing gen-decls needs), or
|
|
5
|
+
* undefined otherwise.
|
|
6
|
+
*/
|
|
7
|
+
export declare function discoverIdfRootForGenDecls(): string | undefined;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Minimal IDF discovery for the gen-decls pre-transpile pass.
|
|
2
|
+
//
|
|
3
|
+
// We can't import framework-esp32's discovery from cuttlefish (it would
|
|
4
|
+
// create a package dependency from core → framework). This module mirrors
|
|
5
|
+
// the cheapest strategies from framework-esp32/src/toolchain/discover.ts:
|
|
6
|
+
// $IDF_PATH env var, well-known install locations, and a version-dir scan
|
|
7
|
+
// under <sysdrive>/esp/ (where the Espressif Windows installer puts
|
|
8
|
+
// `v6.0.2/esp-idf`, etc.). It is good enough to find an IDF install on a
|
|
9
|
+
// developer machine so the .d.ts cache can be populated before type-checking.
|
|
10
|
+
//
|
|
11
|
+
// For richer discovery (EIM manifest, PATH search) the framework-esp32
|
|
12
|
+
// compile path performs its own discovery at compile time and regenerates
|
|
13
|
+
// the cache — this minimal version is only for the pre-transpile pass.
|
|
14
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
const IS_WIN = process.platform === 'win32';
|
|
17
|
+
function wellKnownPaths() {
|
|
18
|
+
const paths = [];
|
|
19
|
+
if (IS_WIN) {
|
|
20
|
+
const sysDrive = process.env.SystemDrive ?? 'C:';
|
|
21
|
+
paths.push(join(`${sysDrive}\\esp`, 'esp-idf'), join(`${sysDrive}\\Espressif`, 'esp-idf'));
|
|
22
|
+
// Versioned subdir scan: <sysdrive>/esp/v*/esp-idf (Windows installer layout).
|
|
23
|
+
const espRoot = `${sysDrive}\\esp`;
|
|
24
|
+
if (existsSync(espRoot)) {
|
|
25
|
+
try {
|
|
26
|
+
for (const entry of readdirSync(espRoot)) {
|
|
27
|
+
if (/^v/i.test(entry)) {
|
|
28
|
+
paths.push(join(espRoot, entry, 'esp-idf'));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// readdir failed; skip.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
39
|
+
paths.push(join(home, 'esp', 'esp-idf'), join(home, '.espressif', 'esp-idf'), '/opt/esp/esp-idf');
|
|
40
|
+
}
|
|
41
|
+
return paths;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Discover an ESP-IDF install root by checking $IDF_PATH first, then
|
|
45
|
+
* well-known install locations. Returns the absolute path if found and
|
|
46
|
+
* the `components/` subdir exists (the only thing gen-decls needs), or
|
|
47
|
+
* undefined otherwise.
|
|
48
|
+
*/
|
|
49
|
+
export function discoverIdfRootForGenDecls() {
|
|
50
|
+
const candidates = [
|
|
51
|
+
process.env.IDF_PATH,
|
|
52
|
+
...wellKnownPaths(),
|
|
53
|
+
].filter((p) => typeof p === 'string' && p.length > 0);
|
|
54
|
+
for (const p of candidates) {
|
|
55
|
+
if (existsSync(join(p, 'components')))
|
|
56
|
+
return p;
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
package/dist/libdef/registry.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { listFilesRecursive, readText } from "../utils/fs.js";
|
|
4
4
|
import { toModuleKey, toPascalCase } from "../utils/strings.js";
|
|
5
5
|
import { getLoadedFramework, hasLoadedFramework } from "../framework-registry.js";
|
|
6
6
|
function toArchitectureFromFqbn(fqbn) {
|
|
@@ -21,7 +21,10 @@ function getLibraryResolver() {
|
|
|
21
21
|
}
|
|
22
22
|
export function loadLibraryDefinitions(definitionsDir) {
|
|
23
23
|
const registry = new Map();
|
|
24
|
-
|
|
24
|
+
// Recursive scan: libdefs may live at the entry dir (single-level convention)
|
|
25
|
+
// or nested under cache trees like `.cuttlefish/component-decls/<component>/`
|
|
26
|
+
// (per-component overrides generated alongside .d.ts stubs).
|
|
27
|
+
const files = listFilesRecursive(definitionsDir, ".libdef.json");
|
|
25
28
|
for (const filePath of files) {
|
|
26
29
|
let def;
|
|
27
30
|
try {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent cache for the ESLint build gate.
|
|
3
|
+
*
|
|
4
|
+
* ESLint is a mandatory correctness gate: it excludes non-AOT code patterns
|
|
5
|
+
* that the transpiler cannot accept. Running it on every `cuttlefish build`
|
|
6
|
+
* costs ~3s (38% of a small-project build) and almost always finds nothing on
|
|
7
|
+
* repeat runs. Its result (zero errors) is a whole-program boolean that
|
|
8
|
+
* depends only on a small set of inputs, so it is cleanly cacheable.
|
|
9
|
+
*
|
|
10
|
+
* Soundness contract:
|
|
11
|
+
* - The cache ONLY records a successful (zero-error) lint result.
|
|
12
|
+
* - A failing lint never persists (the build aborts anyway).
|
|
13
|
+
* - On ANY input change the entry is invalidated and ESLint runs for real.
|
|
14
|
+
* - `options.force` and the `CUTTLEFISH_NO_CACHE` env var bypass entirely.
|
|
15
|
+
*
|
|
16
|
+
* This mirrors the historical `.cuttlefish-cache.json` timestamp+hash cache
|
|
17
|
+
* that used to live in this package. It is scoped to the ESLint gate (and the
|
|
18
|
+
* type-check gate) rather than the transpile-IR pass, because — unlike IR
|
|
19
|
+
* lowering — these gates' outcomes are whole-program booleans with no
|
|
20
|
+
* cross-module rehydration requirement.
|
|
21
|
+
*/
|
|
22
|
+
/** Resolve the eslint config the same way runEslintCheck does, to avoid drift. */
|
|
23
|
+
export declare function resolveEslintConfigPath(projectRoot: string): string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Compute the inputs that the ESLint result depends on. Two builds with the
|
|
26
|
+
* same fingerprint are guaranteed to produce the same lint outcome.
|
|
27
|
+
*/
|
|
28
|
+
export interface LintFingerprint {
|
|
29
|
+
digest: string;
|
|
30
|
+
/** Absolute paths that were hashed into the digest (for debugging). */
|
|
31
|
+
inputs: string[];
|
|
32
|
+
}
|
|
33
|
+
export declare function computeLintFingerprint(projectRoot: string, srcDir: string): LintFingerprint | null;
|
|
34
|
+
export interface GateCacheResult {
|
|
35
|
+
/** True when the gate can be skipped because the recorded success still holds. */
|
|
36
|
+
hit: boolean;
|
|
37
|
+
/** The fingerprint to record after a successful gate run. */
|
|
38
|
+
fingerprint: LintFingerprint | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Decide whether the ESLint gate can be skipped for this project.
|
|
42
|
+
*
|
|
43
|
+
* Returns `hit: true` only when:
|
|
44
|
+
* - caching is not disabled, not force-bypassed,
|
|
45
|
+
* - a fingerprint can be computed (config + eslint resolvable), and
|
|
46
|
+
* - the on-disk cache records a success for that exact fingerprint.
|
|
47
|
+
*
|
|
48
|
+
* `force` mirrors TranspileOptions.force and bypasses the cache.
|
|
49
|
+
*/
|
|
50
|
+
export declare function checkLintCache(projectRoot: string, srcDir: string, opts?: {
|
|
51
|
+
force?: boolean;
|
|
52
|
+
}): GateCacheResult;
|
|
53
|
+
/**
|
|
54
|
+
* Record a successful ESLint run (zero errors). Never call this after a
|
|
55
|
+
* failing run — a failing build must not persist a "clean" marker.
|
|
56
|
+
*/
|
|
57
|
+
export declare function recordLintSuccess(projectRoot: string, fingerprint: LintFingerprint): void;
|
|
58
|
+
/** Drop the lint entry (used when the gate is skipped entirely / nothing to cache). */
|
|
59
|
+
export declare function invalidateLint(projectRoot: string): void;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent cache for the ESLint build gate.
|
|
3
|
+
*
|
|
4
|
+
* ESLint is a mandatory correctness gate: it excludes non-AOT code patterns
|
|
5
|
+
* that the transpiler cannot accept. Running it on every `cuttlefish build`
|
|
6
|
+
* costs ~3s (38% of a small-project build) and almost always finds nothing on
|
|
7
|
+
* repeat runs. Its result (zero errors) is a whole-program boolean that
|
|
8
|
+
* depends only on a small set of inputs, so it is cleanly cacheable.
|
|
9
|
+
*
|
|
10
|
+
* Soundness contract:
|
|
11
|
+
* - The cache ONLY records a successful (zero-error) lint result.
|
|
12
|
+
* - A failing lint never persists (the build aborts anyway).
|
|
13
|
+
* - On ANY input change the entry is invalidated and ESLint runs for real.
|
|
14
|
+
* - `options.force` and the `CUTTLEFISH_NO_CACHE` env var bypass entirely.
|
|
15
|
+
*
|
|
16
|
+
* This mirrors the historical `.cuttlefish-cache.json` timestamp+hash cache
|
|
17
|
+
* that used to live in this package. It is scoped to the ESLint gate (and the
|
|
18
|
+
* type-check gate) rather than the transpile-IR pass, because — unlike IR
|
|
19
|
+
* lowering — these gates' outcomes are whole-program booleans with no
|
|
20
|
+
* cross-module rehydration requirement.
|
|
21
|
+
*/
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import crypto from "node:crypto";
|
|
25
|
+
import { createRequire } from "node:module";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const CACHE_VERSION = 2;
|
|
30
|
+
const DEFAULT_CACHE_NAME = ".cuttlefish-cache.json";
|
|
31
|
+
/** Bypass the cache entirely when set (debugging / CI cold runs). */
|
|
32
|
+
function isCacheDisabled() {
|
|
33
|
+
return process.env.CUTTLEFISH_NO_CACHE === "1" || process.env.CUTTLEFISH_NO_CACHE === "true";
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Hash a file's content. Returns null if the file cannot be read (treated as
|
|
37
|
+
* "changed" by callers because the input set is no longer what we recorded).
|
|
38
|
+
*/
|
|
39
|
+
function hashFile(absPath) {
|
|
40
|
+
try {
|
|
41
|
+
const content = fs.readFileSync(absPath, "utf8");
|
|
42
|
+
return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Hash size + mtime for a file. Used for inputs (eslint config, eslint package)
|
|
50
|
+
* that we don't want to fully read on every build, and whose identity is
|
|
51
|
+
* sufficiently captured by size+mtime. Returns null if the file is missing.
|
|
52
|
+
*/
|
|
53
|
+
function fingerprintStat(absPath) {
|
|
54
|
+
try {
|
|
55
|
+
const stat = fs.statSync(absPath);
|
|
56
|
+
return `${stat.size}:${stat.mtimeMs}`;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Transpiler-self fingerprint: invalidate every cache entry when the
|
|
64
|
+
* transpiler's own compiled sources change. Mirrors the historical
|
|
65
|
+
* computeToolchainFingerprint() in incremental-cache.ts.
|
|
66
|
+
*/
|
|
67
|
+
let cachedToolchainFingerprint;
|
|
68
|
+
function computeToolchainFingerprint() {
|
|
69
|
+
if (cachedToolchainFingerprint !== undefined)
|
|
70
|
+
return cachedToolchainFingerprint;
|
|
71
|
+
const candidates = [
|
|
72
|
+
"transpile.js",
|
|
73
|
+
"lint-cache.js",
|
|
74
|
+
"eslint-check.js",
|
|
75
|
+
path.join("emit", "cpp-emitter.js"),
|
|
76
|
+
path.join("ir", "build-ir.js"),
|
|
77
|
+
path.join("ir", "ownership-analysis.js"),
|
|
78
|
+
path.join("ir", "validation-orchestrator.js"),
|
|
79
|
+
];
|
|
80
|
+
const signature = candidates
|
|
81
|
+
.map((rel) => path.join(__dirname, rel))
|
|
82
|
+
.filter((p) => fs.existsSync(p))
|
|
83
|
+
.map((p) => {
|
|
84
|
+
const stat = fs.statSync(p);
|
|
85
|
+
return `${path.basename(p)}:${stat.size}:${stat.mtimeMs}`;
|
|
86
|
+
})
|
|
87
|
+
.join("|");
|
|
88
|
+
cachedToolchainFingerprint = crypto.createHash("sha256").update(signature).digest("hex").slice(0, 16);
|
|
89
|
+
return cachedToolchainFingerprint;
|
|
90
|
+
}
|
|
91
|
+
/** Resolve the eslint config the same way runEslintCheck does, to avoid drift. */
|
|
92
|
+
export function resolveEslintConfigPath(projectRoot) {
|
|
93
|
+
const cuttlefishConfig = path.join(projectRoot, ".cuttlefish", "eslint.config.mjs");
|
|
94
|
+
if (fs.existsSync(cuttlefishConfig))
|
|
95
|
+
return cuttlefishConfig;
|
|
96
|
+
for (const name of ["eslint.config.mjs", "eslint.config.js", "eslint.config.cjs"]) {
|
|
97
|
+
const candidate = path.join(projectRoot, name);
|
|
98
|
+
if (fs.existsSync(candidate))
|
|
99
|
+
return candidate;
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
/** Resolve the eslint package version that runEslintCheck would load. */
|
|
104
|
+
function resolveEslintIdentity(projectRoot) {
|
|
105
|
+
const resolvers = [
|
|
106
|
+
createRequire(path.join(projectRoot, "package.json")),
|
|
107
|
+
require,
|
|
108
|
+
];
|
|
109
|
+
for (const r of resolvers) {
|
|
110
|
+
try {
|
|
111
|
+
const eslintPath = r.resolve("eslint");
|
|
112
|
+
const pkgPath = r.resolve("eslint/package.json");
|
|
113
|
+
const version = JSON.parse(fs.readFileSync(pkgPath, "utf8")).version ?? "unknown";
|
|
114
|
+
return { path: eslintPath, version };
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// try next resolver
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
/** Recursively gather .ts/.tsx/.ui files under a directory (the lint input set). */
|
|
123
|
+
function gatherSourceFiles(srcDir) {
|
|
124
|
+
const out = [];
|
|
125
|
+
if (!fs.existsSync(srcDir) || !fs.statSync(srcDir).isDirectory())
|
|
126
|
+
return out;
|
|
127
|
+
const stack = [srcDir];
|
|
128
|
+
while (stack.length > 0) {
|
|
129
|
+
const dir = stack.pop();
|
|
130
|
+
let entries;
|
|
131
|
+
try {
|
|
132
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
const full = path.join(dir, entry.name);
|
|
139
|
+
if (entry.isDirectory()) {
|
|
140
|
+
if (entry.name === "node_modules" || entry.name === "out" || entry.name.startsWith("out-"))
|
|
141
|
+
continue;
|
|
142
|
+
stack.push(full);
|
|
143
|
+
}
|
|
144
|
+
else if (entry.isFile()) {
|
|
145
|
+
const ext = entry.name.toLowerCase();
|
|
146
|
+
if (ext.endsWith(".ts") || ext.endsWith(".tsx") || ext.endsWith(".ui")) {
|
|
147
|
+
out.push(full);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
out.sort();
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
export function computeLintFingerprint(projectRoot, srcDir) {
|
|
156
|
+
const configPath = resolveEslintConfigPath(projectRoot);
|
|
157
|
+
if (!configPath)
|
|
158
|
+
return null; // no config => runEslintCheck returns [] without loading eslint
|
|
159
|
+
const eslint = resolveEslintIdentity(projectRoot);
|
|
160
|
+
if (!eslint)
|
|
161
|
+
return null; // eslint unresolvable => runEslintCheck returns []
|
|
162
|
+
const parts = [];
|
|
163
|
+
parts.push(["toolchain", computeToolchainFingerprint()]);
|
|
164
|
+
parts.push(["eslint", `${eslint.version}@${eslint.path}`]);
|
|
165
|
+
parts.push(["config", `${configPath}:${fingerprintStat(configPath) ?? "missing"}`]);
|
|
166
|
+
const sources = gatherSourceFiles(srcDir);
|
|
167
|
+
for (const f of sources) {
|
|
168
|
+
const h = hashFile(f);
|
|
169
|
+
parts.push(["src", `${f}:${h ?? "missing"}`]);
|
|
170
|
+
}
|
|
171
|
+
const digest = crypto
|
|
172
|
+
.createHash("sha256")
|
|
173
|
+
.update(parts.map((p) => p.join("=")).join("\n"))
|
|
174
|
+
.digest("hex")
|
|
175
|
+
.slice(0, 32);
|
|
176
|
+
return { digest, inputs: parts.map((p) => p[1]) };
|
|
177
|
+
}
|
|
178
|
+
function cachePathFor(projectRoot) {
|
|
179
|
+
return path.join(projectRoot, DEFAULT_CACHE_NAME);
|
|
180
|
+
}
|
|
181
|
+
function loadCacheFile(projectRoot) {
|
|
182
|
+
const cachePath = cachePathFor(projectRoot);
|
|
183
|
+
try {
|
|
184
|
+
if (!fs.existsSync(cachePath))
|
|
185
|
+
return null;
|
|
186
|
+
const data = JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
187
|
+
if (data.version === CACHE_VERSION &&
|
|
188
|
+
data.toolchainFingerprint === computeToolchainFingerprint() &&
|
|
189
|
+
path.resolve(data.rootDir) === path.resolve(projectRoot)) {
|
|
190
|
+
return data;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// Corrupt or unreadable — treat as empty.
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
function saveCacheFile(projectRoot, data) {
|
|
199
|
+
data.updatedAt = Date.now();
|
|
200
|
+
try {
|
|
201
|
+
fs.writeFileSync(cachePathFor(projectRoot), JSON.stringify(data, null, 2), "utf8");
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// Non-fatal: caching is best-effort. Next build just re-runs the gate.
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function freshCacheFile(projectRoot) {
|
|
208
|
+
return {
|
|
209
|
+
version: CACHE_VERSION,
|
|
210
|
+
toolchainFingerprint: computeToolchainFingerprint(),
|
|
211
|
+
rootDir: path.resolve(projectRoot),
|
|
212
|
+
gates: {},
|
|
213
|
+
createdAt: Date.now(),
|
|
214
|
+
updatedAt: Date.now(),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Decide whether the ESLint gate can be skipped for this project.
|
|
219
|
+
*
|
|
220
|
+
* Returns `hit: true` only when:
|
|
221
|
+
* - caching is not disabled, not force-bypassed,
|
|
222
|
+
* - a fingerprint can be computed (config + eslint resolvable), and
|
|
223
|
+
* - the on-disk cache records a success for that exact fingerprint.
|
|
224
|
+
*
|
|
225
|
+
* `force` mirrors TranspileOptions.force and bypasses the cache.
|
|
226
|
+
*/
|
|
227
|
+
export function checkLintCache(projectRoot, srcDir, opts = {}) {
|
|
228
|
+
const fingerprint = computeLintFingerprint(projectRoot, srcDir);
|
|
229
|
+
if (fingerprint === null)
|
|
230
|
+
return { hit: false, fingerprint: null };
|
|
231
|
+
if (opts.force || isCacheDisabled())
|
|
232
|
+
return { hit: false, fingerprint };
|
|
233
|
+
const data = loadCacheFile(projectRoot);
|
|
234
|
+
if (!data)
|
|
235
|
+
return { hit: false, fingerprint };
|
|
236
|
+
const entry = data.gates.lint;
|
|
237
|
+
return { hit: entry?.digest === fingerprint.digest, fingerprint };
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Record a successful ESLint run (zero errors). Never call this after a
|
|
241
|
+
* failing run — a failing build must not persist a "clean" marker.
|
|
242
|
+
*/
|
|
243
|
+
export function recordLintSuccess(projectRoot, fingerprint) {
|
|
244
|
+
if (isCacheDisabled())
|
|
245
|
+
return;
|
|
246
|
+
const data = loadCacheFile(projectRoot) ?? freshCacheFile(projectRoot);
|
|
247
|
+
data.gates.lint = { digest: fingerprint.digest, recordedAt: Date.now() };
|
|
248
|
+
saveCacheFile(projectRoot, data);
|
|
249
|
+
}
|
|
250
|
+
/** Drop the lint entry (used when the gate is skipped entirely / nothing to cache). */
|
|
251
|
+
export function invalidateLint(projectRoot) {
|
|
252
|
+
const data = loadCacheFile(projectRoot);
|
|
253
|
+
if (data && data.gates.lint) {
|
|
254
|
+
delete data.gates.lint;
|
|
255
|
+
saveCacheFile(projectRoot, data);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -167,14 +167,18 @@ export function collectTranspileGraph(entryFile, boardPackage) {
|
|
|
167
167
|
if (moduleSpecifier === "@typecad/ui" || moduleSpecifier === "@typecad/ui") {
|
|
168
168
|
continue;
|
|
169
169
|
}
|
|
170
|
-
// Skip @typecad/board, @typecad/board-*, @typecad/mcu-*,
|
|
171
|
-
// @typecad/framework-* — these packages ship src/ for HAL metadata
|
|
170
|
+
// Skip @typecad/board, @typecad/board-*, @typecad/mcu-*, @typecad/hal,
|
|
171
|
+
// and @typecad/framework-* — these packages ship src/ for HAL metadata
|
|
172
172
|
// introspection (hal-parser.ts, board-resolver.ts) but their source
|
|
173
173
|
// must NOT be transpiled to C++. The HAL resolver loads class/method
|
|
174
174
|
// metadata from these files separately; emitting them as C++ produces
|
|
175
175
|
// thousands of lines of stub functions (board(), gpioWrite(), etc.)
|
|
176
176
|
// and pulls in unsupported types (Promise, variant, Object.freeze).
|
|
177
|
+
// Skipping @typecad/hal is especially important: its 28 source files
|
|
178
|
+
// (gpio.ts, i2c.ts, spi.ts, etc.) were all walked through full
|
|
179
|
+
// buildProgramIR, adding ~40 seconds to every transpile.
|
|
177
180
|
if (moduleSpecifier === "@typecad/board"
|
|
181
|
+
|| moduleSpecifier === "@typecad/hal"
|
|
178
182
|
|| moduleSpecifier.startsWith("@typecad/board-")
|
|
179
183
|
|| moduleSpecifier.startsWith("@typecad/mcu-")
|
|
180
184
|
|| moduleSpecifier.startsWith("@typecad/framework-")) {
|
|
@@ -9,6 +9,7 @@ export declare function setDisplayProfile(profile: DisplayProfile, wiring: {
|
|
|
9
9
|
address?: number;
|
|
10
10
|
reset?: number;
|
|
11
11
|
buildTarget?: string;
|
|
12
|
+
psram?: boolean;
|
|
12
13
|
}): void;
|
|
13
14
|
/** Get the current display profile, or a default if none set. */
|
|
14
15
|
export declare function getDisplayProfile(): ResolvedDisplay;
|
|
@@ -20,6 +20,7 @@ export function setDisplayProfile(profile, wiring) {
|
|
|
20
20
|
_mountAddress: wiring.address ?? 0x3C,
|
|
21
21
|
_mountReset: wiring.reset ?? -1,
|
|
22
22
|
_buildTarget: wiring.buildTarget,
|
|
23
|
+
_psram: wiring.psram,
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
/** Get the current display profile, or a default if none set. */
|
package/dist/testing.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export { LINT_RULES, ESLINT_OPT_OUT_KINDS, kindRegistryEntries, } from "./ir/fea
|
|
|
30
30
|
export type { LintRule, FeatureEntry, FeatureStatus, DiagnosticMatch } from "./ir/feature-registry.js";
|
|
31
31
|
export { runEslintCheck } from "./eslint-check.js";
|
|
32
32
|
export type { ESLintError } from "./eslint-check.js";
|
|
33
|
+
export { checkLintCache, recordLintSuccess, invalidateLint, computeLintFingerprint, resolveEslintConfigPath, } from "./lint-cache.js";
|
|
34
|
+
export type { LintFingerprint, GateCacheResult } from "./lint-cache.js";
|
|
33
35
|
export { runSemanticGates } from "./orchestrator/type-checker.js";
|
|
34
36
|
export { canonicalize, buildSemanticFacts, } from "./orchestrator/semantic-facts.js";
|
|
35
37
|
export type { CanonicalType, SemanticFacts, FactStore, ValueCategory, Lifetime, Nullable, SemanticOrigin, AnalysisResult, BindingResolver, } from "./orchestrator/semantic-facts.js";
|
|
@@ -40,7 +42,7 @@ export { parseCommandLine } from "./utils/cli.js";
|
|
|
40
42
|
export { parseHeader, stripPreprocessorBlocks } from "./libdef/header-parser.js";
|
|
41
43
|
export { BaseClassResolver, buildClassIndex } from "./libdef/base-class-resolver.js";
|
|
42
44
|
export type { ResolveResult } from "./libdef/base-class-resolver.js";
|
|
43
|
-
export { generateDecl, generateDeclsForDirectory } from "./libdef/cpp-to-decl.js";
|
|
45
|
+
export { generateDecl, generateDeclsForDirectory, generateComponentDeclsForProject } from "./libdef/cpp-to-decl.js";
|
|
44
46
|
export { renderExprAsText } from "./ir/render-expr.js";
|
|
45
47
|
export { contextStorage, CompilationContext } from "./ir/build-ir-state.js";
|
|
46
48
|
export type { ExpressionIR } from "./api/index.js";
|
package/dist/testing.js
CHANGED
|
@@ -34,6 +34,8 @@ export { prescanUnsupportedFeatures } from "./ir/feature-prescan.js";
|
|
|
34
34
|
export { LINT_RULES, ESLINT_OPT_OUT_KINDS, kindRegistryEntries, } from "./ir/feature-registry.js";
|
|
35
35
|
// ── ESLint gate ──────────────────────────────────────────────────────────────
|
|
36
36
|
export { runEslintCheck } from "./eslint-check.js";
|
|
37
|
+
// ── ESLint gate cache ────────────────────────────────────────────────────────
|
|
38
|
+
export { checkLintCache, recordLintSuccess, invalidateLint, computeLintFingerprint, resolveEslintConfigPath, } from "./lint-cache.js";
|
|
37
39
|
// ── Semantic gates (TypeChecker-based) ──────────────────────────────────────
|
|
38
40
|
export { runSemanticGates } from "./orchestrator/type-checker.js";
|
|
39
41
|
export { canonicalize, buildSemanticFacts, } from "./orchestrator/semantic-facts.js";
|
|
@@ -43,7 +45,7 @@ export { discoverWatchDirs, isRelevantChange } from "./watch.js";
|
|
|
43
45
|
export { parseCommandLine } from "./utils/cli.js";
|
|
44
46
|
export { parseHeader, stripPreprocessorBlocks } from "./libdef/header-parser.js";
|
|
45
47
|
export { BaseClassResolver, buildClassIndex } from "./libdef/base-class-resolver.js";
|
|
46
|
-
export { generateDecl, generateDeclsForDirectory } from "./libdef/cpp-to-decl.js";
|
|
48
|
+
export { generateDecl, generateDeclsForDirectory, generateComponentDeclsForProject } from "./libdef/cpp-to-decl.js";
|
|
47
49
|
// ── IR rendering internals (for fail-closed regression tests) ───────────────
|
|
48
50
|
export { renderExprAsText } from "./ir/render-expr.js";
|
|
49
51
|
export { contextStorage, CompilationContext } from "./ir/build-ir-state.js";
|
package/dist/transpile.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import { GenerateLibdefOptions, GeneratedOutputs, TranspileOptions } from "./types.js";
|
|
2
|
+
import { loadFrameworkPackage } from "./framework-package.js";
|
|
3
|
+
export { loadFrameworkPackage };
|
|
4
|
+
export { getLoadedFramework, hasLoadedFramework } from "./framework-registry.js";
|
|
2
5
|
export declare function transpileFile(options: TranspileOptions): Promise<GeneratedOutputs>;
|
|
3
6
|
export declare function generateLibraryDefinitions(options: GenerateLibdefOptions): string[];
|
package/dist/transpile.js
CHANGED
|
@@ -19,7 +19,7 @@ import { clearCaches, } from "./cache.js";
|
|
|
19
19
|
import { detectEntryPoints, detectExportedEntryPoints } from "./ir/entry-points.js";
|
|
20
20
|
import { analyzeReachability } from "./ir/reachability.js";
|
|
21
21
|
import { filterProgramIR } from "./ir/filter.js";
|
|
22
|
-
import { setActiveStrategy } from "./ir/hal-resolver.js";
|
|
22
|
+
import { setActiveStrategy, loadHALModules } from "./ir/hal-resolver.js";
|
|
23
23
|
import { CompilationContext, contextStorage } from "./ir/build-ir-state.js";
|
|
24
24
|
import { buildSymbolTable, mergeSymbolTable, resolveInheritance, createSymbolTable } from "./ir/symbol-table.js";
|
|
25
25
|
import { loadBreakpoints, preprocess as debugPreprocess } from "./debug/index.js";
|
|
@@ -28,6 +28,7 @@ import { typeCheckFiles } from "./orchestrator/type-checker.js";
|
|
|
28
28
|
import { runSemanticGates } from "./orchestrator/type-checker.js";
|
|
29
29
|
import { autoGenerateMissingDecls } from "./orchestrator/dts-generator.js";
|
|
30
30
|
import { runEslintCheck, printEslintErrors } from "./eslint-check.js";
|
|
31
|
+
import { checkLintCache, recordLintSuccess } from "./lint-cache.js";
|
|
31
32
|
import { initProfiler, getProfiler } from "./profiler/index.js";
|
|
32
33
|
import { buildDiagnosticsReport, writeDiagnosticsReport } from "./diagnostics/diagnostics-report.js";
|
|
33
34
|
import { resolveImport, } from "./transpile/resolution.js";
|
|
@@ -47,17 +48,12 @@ function loadExpectPreprocessor() {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
function cleanOutput(_entryDir, outDir) {
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
catch (e) {
|
|
58
|
-
if (process.env.CUTTLEFISH_DEBUG)
|
|
59
|
-
console.error("[transpile] Failed to clean output dir:", e);
|
|
60
|
-
}
|
|
51
|
+
// Preserved for incremental-build support: writeText now skips writing when
|
|
52
|
+
// content is identical, so keeping the existing output dir intact lets
|
|
53
|
+
// downstream build tools (idf.py/ninja, arduino-cli) reuse their build
|
|
54
|
+
// caches. Stale files from removed source modules are harmless — they're
|
|
55
|
+
// not referenced by the current entry file and won't be compiled.
|
|
56
|
+
// The output dir is still created (via writeText → ensureDir) on first run.
|
|
61
57
|
}
|
|
62
58
|
/**
|
|
63
59
|
* Auto-generates .d.ts files for C++ modules that are missing declarations.
|
|
@@ -131,6 +127,8 @@ function applyTreeShaking(programIR, target, treeShakingOptions) {
|
|
|
131
127
|
import { isStringEnum } from "./api/shared/index.js";
|
|
132
128
|
import { resolveStrategy } from "./platform/registry.js";
|
|
133
129
|
import { loadFrameworkPackage } from "./framework-package.js";
|
|
130
|
+
export { loadFrameworkPackage };
|
|
131
|
+
export { getLoadedFramework, hasLoadedFramework } from "./framework-registry.js";
|
|
134
132
|
import { getLoadedFramework, hasLoadedFramework } from "./framework-registry.js";
|
|
135
133
|
function formatFatalDiagnostics(entries) {
|
|
136
134
|
const errors = entries.filter(({ diagnostic }) => diagnostic.severity === "error");
|
|
@@ -242,16 +240,33 @@ export async function transpileFile(options) {
|
|
|
242
240
|
// Load built-in profiles from the framework package via its exported path
|
|
243
241
|
const registry = new Map();
|
|
244
242
|
if (options.frameworkPackage) {
|
|
243
|
+
// Built-in display profiles live in the framework's displays/ili9341-spi
|
|
244
|
+
// module. Non-Arduino frameworks (avr, esp32) may not ship their own
|
|
245
|
+
// profile registry — fall back to framework-arduino, which all current
|
|
246
|
+
// frameworks depend on and which owns the canonical profile definitions.
|
|
245
247
|
const profileMod = await import(options.frameworkPackage + "/displays/ili9341-spi").catch(() => null);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
248
|
+
// The fallback uses a non-literal specifier so tsc does not require
|
|
249
|
+
// framework-arduino to be a build-time dependency (it is an optional
|
|
250
|
+
// runtime fallback — frameworks like avr/esp32 may not ship their own
|
|
251
|
+
// profile registry). Declaring it as a dependency would create a cycle
|
|
252
|
+
// (framework-arduino already depends on cuttlefish).
|
|
253
|
+
const fallbackPkg = "@typecad/framework-arduino";
|
|
254
|
+
const fallbackMod = (options.frameworkPackage !== fallbackPkg)
|
|
255
|
+
? await import(fallbackPkg + "/displays/ili9341-spi").catch(() => null)
|
|
256
|
+
: null;
|
|
257
|
+
for (const mod of [profileMod, fallbackMod]) {
|
|
258
|
+
if (mod?.BUILT_IN_PROFILES) {
|
|
259
|
+
for (const [k, v] of Object.entries(mod.BUILT_IN_PROFILES)) {
|
|
260
|
+
registry.set(k, v);
|
|
261
|
+
}
|
|
249
262
|
}
|
|
250
263
|
}
|
|
251
264
|
}
|
|
252
265
|
const resolved = resolveDisplayProfile(configDisplay, registry);
|
|
253
266
|
const buildTarget = options.platformContext?.frameworkData?.buildTarget;
|
|
254
|
-
|
|
267
|
+
const psramRaw = options.platformContext?.frameworkData?.psram;
|
|
268
|
+
const psram = psramRaw === 'opi' || psramRaw === 'quad';
|
|
269
|
+
setDisplayProfile(resolved.profile, { cs: resolved.cs, dc: resolved.dc, rst: resolved.rst, bus: resolved.bus, address: resolved.address, reset: resolved.reset, buildTarget, psram });
|
|
255
270
|
}
|
|
256
271
|
catch {
|
|
257
272
|
// Fall back to default profile — not fatal
|
|
@@ -302,22 +317,43 @@ export async function transpileFile(options) {
|
|
|
302
317
|
}
|
|
303
318
|
// ── ESLint gate ──────────────────────────────────────────────────────────
|
|
304
319
|
// ESLint catches what the type-checker cannot (idiom violations, banned
|
|
305
|
-
// globals, explicit `any`, etc.).
|
|
306
|
-
//
|
|
320
|
+
// globals, explicit `any`, etc.). It is a mandatory correctness gate: it
|
|
321
|
+
// excludes non-AOT code patterns the transpiler cannot accept, so it cannot
|
|
322
|
+
// be dropped. Errors abort the build, mirroring the type-check behavior
|
|
323
|
+
// above. Skipped alongside type-checking when disabled.
|
|
324
|
+
//
|
|
325
|
+
// Because the lint result is a whole-program boolean that depends only on
|
|
326
|
+
// the source files, the eslint config, and the eslint/transpiler versions,
|
|
327
|
+
// it is cacheable. On a cache hit we skip the ~3s ESLint run entirely; on a
|
|
328
|
+
// miss we run ESLint and, only if clean, persist the result. See lint-cache.ts
|
|
329
|
+
// for the soundness contract.
|
|
307
330
|
if (options.skipLint !== true && options.skipTypeCheck !== true && transpileFiles.length > 0) {
|
|
308
|
-
profiler.startTimer("lint:eslint");
|
|
309
331
|
// The eslint config lives at the project root (next to cuttlefish.config.ts),
|
|
310
332
|
// not under src/. Fall back to entryDir for ad-hoc API/test callers that pass
|
|
311
333
|
// a bare input file without a configured project.
|
|
312
334
|
const eslintRoot = options.projectRoot ?? entryDir;
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
335
|
+
const lintCache = checkLintCache(eslintRoot, sourceDir, { force: options.force });
|
|
336
|
+
if (lintCache.hit) {
|
|
337
|
+
// Cache hit: previous clean run still applies, skip ESLint entirely.
|
|
338
|
+
profiler.startTimer("lint:eslint:cached");
|
|
339
|
+
profiler.endTimer("lint:eslint:cached");
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
profiler.startTimer("lint:eslint");
|
|
343
|
+
const eslintErrors = await runEslintCheck(eslintRoot);
|
|
344
|
+
profiler.endTimer("lint:eslint");
|
|
345
|
+
if (eslintErrors.length > 0) {
|
|
346
|
+
// Abort with a formatted message. The structured-diagnostic channel is
|
|
347
|
+
// not populated here because a thrown error discards the output anyway;
|
|
348
|
+
// printEslintErrors gives the user file/line/column/caret directly.
|
|
349
|
+
// Do NOT persist a cache entry for a failing run.
|
|
350
|
+
printEslintErrors(eslintErrors);
|
|
351
|
+
throw new Error(`ESLint reported ${eslintErrors.length} error${eslintErrors.length === 1 ? "" : "s"} — transpilation aborted.`);
|
|
352
|
+
}
|
|
353
|
+
// Clean run: record the fingerprint so subsequent unchanged builds skip.
|
|
354
|
+
if (lintCache.fingerprint) {
|
|
355
|
+
recordLintSuccess(eslintRoot, lintCache.fingerprint);
|
|
356
|
+
}
|
|
321
357
|
}
|
|
322
358
|
}
|
|
323
359
|
const npmPackages = graphResult.npmPackages;
|
|
@@ -459,6 +495,15 @@ export async function transpileFile(options) {
|
|
|
459
495
|
};
|
|
460
496
|
profiler.startTimer("ir:build-all");
|
|
461
497
|
profiler.captureMemorySnapshot("ir:pre-build");
|
|
498
|
+
// Load + parse the @typecad/hal source files ONCE for this transpile run.
|
|
499
|
+
// buildProgramIR used to force-reload HAL per graph file (O(files) re-reads
|
|
500
|
+
// and re-parses of all 28 HAL modules); warming it here makes the per-file
|
|
501
|
+
// loadHALModules() call inside buildProgramIR a cheap no-op. A fresh run of
|
|
502
|
+
// transpileFile always reaches this point, so edits to @typecad/hal source
|
|
503
|
+
// are picked up on the next build.
|
|
504
|
+
profiler.startTimer("ir:load-hal");
|
|
505
|
+
loadHALModules(true);
|
|
506
|
+
profiler.endTimer("ir:load-hal");
|
|
462
507
|
const rawIRArray = await Promise.all(filesToProcess.map(buildRawIR));
|
|
463
508
|
profiler.captureMemorySnapshot("ir:post-build");
|
|
464
509
|
profiler.endTimer("ir:build-all");
|