@telorun/kernel 0.25.0 → 0.26.1
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/controller-loader.d.ts +4 -0
- package/dist/controller-loader.d.ts.map +1 -1
- package/dist/controller-loader.js +4 -1
- package/dist/controller-loader.js.map +1 -1
- package/dist/controller-loaders/npm-loader.d.ts +10 -0
- package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
- package/dist/controller-loaders/npm-loader.js +2 -1
- package/dist/controller-loaders/npm-loader.js.map +1 -1
- package/dist/controllers/module/import-controller.d.ts.map +1 -1
- package/dist/controllers/module/import-controller.js +48 -2
- package/dist/controllers/module/import-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js +1 -0
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/kernel.d.ts +8 -0
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +29 -14
- package/dist/kernel.js.map +1 -1
- package/dist/manifest-sources/analysis-stamp.d.ts +2 -2
- package/dist/manifest-sources/analysis-stamp.d.ts.map +1 -1
- package/dist/manifest-sources/analysis-stamp.js +9 -4
- package/dist/manifest-sources/analysis-stamp.js.map +1 -1
- package/dist/manifest-sources/local-manifest-cache-source.d.ts +13 -3
- package/dist/manifest-sources/local-manifest-cache-source.d.ts.map +1 -1
- package/dist/manifest-sources/local-manifest-cache-source.js +25 -6
- package/dist/manifest-sources/local-manifest-cache-source.js.map +1 -1
- package/dist/resource-context.d.ts +1 -0
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +3 -0
- package/dist/resource-context.js.map +1 -1
- package/dist/schema-validator.d.ts +8 -1
- package/dist/schema-validator.d.ts.map +1 -1
- package/dist/schema-validator.js +8 -2
- package/dist/schema-validator.js.map +1 -1
- package/package.json +3 -3
- package/src/controller-loader.ts +8 -1
- package/src/controller-loaders/npm-loader.ts +12 -1
- package/src/controllers/module/import-controller.ts +60 -4
- package/src/controllers/resource-definition/resource-definition-controller.ts +1 -0
- package/src/index.ts +1 -0
- package/src/kernel.ts +41 -15
- package/src/manifest-sources/analysis-stamp.ts +9 -2
- package/src/manifest-sources/local-manifest-cache-source.ts +29 -4
- package/src/resource-context.ts +4 -0
- package/src/schema-validator.ts +8 -2
package/src/controller-loader.ts
CHANGED
|
@@ -55,6 +55,10 @@ export interface ControllerLoaderOptions {
|
|
|
55
55
|
* (cargo loader has its own per-crate cache and does not need this).
|
|
56
56
|
*/
|
|
57
57
|
entryUrl?: string;
|
|
58
|
+
/** Explicit npm install root (`<cache-root>/npm`), threaded from the kernel's
|
|
59
|
+
* single `resolveCacheRoot`. Overrides the entry-anchored default so a
|
|
60
|
+
* relocated `TELO_CACHE_DIR` is honoured. */
|
|
61
|
+
installRoot?: string;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
/**
|
|
@@ -82,7 +86,10 @@ export class ControllerLoader {
|
|
|
82
86
|
|
|
83
87
|
constructor(options: ControllerLoaderOptions = {}) {
|
|
84
88
|
this.emit = options.emit;
|
|
85
|
-
this.npmLoader = new NpmControllerLoader({
|
|
89
|
+
this.npmLoader = new NpmControllerLoader({
|
|
90
|
+
entryUrl: options.entryUrl,
|
|
91
|
+
installRoot: options.installRoot,
|
|
92
|
+
});
|
|
86
93
|
this.napiLoader = new NapiControllerLoader();
|
|
87
94
|
this.bundleLoader = new BundleControllerLoader();
|
|
88
95
|
}
|
|
@@ -97,6 +97,14 @@ export interface NpmControllerLoaderOptions {
|
|
|
97
97
|
* `pkg-name` cli command resolves it from its argument.
|
|
98
98
|
*/
|
|
99
99
|
entryUrl?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Explicit install root, threaded from the kernel's single `resolveCacheRoot`
|
|
102
|
+
* (`<cache-root>/npm`). When set it overrides the entry-anchored
|
|
103
|
+
* `computeInstallRoot`, so a relocated `TELO_CACHE_DIR` (e.g. a prebuilt
|
|
104
|
+
* image baking deps at `/telo-cache`) is honoured without this loader reading
|
|
105
|
+
* the env itself.
|
|
106
|
+
*/
|
|
107
|
+
installRoot?: string;
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
/**
|
|
@@ -127,6 +135,8 @@ export interface NpmControllerLoaderOptions {
|
|
|
127
135
|
*/
|
|
128
136
|
export class NpmControllerLoader {
|
|
129
137
|
private readonly entryUrl?: string;
|
|
138
|
+
/** Threaded install root (`<cache-root>/npm`); overrides `computeInstallRoot`. */
|
|
139
|
+
private readonly installRootOverride?: string;
|
|
130
140
|
|
|
131
141
|
/**
|
|
132
142
|
* Per-process cache of "this controller's package + version is already
|
|
@@ -164,6 +174,7 @@ export class NpmControllerLoader {
|
|
|
164
174
|
|
|
165
175
|
constructor(options: NpmControllerLoaderOptions = {}) {
|
|
166
176
|
this.entryUrl = options.entryUrl;
|
|
177
|
+
this.installRootOverride = options.installRoot;
|
|
167
178
|
}
|
|
168
179
|
|
|
169
180
|
async load(purl: string, baseUri: string): Promise<NpmLoadResult> {
|
|
@@ -219,7 +230,7 @@ export class NpmControllerLoader {
|
|
|
219
230
|
);
|
|
220
231
|
}
|
|
221
232
|
const entryUrlStr = this.entryUrl;
|
|
222
|
-
const installRoot = computeInstallRoot(entryUrlStr);
|
|
233
|
+
const installRoot = this.installRootOverride ?? computeInstallRoot(entryUrlStr);
|
|
223
234
|
|
|
224
235
|
// Build the install-root package.json: kernel-runtime deps as `file:` refs
|
|
225
236
|
// pointing at the kernel-side realpath. Modules declare these names as
|
|
@@ -121,10 +121,22 @@ export async function create(
|
|
|
121
121
|
// no resources/env/ports in import inputs — is enforced by the analyzer, not here;
|
|
122
122
|
// a `${{ resources.X }}` slipping past analysis (skipValidation / programmatic
|
|
123
123
|
// load) would evaluate at runtime rather than being rejected.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
// Defaults declared on the library's own `variables` / `secrets` contract fill
|
|
125
|
+
// any input the importer didn't override — the import-time analogue of the root
|
|
126
|
+
// Application's env defaulting (`resolveApplicationEnv`). Child modules are
|
|
127
|
+
// isolated from the host environment, so there is no env lookup here: the value
|
|
128
|
+
// is the importer's override, else the library default. Without this a contract
|
|
129
|
+
// var with a default but no override reaches a `${{ variables.X }}` template as
|
|
130
|
+
// a missing key, even though analysis validated the reference against the
|
|
131
|
+
// (defaulted) contract.
|
|
132
|
+
const importVariables = applyDefaults(
|
|
133
|
+
(ctx.expandValue(resource.variables, {}) as Record<string, unknown>) ?? {},
|
|
134
|
+
moduleManifest.variables ?? {},
|
|
135
|
+
);
|
|
136
|
+
const importSecrets = applyDefaults(
|
|
137
|
+
(ctx.expandValue(resource.secrets, {}) as Record<string, unknown>) ?? {},
|
|
138
|
+
moduleManifest.secrets ?? {},
|
|
139
|
+
);
|
|
128
140
|
const childCtx = new ModuleContext(
|
|
129
141
|
ctx.moduleContext.source,
|
|
130
142
|
importVariables,
|
|
@@ -159,6 +171,30 @@ export async function create(
|
|
|
159
171
|
child.registerManifest(manifest);
|
|
160
172
|
}
|
|
161
173
|
|
|
174
|
+
// Initialize the child's resources in dependency order, mirroring the root
|
|
175
|
+
// context (kernel.boot → setInitOrder). Without this, an imported library's
|
|
176
|
+
// resources init in registration order, so a dependent can run Phase 5
|
|
177
|
+
// injection before its dependency exists — e.g. an Http.Api whose inline route
|
|
178
|
+
// handler is appended after it during inline-resource extraction would init,
|
|
179
|
+
// and inject, before the handler is created, leaving the handler ref
|
|
180
|
+
// unresolved. The order is computed in the library's OWN scope (childRegistry),
|
|
181
|
+
// so an anonymous child resolves against the declaring library, not the
|
|
182
|
+
// consumer. setInitOrder ignores names it doesn't recognize, so a partial order
|
|
183
|
+
// is safe. A cycle purely among a library's own resources is invisible to the
|
|
184
|
+
// root graph (which stops at the import boundary), so this is the only place it
|
|
185
|
+
// surfaces — throw it the same way the root does rather than silently falling
|
|
186
|
+
// back to registration order (which would re-manifest as a confusing runtime
|
|
187
|
+
// ERR_RESOURCE_NOT_INVOKABLE). `prepare` returns null order on a ref-validation
|
|
188
|
+
// error too (already reported by analyze above); those we leave to fall back.
|
|
189
|
+
const { order, cycleError } = analyzer.prepare(manifests, childRegistry);
|
|
190
|
+
if (cycleError) {
|
|
191
|
+
throw new RuntimeError(
|
|
192
|
+
"ERR_CIRCULAR_DEPENDENCY",
|
|
193
|
+
`Circular dependency in imported library '${targetModule}': ${cycleError}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (order) (child as ModuleContext).setInitOrder(order);
|
|
197
|
+
|
|
162
198
|
// Link the target module context as a child of the declaring module context in
|
|
163
199
|
// the lifecycle tree. This enables cascading teardown (parent → child order)
|
|
164
200
|
// and makes the import hierarchy visible at runtime.
|
|
@@ -251,6 +287,26 @@ export async function create(
|
|
|
251
287
|
};
|
|
252
288
|
}
|
|
253
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Fill in library-declared `default:` values for any input the importer left
|
|
292
|
+
* unset. Mirrors the root Application's env defaulting: a provided value (incl.
|
|
293
|
+
* an explicit `null`) wins; otherwise the contract default applies. Returns a new
|
|
294
|
+
* object — the resolved input map is never mutated in place.
|
|
295
|
+
*/
|
|
296
|
+
function applyDefaults(
|
|
297
|
+
provided: Record<string, unknown>,
|
|
298
|
+
schemaDefs: Record<string, any>,
|
|
299
|
+
): Record<string, unknown> {
|
|
300
|
+
const out = { ...provided };
|
|
301
|
+
for (const [key, def] of Object.entries(schemaDefs)) {
|
|
302
|
+
if (typeof def !== "object" || def === null || !("default" in def)) continue;
|
|
303
|
+
if (out[key] === undefined || out[key] === null) {
|
|
304
|
+
out[key] = def.default;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
|
|
254
310
|
function validateRequiredInputs(
|
|
255
311
|
schemaDefs: Record<string, any>,
|
|
256
312
|
provided: Record<string, unknown>,
|
|
@@ -54,6 +54,7 @@ class ResourceDefinition implements ResourceInstance {
|
|
|
54
54
|
const loader = new ControllerLoader({
|
|
55
55
|
emit: (e) => ctx.emit(e.name, e.payload),
|
|
56
56
|
entryUrl: ctx.getEntryUrl(),
|
|
57
|
+
installRoot: ctx.getInstallRoot(),
|
|
57
58
|
});
|
|
58
59
|
const controllerInstance = await loader.load(
|
|
59
60
|
this.resource.controllers,
|
package/src/index.ts
CHANGED
package/src/kernel.ts
CHANGED
|
@@ -40,7 +40,9 @@ import {
|
|
|
40
40
|
readAnalysisStamp,
|
|
41
41
|
writeAnalysisStamp,
|
|
42
42
|
} from "./manifest-sources/analysis-stamp.js";
|
|
43
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
resolveCacheRoot,
|
|
45
|
+
} from "./manifest-sources/local-manifest-cache-source.js";
|
|
44
46
|
import {
|
|
45
47
|
precompileApplicationEnvSchemas,
|
|
46
48
|
precompileDefinitionSchemas,
|
|
@@ -169,6 +171,9 @@ export class Kernel implements IKernel {
|
|
|
169
171
|
private rootContext!: ModuleContext;
|
|
170
172
|
private staticManifests: ResourceManifest[] = [];
|
|
171
173
|
private _entryUrl?: string;
|
|
174
|
+
/** The `.telo` cache root for this load, resolved once in `load()` and
|
|
175
|
+
* threaded to the validator, analysis stamp, and npm install root. */
|
|
176
|
+
private _cacheRoot?: string | null;
|
|
172
177
|
private _loadedGraph?: LoadedGraph;
|
|
173
178
|
// Lifecycle state — guards boot/runTargets/teardown/invoke transitions.
|
|
174
179
|
// teardown() is the only idempotent method; everything else throws on misuse.
|
|
@@ -319,18 +324,32 @@ export class Kernel implements IKernel {
|
|
|
319
324
|
* session rootfs — hits the stamp and skips the validation walk entirely
|
|
320
325
|
* instead of failing to write the caches on every boot.
|
|
321
326
|
*/
|
|
322
|
-
async load(
|
|
327
|
+
async load(
|
|
328
|
+
url: string,
|
|
329
|
+
options?: { analyzeOnly?: boolean; cacheDir?: string | null; writeCache?: boolean },
|
|
330
|
+
): Promise<void> {
|
|
323
331
|
const sourceUrl = await this.loader.resolveEntryPoint(url);
|
|
324
332
|
this._entryUrl = sourceUrl;
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
const
|
|
333
|
+
// Resolve the `.telo` cache root ONCE and thread it to every consumer
|
|
334
|
+
// (validators, analysis stamp, npm install root) — no consumer re-derives
|
|
335
|
+
// it or reads the env independently. A caller (the CLI) may pass `cacheDir`
|
|
336
|
+
// so the env is read exactly once per invocation; otherwise resolve here.
|
|
337
|
+
const cacheRoot =
|
|
338
|
+
options?.cacheDir !== undefined ? options.cacheDir : resolveCacheRoot(sourceUrl);
|
|
339
|
+
this._cacheRoot = cacheRoot;
|
|
340
|
+
const manifestsDir = cacheRoot ? `${cacheRoot}/manifests` : undefined;
|
|
341
|
+
// `writeCache: false` (`telo run --no-cache-write`) keeps the cache
|
|
342
|
+
// READ-only: compiled validators and the analysis stamp are still loaded
|
|
343
|
+
// from disk, but never written back — so an ephemeral, read-only session
|
|
344
|
+
// rootfs validates in-memory without touching the baked cache.
|
|
345
|
+
const writeCache = options?.writeCache !== false;
|
|
346
|
+
// Point the shared schema validator at the cache so compiled AJV validators
|
|
347
|
+
// are loaded (and, when writable, persisted) under
|
|
348
|
+
// `<cache-root>/manifests/__validators/`. Memory-/HTTP-rooted entries skip
|
|
349
|
+
// the cache; their schema compiles stay in-process only.
|
|
330
350
|
this.sharedSchemaValidator.setCacheDir(
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
: undefined,
|
|
351
|
+
manifestsDir ? `${manifestsDir}/__validators` : undefined,
|
|
352
|
+
{ write: writeCache },
|
|
334
353
|
);
|
|
335
354
|
this.rootContext = new ModuleContext(
|
|
336
355
|
sourceUrl,
|
|
@@ -393,9 +412,10 @@ export class Kernel implements IKernel {
|
|
|
393
412
|
// and inline-resource normalisation still runs — only the diagnostic
|
|
394
413
|
// passes are elided. Memory- / HTTP-rooted entries have no
|
|
395
414
|
// local stamp store and always re-validate.
|
|
396
|
-
const entryDir = resolveEntryDir(sourceUrl);
|
|
397
415
|
const analysisSignature = computeAnalysisSignature(analysisGraph);
|
|
398
|
-
const stamp =
|
|
416
|
+
const stamp = manifestsDir
|
|
417
|
+
? await readAnalysisStamp("", manifestsDir)
|
|
418
|
+
: undefined;
|
|
399
419
|
const skipValidation = stamp?.signature === analysisSignature;
|
|
400
420
|
const errors = this.analyzer.analyzeErrors(
|
|
401
421
|
staticManifests,
|
|
@@ -416,13 +436,13 @@ export class Kernel implements IKernel {
|
|
|
416
436
|
})),
|
|
417
437
|
);
|
|
418
438
|
}
|
|
419
|
-
if (
|
|
439
|
+
if (manifestsDir && writeCache && !skipValidation) {
|
|
420
440
|
// Best-effort: stamp the verdict so subsequent loads hit the fast
|
|
421
441
|
// path. A read-only filesystem (baked Docker image) reports the
|
|
422
442
|
// failure on stderr and keeps running — the lookup above will
|
|
423
|
-
// simply miss next time.
|
|
443
|
+
// simply miss next time. Skipped under `--no-cache-write`.
|
|
424
444
|
try {
|
|
425
|
-
await writeAnalysisStamp(
|
|
445
|
+
await writeAnalysisStamp("", analysisSignature, manifestsDir);
|
|
426
446
|
} catch (err) {
|
|
427
447
|
this.stderr.write(
|
|
428
448
|
`[telo:kernel] analysis stamp write failed: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
@@ -744,6 +764,12 @@ export class Kernel implements IKernel {
|
|
|
744
764
|
return this._entryUrl;
|
|
745
765
|
}
|
|
746
766
|
|
|
767
|
+
/** The npm install root for this load (`<cache-root>/npm`), threaded to the
|
|
768
|
+
* controller loader so it doesn't re-derive it from the entry URL. */
|
|
769
|
+
getInstallRoot(): string | undefined {
|
|
770
|
+
return this._cacheRoot ? `${this._cacheRoot}/npm` : undefined;
|
|
771
|
+
}
|
|
772
|
+
|
|
747
773
|
/** Authored `kind` of a declared resource by name, from the static manifest
|
|
748
774
|
* set. Init-order-independent (unlike `resourceInstances`), so a controller
|
|
749
775
|
* resolving a `!ref <name>` sentinel before the target initializes can still
|
|
@@ -136,9 +136,13 @@ export function computeAnalysisSignature(graph: LoadedGraph): string {
|
|
|
136
136
|
* reading a newer stamp (or vice versa) discard rather than misparse. */
|
|
137
137
|
export async function readAnalysisStamp(
|
|
138
138
|
entryDir: string,
|
|
139
|
+
manifestsDir?: string,
|
|
139
140
|
): Promise<AnalysisStamp | undefined> {
|
|
141
|
+
const stampPath = manifestsDir
|
|
142
|
+
? path.join(manifestsDir, ".validated.json")
|
|
143
|
+
: path.join(entryDir, ANALYSIS_STAMP_FILE);
|
|
140
144
|
try {
|
|
141
|
-
const text = await fs.readFile(
|
|
145
|
+
const text = await fs.readFile(stampPath, "utf-8");
|
|
142
146
|
const parsed = JSON.parse(text) as Partial<AnalysisStamp>;
|
|
143
147
|
if (
|
|
144
148
|
parsed?.version === ANALYSIS_STAMP_FORMAT_VERSION &&
|
|
@@ -158,12 +162,15 @@ export async function readAnalysisStamp(
|
|
|
158
162
|
export async function writeAnalysisStamp(
|
|
159
163
|
entryDir: string,
|
|
160
164
|
signature: string,
|
|
165
|
+
manifestsDir?: string,
|
|
161
166
|
): Promise<void> {
|
|
162
167
|
const stamp: AnalysisStamp = {
|
|
163
168
|
version: ANALYSIS_STAMP_FORMAT_VERSION,
|
|
164
169
|
signature,
|
|
165
170
|
};
|
|
166
|
-
const target =
|
|
171
|
+
const target = manifestsDir
|
|
172
|
+
? path.join(manifestsDir, ".validated.json")
|
|
173
|
+
: path.join(entryDir, ANALYSIS_STAMP_FILE);
|
|
167
174
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
168
175
|
await fs.writeFile(target, JSON.stringify(stamp), "utf-8");
|
|
169
176
|
}
|
|
@@ -133,8 +133,15 @@ export class LocalManifestCacheSource implements ManifestSource {
|
|
|
133
133
|
private readonly cacheRoot: string;
|
|
134
134
|
private readonly registryUrl: string;
|
|
135
135
|
|
|
136
|
-
constructor(
|
|
137
|
-
|
|
136
|
+
constructor(
|
|
137
|
+
entryDir: string,
|
|
138
|
+
registryUrl: string = DEFAULT_REGISTRY_URL,
|
|
139
|
+
manifestsDir?: string,
|
|
140
|
+
) {
|
|
141
|
+
// `manifestsDir` is the resolved manifest-cache directory threaded from a
|
|
142
|
+
// single `resolveCacheRoot` (honours `TELO_CACHE_DIR`); when absent we fall
|
|
143
|
+
// back to the entry-anchored default so library/test callers are unchanged.
|
|
144
|
+
this.cacheRoot = manifestsDir ?? path.join(entryDir, CACHE_SUBDIR);
|
|
138
145
|
this.registryUrl = registryUrl;
|
|
139
146
|
}
|
|
140
147
|
|
|
@@ -189,8 +196,9 @@ export function cachePathForCanonical(
|
|
|
189
196
|
canonicalSource: string,
|
|
190
197
|
entryDir: string,
|
|
191
198
|
registryUrl: string,
|
|
199
|
+
manifestsDir?: string,
|
|
192
200
|
): string | null {
|
|
193
|
-
const cacheRoot = path.join(entryDir, CACHE_SUBDIR);
|
|
201
|
+
const cacheRoot = manifestsDir ?? path.join(entryDir, CACHE_SUBDIR);
|
|
194
202
|
return cachePathForUrl(canonicalSource, cacheRoot, registryUrl);
|
|
195
203
|
}
|
|
196
204
|
|
|
@@ -210,6 +218,7 @@ export async function writeManifestCache(
|
|
|
210
218
|
graph: LoadedGraph,
|
|
211
219
|
entryDir: string,
|
|
212
220
|
registryUrl: string = DEFAULT_REGISTRY_URL,
|
|
221
|
+
manifestsDir?: string,
|
|
213
222
|
): Promise<string[]> {
|
|
214
223
|
const written: string[] = [];
|
|
215
224
|
const seen = new Set<string>();
|
|
@@ -220,7 +229,7 @@ export async function writeManifestCache(
|
|
|
220
229
|
if (seen.has(file.source)) continue;
|
|
221
230
|
seen.add(file.source);
|
|
222
231
|
|
|
223
|
-
const target = cachePathForCanonical(file.source, entryDir, registryUrl);
|
|
232
|
+
const target = cachePathForCanonical(file.source, entryDir, registryUrl, manifestsDir);
|
|
224
233
|
if (!target) continue;
|
|
225
234
|
|
|
226
235
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
@@ -254,3 +263,19 @@ export function resolveEntryDir(entryPath: string): string | null {
|
|
|
254
263
|
return path.dirname(absolute);
|
|
255
264
|
}
|
|
256
265
|
}
|
|
266
|
+
|
|
267
|
+
/** The single `.telo` cache root for an entry, resolved once and threaded to
|
|
268
|
+
* every consumer (manifest cache, compiled validators, analysis stamp, npm
|
|
269
|
+
* install root) so none of them re-derive it or read the env independently.
|
|
270
|
+
*
|
|
271
|
+
* `TELO_CACHE_DIR` (the relocated root a prebuilt image bakes its deps into)
|
|
272
|
+
* wins; otherwise the root sits beside the entry at `<entry-dir>/.telo`.
|
|
273
|
+
* Returns `null` for http(s) entries with no local anchor (disk cache skipped).
|
|
274
|
+
* Consumers append the conventional subdirs: `manifests/`, `manifests/__validators/`,
|
|
275
|
+
* `npm/`. */
|
|
276
|
+
export function resolveCacheRoot(entryPath: string): string | null {
|
|
277
|
+
const override = process.env.TELO_CACHE_DIR;
|
|
278
|
+
if (override && override.trim()) return path.resolve(override.trim());
|
|
279
|
+
const entryDir = resolveEntryDir(entryPath);
|
|
280
|
+
return entryDir ? path.join(entryDir, ".telo") : null;
|
|
281
|
+
}
|
package/src/resource-context.ts
CHANGED
|
@@ -344,6 +344,10 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
344
344
|
return this.kernel.getEntryUrl();
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
+
getInstallRoot(): string | undefined {
|
|
348
|
+
return this.kernel.getInstallRoot();
|
|
349
|
+
}
|
|
350
|
+
|
|
347
351
|
on(event: string, handler: (payload?: any) => void | Promise<void>): void {
|
|
348
352
|
this.kernel.on(event, handler);
|
|
349
353
|
}
|
package/src/schema-validator.ts
CHANGED
|
@@ -117,6 +117,11 @@ export class SchemaValidator {
|
|
|
117
117
|
private rawSchemas = new Map<string, object>();
|
|
118
118
|
private compiledValidators = new WeakMap<object, DataValidator>();
|
|
119
119
|
private cacheDir: string | undefined;
|
|
120
|
+
/** When false, the disk cache is read-only: compiled validators are still
|
|
121
|
+
* loaded from `cacheDir` but never written back. `telo run --no-cache-write`
|
|
122
|
+
* sets this so an ephemeral, read-only session rootfs validates in-memory
|
|
123
|
+
* without touching (or failing to write) the baked cache. */
|
|
124
|
+
private cacheWritable = true;
|
|
120
125
|
/** Tracks (schema-hash → in-memory compiled validator) so two distinct
|
|
121
126
|
* but content-equal schema objects share one compile across the kernel
|
|
122
127
|
* process — `compiledValidators` is keyed by object identity and would
|
|
@@ -183,8 +188,9 @@ export class SchemaValidator {
|
|
|
183
188
|
* kernel anchors this under `<entry-dir>/.telo/manifests/__validators/`
|
|
184
189
|
* so it lives next to the manifest cache and rides along in
|
|
185
190
|
* `COPY --from=build /srv /srv` Docker images. */
|
|
186
|
-
setCacheDir(dir: string | undefined): void {
|
|
191
|
+
setCacheDir(dir: string | undefined, opts?: { write?: boolean }): void {
|
|
187
192
|
this.cacheDir = dir;
|
|
193
|
+
this.cacheWritable = opts?.write ?? true;
|
|
188
194
|
}
|
|
189
195
|
|
|
190
196
|
compile(schema: any): DataValidator {
|
|
@@ -315,7 +321,7 @@ export class SchemaValidator {
|
|
|
315
321
|
}
|
|
316
322
|
|
|
317
323
|
const validate = this.ajv.compile(schema) as ValidateFunction;
|
|
318
|
-
if (cacheDir) {
|
|
324
|
+
if (cacheDir && this.cacheWritable) {
|
|
319
325
|
try {
|
|
320
326
|
const body = standaloneCode(this.ajv, validate);
|
|
321
327
|
const integrity = createHash("sha256").update(body).digest("hex");
|