@telorun/kernel 0.79.0 → 0.81.0
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/bundle/module-artifact.d.ts +13 -1
- package/dist/bundle/module-artifact.d.ts.map +1 -1
- package/dist/bundle/module-artifact.js +22 -3
- package/dist/bundle/module-artifact.js.map +1 -1
- package/dist/controller-loader.js +1 -1
- package/dist/controller-loader.js.map +1 -1
- package/dist/controller-loaders/napi-loader.d.ts +20 -0
- package/dist/controller-loaders/napi-loader.d.ts.map +1 -1
- package/dist/controller-loaders/napi-loader.js +31 -4
- package/dist/controller-loaders/napi-loader.js.map +1 -1
- package/dist/controller-loaders/npm-install-root.d.ts +56 -0
- package/dist/controller-loaders/npm-install-root.d.ts.map +1 -0
- package/dist/controller-loaders/npm-install-root.js +109 -0
- package/dist/controller-loaders/npm-install-root.js.map +1 -0
- package/dist/controller-loaders/npm-loader.d.ts +3 -0
- package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
- package/dist/controller-loaders/npm-loader.js +79 -5
- package/dist/controller-loaders/npm-loader.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/kernel.d.ts +6 -2
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +24 -13
- package/dist/kernel.js.map +1 -1
- package/dist/manifest-sources/analysis-stamp.d.ts +12 -8
- package/dist/manifest-sources/analysis-stamp.d.ts.map +1 -1
- package/dist/manifest-sources/analysis-stamp.js +50 -23
- package/dist/manifest-sources/analysis-stamp.js.map +1 -1
- package/dist/manifest-sources/local-manifest-cache-source.d.ts +45 -9
- package/dist/manifest-sources/local-manifest-cache-source.d.ts.map +1 -1
- package/dist/manifest-sources/local-manifest-cache-source.js +65 -12
- package/dist/manifest-sources/local-manifest-cache-source.js.map +1 -1
- package/dist/runtime-seam.d.ts.map +1 -1
- package/dist/runtime-seam.js +9 -1
- package/dist/runtime-seam.js.map +1 -1
- package/dist/workspace-marker.d.ts +37 -0
- package/dist/workspace-marker.d.ts.map +1 -0
- package/dist/workspace-marker.js +68 -0
- package/dist/workspace-marker.js.map +1 -0
- package/package.json +4 -3
- package/src/bundle/module-artifact.ts +22 -7
- package/src/controller-loader.ts +1 -1
- package/src/controller-loaders/napi-loader.ts +36 -3
- package/src/controller-loaders/npm-install-root.ts +131 -0
- package/src/controller-loaders/npm-loader.ts +84 -5
- package/src/index.ts +1 -0
- package/src/kernel.ts +24 -14
- package/src/manifest-sources/analysis-stamp.ts +53 -25
- package/src/manifest-sources/local-manifest-cache-source.ts +68 -13
- package/src/runtime-seam.ts +9 -1
- package/src/workspace-marker.ts +68 -0
package/src/kernel.ts
CHANGED
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
} from "./manifest-sources/analysis-stamp.js";
|
|
66
66
|
import {
|
|
67
67
|
cachePathForCanonical,
|
|
68
|
+
legacyManifestsDirFallback,
|
|
68
69
|
resolveCacheRoot,
|
|
69
70
|
resolveEntryDir,
|
|
70
71
|
} from "./manifest-sources/local-manifest-cache-source.js";
|
|
@@ -421,19 +422,20 @@ export class Kernel implements IKernel {
|
|
|
421
422
|
options?.cacheDir !== undefined ? options.cacheDir : resolveCacheRoot(sourceUrl);
|
|
422
423
|
this._cacheRoot = cacheRoot;
|
|
423
424
|
const manifestsDir = cacheRoot ? `${cacheRoot}/manifests` : undefined;
|
|
425
|
+
const analysisDir = cacheRoot ? `${cacheRoot}/analysis` : undefined;
|
|
424
426
|
// `writeCache: false` (`telo run --no-cache-write`) keeps the cache
|
|
425
427
|
// READ-only: compiled validators and the analysis stamp are still loaded
|
|
426
428
|
// from disk, but never written back — so an ephemeral, read-only session
|
|
427
429
|
// rootfs validates in-memory without touching the baked cache.
|
|
428
430
|
const writeCache = options?.writeCache !== false;
|
|
429
431
|
// Point the shared schema validator at the cache so compiled AJV validators
|
|
430
|
-
// are loaded (and, when writable, persisted) under
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
);
|
|
432
|
+
// are loaded (and, when writable, persisted) under `<cache-root>/validators/`.
|
|
433
|
+
// Beside `manifests/` rather than inside it: a compiled validator is not a
|
|
434
|
+
// cached module manifest. Memory-/HTTP-rooted entries skip the cache; their
|
|
435
|
+
// schema compiles stay in-process only.
|
|
436
|
+
this.sharedSchemaValidator.setCacheDir(cacheRoot ? `${cacheRoot}/validators` : undefined, {
|
|
437
|
+
write: writeCache,
|
|
438
|
+
});
|
|
437
439
|
this.rootContext = new ModuleContext(
|
|
438
440
|
sourceUrl,
|
|
439
441
|
{},
|
|
@@ -551,9 +553,7 @@ export class Kernel implements IKernel {
|
|
|
551
553
|
// passes are elided. Memory- / HTTP-rooted entries have no
|
|
552
554
|
// local stamp store and always re-validate.
|
|
553
555
|
const analysisSignature = computeAnalysisSignature(analysisGraph);
|
|
554
|
-
const stamp =
|
|
555
|
-
? await readAnalysisStamp("", manifestsDir)
|
|
556
|
-
: undefined;
|
|
556
|
+
const stamp = analysisDir ? await readAnalysisStamp(sourceUrl, analysisDir) : undefined;
|
|
557
557
|
const skipValidation = stamp?.signature === analysisSignature;
|
|
558
558
|
const errors = this.analyzer.analyzeErrors(
|
|
559
559
|
staticManifests,
|
|
@@ -574,13 +574,13 @@ export class Kernel implements IKernel {
|
|
|
574
574
|
errors.map(staticDiagnosticToRuntime),
|
|
575
575
|
);
|
|
576
576
|
}
|
|
577
|
-
if (
|
|
577
|
+
if (analysisDir && writeCache && !skipValidation) {
|
|
578
578
|
// Best-effort: stamp the verdict so subsequent loads hit the fast
|
|
579
579
|
// path. A read-only filesystem (baked Docker image) reports the
|
|
580
580
|
// failure on stderr and keeps running — the lookup above will
|
|
581
581
|
// simply miss next time. Skipped under `--no-cache-write`.
|
|
582
582
|
try {
|
|
583
|
-
await writeAnalysisStamp(
|
|
583
|
+
await writeAnalysisStamp(sourceUrl, analysisSignature, analysisDir);
|
|
584
584
|
} catch (err) {
|
|
585
585
|
this.logging.kernelLogger().warn("analysis stamp write failed", undefined, { error: err });
|
|
586
586
|
}
|
|
@@ -1017,8 +1017,12 @@ export class Kernel implements IKernel {
|
|
|
1017
1017
|
return this._entryUrl;
|
|
1018
1018
|
}
|
|
1019
1019
|
|
|
1020
|
-
/** The npm install
|
|
1021
|
-
* controller loader so it doesn't re-derive it from the entry URL.
|
|
1020
|
+
/** The npm install BASE for this load (`<cache-root>/npm`), threaded to the
|
|
1021
|
+
* controller loader so it doesn't re-derive it from the entry URL. The base
|
|
1022
|
+
* holds one root per (entry path, host platform); the loader picks which one
|
|
1023
|
+
* this runner uses, so two runners over one workspace — a host and a
|
|
1024
|
+
* bind-mounting container — never share a tree whose `package.json` can only
|
|
1025
|
+
* be true for one of them. */
|
|
1022
1026
|
getInstallRoot(): string | undefined {
|
|
1023
1027
|
return this._cacheRoot ? `${this._cacheRoot}/npm` : undefined;
|
|
1024
1028
|
}
|
|
@@ -1060,6 +1064,11 @@ export class Kernel implements IKernel {
|
|
|
1060
1064
|
this.siblingLibraries.clear();
|
|
1061
1065
|
const transports = defaultTransportRegistry(this.registryUrl);
|
|
1062
1066
|
const entryDir = this._entryUrl ? resolveEntryDir(this._entryUrl) ?? "" : "";
|
|
1067
|
+
// The same pre-anchor root `LocalManifestCacheSource` falls back to. Layers
|
|
1068
|
+
// live beside the cached manifest, so both halves have to look in the same
|
|
1069
|
+
// place or an offline upgrade resolves a manifest from disk and then fetches
|
|
1070
|
+
// its controllers.
|
|
1071
|
+
const legacyDir = legacyManifestsDirFallback(entryDir, manifestsDir ?? null);
|
|
1063
1072
|
// Parsed once per module and shared with the sibling-library join below: a
|
|
1064
1073
|
// `telo.yaml` is a large multi-document file, and re-reading each target's
|
|
1065
1074
|
// once per import edge put dozens of redundant full-YAML parses on the boot
|
|
@@ -1077,6 +1086,7 @@ export class Kernel implements IKernel {
|
|
|
1077
1086
|
entryDir,
|
|
1078
1087
|
this.registryUrl,
|
|
1079
1088
|
manifestsDir,
|
|
1089
|
+
legacyDir,
|
|
1080
1090
|
);
|
|
1081
1091
|
directories.set(file.source, moduleDir ?? undefined);
|
|
1082
1092
|
const artifact = moduleArtifactFor({
|
|
@@ -7,19 +7,30 @@ import * as path from "path";
|
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Hash-keyed analysis cache: a tiny JSON sidecar
|
|
10
|
+
* Hash-keyed analysis cache: a tiny JSON sidecar under `.telo/analysis/`
|
|
11
11
|
* recording that an exact set of manifest bytes — under specific
|
|
12
12
|
* `@telorun/kernel` and `@telorun/analyzer` package versions — passed
|
|
13
13
|
* `analyzer.analyzeErrors`. The next `kernel.load` reads the sidecar
|
|
14
14
|
* and, if signatures match, skips the per-resource validation walk.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* ONE STAMP PER ENTRY, in a directory keyed by a hash of the entry URL.
|
|
17
|
+
* A single file holding a single signature was per-app only because every app
|
|
18
|
+
* used to get its own `.telo` beside its manifest; once the cache root is shared
|
|
19
|
+
* across a workspace, one file means each app overwrites the last — A stamps, B
|
|
20
|
+
* misses and overwrites, forever. That is a permanent 100% miss with no error to
|
|
21
|
+
* show for it, worst in the test suite, where a kernel is spawned per manifest
|
|
22
|
+
* and the cache matters most.
|
|
23
|
+
*
|
|
24
|
+
* A DIRECTORY rather than several records in one file, because two kernels
|
|
25
|
+
* loading different manifests concurrently would otherwise read-modify-write the
|
|
26
|
+
* same JSON and lose each other's entry — and concurrent loads are the normal
|
|
27
|
+
* case, not the exception.
|
|
28
|
+
*
|
|
29
|
+
* NOT under `manifests/`, which holds cached module manifests keyed by transport.
|
|
30
|
+
* A verdict about an entry is not a manifest; filing it there made the directory
|
|
31
|
+
* mean two things.
|
|
20
32
|
*/
|
|
21
33
|
|
|
22
|
-
const CACHE_SUBDIR = ".telo/manifests";
|
|
23
34
|
|
|
24
35
|
/** File-format version of the analysis stamp envelope. Only bumped when
|
|
25
36
|
* the on-disk *layout* changes (new fields, restructured payload). The
|
|
@@ -30,7 +41,6 @@ const CACHE_SUBDIR = ".telo/manifests";
|
|
|
30
41
|
* disk. A hand-maintained integer for that purpose would silently mask
|
|
31
42
|
* newly-stricter validation until the next manifest edit. */
|
|
32
43
|
const ANALYSIS_STAMP_FORMAT_VERSION = 1;
|
|
33
|
-
const ANALYSIS_STAMP_FILE = `${CACHE_SUBDIR}/.validated.json`;
|
|
34
44
|
|
|
35
45
|
const localRequire = createRequire(import.meta.url);
|
|
36
46
|
|
|
@@ -128,21 +138,33 @@ export function computeAnalysisSignature(graph: LoadedGraph): string {
|
|
|
128
138
|
.digest("hex");
|
|
129
139
|
}
|
|
130
140
|
|
|
131
|
-
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
141
|
+
/** Where one entry's stamp lives: `<analysisDir>/<hash of entry URL>.json`.
|
|
142
|
+
*
|
|
143
|
+
* Keyed by the entry URL rather than by its directory, because two manifests in
|
|
144
|
+
* one directory (an app and its test harness) are two entries with two verdicts,
|
|
145
|
+
* and a shared cache root makes the directory a far weaker discriminator than it
|
|
146
|
+
* used to be. */
|
|
147
|
+
function stampPath(analysisDir: string, entryUrl: string): string {
|
|
148
|
+
const id = createHash("sha256").update(entryUrl).digest("hex").slice(0, 32);
|
|
149
|
+
return path.join(analysisDir, `${id}.json`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Read the stamped analysis verdict for `entryUrl`, or `undefined` when
|
|
153
|
+
* missing / unreadable / format-mismatched. The `version` field is the
|
|
154
|
+
* on-disk *format* version; semantic invalidation flows through the
|
|
155
|
+
* signature (which embeds package versions). A future format change bumps
|
|
156
|
+
* `version` so older kernels reading a newer stamp (or vice versa) discard
|
|
157
|
+
* rather than misparse.
|
|
158
|
+
*
|
|
159
|
+
* A pre-workspace-anchor `.telo/manifests/.validated.json` is simply never
|
|
160
|
+
* looked at: it is a file where this layout wants a directory, so neither
|
|
161
|
+
* version of the kernel can misread the other's. */
|
|
137
162
|
export async function readAnalysisStamp(
|
|
138
|
-
|
|
139
|
-
|
|
163
|
+
entryUrl: string,
|
|
164
|
+
analysisDir: string,
|
|
140
165
|
): Promise<AnalysisStamp | undefined> {
|
|
141
|
-
const stampPath = manifestsDir
|
|
142
|
-
? path.join(manifestsDir, ".validated.json")
|
|
143
|
-
: path.join(entryDir, ANALYSIS_STAMP_FILE);
|
|
144
166
|
try {
|
|
145
|
-
const text = await fs.readFile(stampPath, "utf-8");
|
|
167
|
+
const text = await fs.readFile(stampPath(analysisDir, entryUrl), "utf-8");
|
|
146
168
|
const parsed = JSON.parse(text) as Partial<AnalysisStamp>;
|
|
147
169
|
if (
|
|
148
170
|
parsed?.version === ANALYSIS_STAMP_FORMAT_VERSION &&
|
|
@@ -160,17 +182,23 @@ export async function readAnalysisStamp(
|
|
|
160
182
|
* per-resource validation walk when the manifest set is unchanged.
|
|
161
183
|
* Idempotent; safe to call after every successful load. */
|
|
162
184
|
export async function writeAnalysisStamp(
|
|
163
|
-
|
|
185
|
+
entryUrl: string,
|
|
164
186
|
signature: string,
|
|
165
|
-
|
|
187
|
+
analysisDir: string,
|
|
166
188
|
): Promise<void> {
|
|
167
189
|
const stamp: AnalysisStamp = {
|
|
168
190
|
version: ANALYSIS_STAMP_FORMAT_VERSION,
|
|
169
191
|
signature,
|
|
170
192
|
};
|
|
171
|
-
const target =
|
|
172
|
-
? path.join(manifestsDir, ".validated.json")
|
|
173
|
-
: path.join(entryDir, ANALYSIS_STAMP_FILE);
|
|
193
|
+
const target = stampPath(analysisDir, entryUrl);
|
|
174
194
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
175
|
-
|
|
195
|
+
// Temp file + rename, as the controller-source cache does. Two kernels loading
|
|
196
|
+
// the SAME entry concurrently — the normal case for a shared root, where a test
|
|
197
|
+
// suite runs many manifests at once — would otherwise interleave writes to one
|
|
198
|
+
// path. A torn stamp is read as a miss rather than as a wrong verdict, so the
|
|
199
|
+
// cost is a silent re-validation, but rename makes it unrepresentable for the
|
|
200
|
+
// price of one syscall.
|
|
201
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
202
|
+
await fs.writeFile(tmp, JSON.stringify(stamp), "utf-8");
|
|
203
|
+
await fs.rename(tmp, target);
|
|
176
204
|
}
|
|
@@ -12,6 +12,7 @@ import { fileURLToPath, pathToFileURL } from "url";
|
|
|
12
12
|
|
|
13
13
|
import { hostEnv } from "../host-env.js";
|
|
14
14
|
import { TransportRegistry, defaultTransportRegistry } from "../transports/transport-registry.js";
|
|
15
|
+
import { findWorkspaceRoot } from "../workspace-marker.js";
|
|
15
16
|
|
|
16
17
|
const CACHE_SUBDIR = ".telo/manifests";
|
|
17
18
|
const DEFAULT_REGISTRY_URL = "https://registry.telo.run";
|
|
@@ -51,15 +52,51 @@ function cachePathForUrl(
|
|
|
51
52
|
return joinUnder(cacheRoot, ...key.split("/"));
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/** The PRE-WORKSPACE-ANCHOR manifest cache for an entry: always
|
|
56
|
+
* `<entry-dir>/.telo/manifests`, regardless of marker or env override. `null`
|
|
57
|
+
* for an entry with no local anchor.
|
|
58
|
+
*
|
|
59
|
+
* A single definition because two things read the old location and they must
|
|
60
|
+
* agree: the manifest source serves `telo.yaml` from it, and `moduleDirectoryFor`
|
|
61
|
+
* places that module's LAYERS beside it. Deriving the path twice is how the two
|
|
62
|
+
* halves end up disagreeing — the manifest resolving from the old root while its
|
|
63
|
+
* controller layers are looked for under the new one, which is exactly the
|
|
64
|
+
* offline boot the fallback exists to keep working. */
|
|
65
|
+
export function legacyManifestsDir(entryDir: string): string | null {
|
|
66
|
+
return entryDir ? path.join(entryDir, CACHE_SUBDIR) : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** `legacyManifestsDir`, or `null` when it would coincide with `current` — so the
|
|
70
|
+
* fallback is never a second lookup at the same place. */
|
|
71
|
+
export function legacyManifestsDirFallback(
|
|
72
|
+
entryDir: string,
|
|
73
|
+
current: string | null,
|
|
74
|
+
): string | null {
|
|
75
|
+
const legacy = legacyManifestsDir(entryDir);
|
|
76
|
+
if (legacy === null) return null;
|
|
77
|
+
if (current !== null && path.resolve(legacy) === path.resolve(current)) return null;
|
|
78
|
+
return legacy;
|
|
79
|
+
}
|
|
80
|
+
|
|
54
81
|
/**
|
|
55
|
-
* Reads previously-cached manifest YAMLs from
|
|
56
|
-
*
|
|
57
|
-
*
|
|
82
|
+
* Reads previously-cached manifest YAMLs from the resolved manifest cache. Sits
|
|
83
|
+
* ahead of `RegistrySource` / `HttpSource` in the source chain — a hit makes boot
|
|
84
|
+
* hermetic, a miss falls through to the network source unchanged.
|
|
58
85
|
*
|
|
59
86
|
* Populated by `writeManifestCache` at install time.
|
|
87
|
+
*
|
|
88
|
+
* On a miss it consults the PRE-WORKSPACE-ANCHOR location
|
|
89
|
+
* (`<entry-dir>/.telo/manifests/`) before giving up. Every other cache in `.telo`
|
|
90
|
+
* costs only CPU when it goes cold; this one costs network, so without the
|
|
91
|
+
* fallback the move to a workspace-anchored root would stop a hermetic setup from
|
|
92
|
+
* booting — its `telo install` output stranded at the old path, with the failure
|
|
93
|
+
* surfacing as a registry fetch on a machine that has no route to one. Read-only
|
|
94
|
+
* and one directory deep: writes always go to the current root, so the old copy
|
|
95
|
+
* ages out rather than being maintained.
|
|
60
96
|
*/
|
|
61
97
|
export class LocalManifestCacheSource implements ManifestSource {
|
|
62
|
-
private readonly cacheRoot: string;
|
|
98
|
+
private readonly cacheRoot: string | null;
|
|
99
|
+
private readonly legacyRoot: string | null;
|
|
63
100
|
private readonly transports: TransportRegistry;
|
|
64
101
|
|
|
65
102
|
constructor(
|
|
@@ -70,7 +107,8 @@ export class LocalManifestCacheSource implements ManifestSource {
|
|
|
70
107
|
// `manifestsDir` is the resolved manifest-cache directory threaded from a
|
|
71
108
|
// single `resolveCacheRoot` (honours `TELO_CACHE_DIR`); when absent we fall
|
|
72
109
|
// back to the entry-anchored default so library/test callers are unchanged.
|
|
73
|
-
this.cacheRoot = manifestsDir ??
|
|
110
|
+
this.cacheRoot = manifestsDir ?? legacyManifestsDir(entryDir);
|
|
111
|
+
this.legacyRoot = legacyManifestsDirFallback(entryDir, this.cacheRoot);
|
|
74
112
|
this.transports = defaultTransportRegistry(registryUrl);
|
|
75
113
|
}
|
|
76
114
|
|
|
@@ -108,7 +146,12 @@ export class LocalManifestCacheSource implements ManifestSource {
|
|
|
108
146
|
}
|
|
109
147
|
|
|
110
148
|
private tryMap(url: string): string | null {
|
|
111
|
-
|
|
149
|
+
return this.tryMapIn(url, this.cacheRoot) ?? this.tryMapIn(url, this.legacyRoot);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private tryMapIn(url: string, root: string | null): string | null {
|
|
153
|
+
if (root === null) return null;
|
|
154
|
+
const candidate = cachePathForUrl(url, root, this.transports);
|
|
112
155
|
if (!candidate) return null;
|
|
113
156
|
// Require a regular file. A directory, dangling symlink, or stat failure
|
|
114
157
|
// (ENOENT, EACCES, EISDIR-on-component) all fall through as a cache miss
|
|
@@ -219,18 +262,30 @@ export function resolveEntryDir(entryPath: string): string | null {
|
|
|
219
262
|
}
|
|
220
263
|
|
|
221
264
|
/** The single `.telo` cache root for an entry, resolved once and threaded to
|
|
222
|
-
* every consumer (manifest cache, compiled validators, analysis
|
|
223
|
-
* install root) so none of them re-derive it or read the env
|
|
265
|
+
* every consumer (manifest cache, compiled validators, analysis stamps, npm
|
|
266
|
+
* install root, cargo target dirs) so none of them re-derive it or read the env
|
|
267
|
+
* independently.
|
|
268
|
+
*
|
|
269
|
+
* Precedence: `TELO_CACHE_DIR` (the relocated root a prebuilt image bakes its
|
|
270
|
+
* deps into) wins; then the directory holding `telo-workspace.yaml`, so every
|
|
271
|
+
* app in one repo shares a cache instead of each carrying its own copy of the
|
|
272
|
+
* same manifests, validators, bundles and npm tree; then `<entry-dir>/.telo`.
|
|
273
|
+
*
|
|
274
|
+
* Anchoring on the marker's LOCATION only — never its `modules:` list, which is
|
|
275
|
+
* release scope — so a manifest in no release subtree (an example, a test
|
|
276
|
+
* fixture) shares the cache exactly as an app does. With no marker anywhere
|
|
277
|
+
* above, this collapses to what it did before the anchor existed, so the file
|
|
278
|
+
* enables the shared cache rather than gating one and deleting it cannot break
|
|
279
|
+
* a build.
|
|
224
280
|
*
|
|
225
|
-
* `TELO_CACHE_DIR` (the relocated root a prebuilt image bakes its deps into)
|
|
226
|
-
* wins; otherwise the root sits beside the entry at `<entry-dir>/.telo`.
|
|
227
281
|
* Returns `null` for an entry with no local anchor — an http(s), `memory://`
|
|
228
282
|
* or any other non-`file:` scheme — in which case the disk cache is skipped.
|
|
229
|
-
* Consumers append the conventional subdirs: `manifests/`, `
|
|
230
|
-
* `npm/`. */
|
|
283
|
+
* Consumers append the conventional subdirs: `manifests/`, `analysis/`,
|
|
284
|
+
* `validators/`, `controller-src/`, `npm/`. */
|
|
231
285
|
export function resolveCacheRoot(entryPath: string): string | null {
|
|
232
286
|
const override = hostEnv().TELO_CACHE_DIR;
|
|
233
287
|
if (override && override.trim()) return path.resolve(override.trim());
|
|
234
288
|
const entryDir = resolveEntryDir(entryPath);
|
|
235
|
-
|
|
289
|
+
if (!entryDir) return null;
|
|
290
|
+
return path.join(findWorkspaceRoot(entryDir) ?? entryDir, ".telo");
|
|
236
291
|
}
|
package/src/runtime-seam.ts
CHANGED
|
@@ -176,7 +176,15 @@ export class KernelRuntimeSeam implements RuntimeSeam {
|
|
|
176
176
|
// child down in its own `finally`, so this only has to close the channels.
|
|
177
177
|
const exitCode = (async () => {
|
|
178
178
|
try {
|
|
179
|
-
|
|
179
|
+
// The child runs a manifest in the SAME workspace as its parent, so it
|
|
180
|
+
// shares the root the parent already resolved instead of deriving one
|
|
181
|
+
// beside the child manifest. Deriving put a `.telo` in every directory
|
|
182
|
+
// holding a test manifest and rebuilt every controller bundle once per
|
|
183
|
+
// test, since `Test.Suite` runs each test through here.
|
|
184
|
+
//
|
|
185
|
+
// `undefined` means "resolve one yourself" and `null` means "no cache" —
|
|
186
|
+
// a parent with no local anchor must yield the first, not the second.
|
|
187
|
+
await child.load(source, { cacheDir: this.kernel.getCacheRoot() ?? undefined });
|
|
180
188
|
await child.start();
|
|
181
189
|
return child.exitCode;
|
|
182
190
|
} catch (err) {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding `telo-workspace.yaml` on disk.
|
|
3
|
+
*
|
|
4
|
+
* Its LOCATION is a general anchor — the directory workspace-relative paths are
|
|
5
|
+
* measured from, the outer bound on how far a walk-up may look, and the root the
|
|
6
|
+
* `.telo` cache is anchored at — while its `modules:` list is release scope. So
|
|
7
|
+
* the finder is deliberately separate from `release/`: `telo run` bounds its
|
|
8
|
+
* `.env` collection with the marker and `resolveCacheRoot` anchors on it, and
|
|
9
|
+
* neither may reach into the release namespace to do so, where `loadWorkspace`
|
|
10
|
+
* (which parses `modules:` and throws when the file is absent) sits one import
|
|
11
|
+
* away.
|
|
12
|
+
*
|
|
13
|
+
* It lives in the KERNEL rather than the CLI because the cache root is resolved
|
|
14
|
+
* here, on a path a CLI-less embedder also takes (a child kernel started through
|
|
15
|
+
* the runtime seam, a test harness). The CLI re-exports it; nothing is duplicated.
|
|
16
|
+
*
|
|
17
|
+
* Parsing the file is the analyzer's (`release/workspace-config.ts`); finding it
|
|
18
|
+
* is the Node half, and this is all of it.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { WORKSPACE_FILENAME } from "@telorun/analyzer";
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
|
+
import * as path from "node:path";
|
|
24
|
+
|
|
25
|
+
export { WORKSPACE_FILENAME };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Walk up from `from` looking for the marker. Returns the directory holding it,
|
|
29
|
+
* or `undefined` — the file is optional. `telo release` requires one and reports
|
|
30
|
+
* its absence; `telo run` and `resolveCacheRoot` read `undefined` as "no parent
|
|
31
|
+
* lookup", falling back to what they did before the marker existed.
|
|
32
|
+
*
|
|
33
|
+
* `from` is resolved through symlinks first: a manifest reached through a linked
|
|
34
|
+
* directory would otherwise walk the LINK's parents, miss a marker that is right
|
|
35
|
+
* there in the real tree, and silently fall back to a narrower answer.
|
|
36
|
+
*/
|
|
37
|
+
export function findWorkspaceRoot(from: string): string | undefined {
|
|
38
|
+
let dir = realPath(path.resolve(from));
|
|
39
|
+
for (;;) {
|
|
40
|
+
if (isFile(path.join(dir, WORKSPACE_FILENAME))) return dir;
|
|
41
|
+
const parent = path.dirname(dir);
|
|
42
|
+
if (parent === dir) return undefined;
|
|
43
|
+
dir = parent;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The marker must be a FILE. `existsSync` would accept a directory of that name
|
|
48
|
+
* and anchor the whole cache on it, and the Rust half tests `is_file()` — a
|
|
49
|
+
* workspace visible to one kernel and not the other is precisely the divergence
|
|
50
|
+
* a shared marker exists to prevent. */
|
|
51
|
+
function isFile(target: string): boolean {
|
|
52
|
+
try {
|
|
53
|
+
return fs.statSync(target).isFile();
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `fs.realpathSync`, falling back to the input when the path does not exist —
|
|
60
|
+
* a manifest path that is wrong is the caller's error to report, not this
|
|
61
|
+
* helper's to convert into a different one. */
|
|
62
|
+
export function realPath(target: string): string {
|
|
63
|
+
try {
|
|
64
|
+
return fs.realpathSync(target);
|
|
65
|
+
} catch {
|
|
66
|
+
return target;
|
|
67
|
+
}
|
|
68
|
+
}
|