@intentius/chant 0.19.1 → 0.20.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/build-params.d.ts +108 -0
- package/dist/build-params.d.ts.map +1 -0
- package/dist/build.d.ts +79 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +31 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +41 -0
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts +54 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon.d.ts +1 -1
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts +27 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/run-client.d.ts +1 -1
- package/dist/cli/handlers/run-client.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +16 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +10 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +12 -5
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts +62 -7
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/sandbox/driver.d.ts +12 -0
- package/dist/components/sandbox/driver.d.ts.map +1 -0
- package/dist/components/sandbox/run.d.ts +42 -0
- package/dist/components/sandbox/run.d.ts.map +1 -0
- package/dist/composite.d.ts +5 -0
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +71 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/entity-wire-codec.d.ts +166 -0
- package/dist/discovery/entity-wire-codec.d.ts.map +1 -0
- package/dist/discovery/entity-wire.d.ts +50 -0
- package/dist/discovery/entity-wire.d.ts.map +1 -0
- package/dist/discovery/fold-import.d.ts +210 -0
- package/dist/discovery/fold-import.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +74 -1
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.d.ts +18 -0
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -0
- package/dist/discovery/sandbox/child-errors.d.ts +15 -0
- package/dist/discovery/sandbox/child-errors.d.ts.map +1 -0
- package/dist/discovery/sandbox/driver.d.ts +13 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -0
- package/dist/discovery/sandbox/run.d.ts +69 -0
- package/dist/discovery/sandbox/run.d.ts.map +1 -0
- package/dist/errors.d.ts +9 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +226 -0
- package/dist/fold/fold.d.ts.map +1 -0
- package/dist/fold/subset.d.ts +102 -0
- package/dist/fold/subset.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon-output.d.ts +7 -2
- package/dist/lexicon-output.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -2
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +81 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts +11 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lint/component-checks.d.ts +7 -1
- package/dist/lint/component-checks.d.ts.map +1 -1
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/evl003-dynamic-property-access.d.ts.map +1 -1
- package/dist/params.d.ts +60 -0
- package/dist/params.d.ts.map +1 -0
- package/dist/provenance.d.ts +21 -0
- package/dist/provenance.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/build-params.test.ts +144 -0
- package/src/build-params.ts +207 -0
- package/src/build.test.ts +38 -0
- package/src/build.ts +144 -7
- package/src/cli/commands/build.test.ts +220 -2
- package/src/cli/commands/build.ts +111 -3
- package/src/cli/commands/check-lexicon-examples.test.ts +92 -0
- package/src/cli/commands/check-lexicon-examples.ts +103 -0
- package/src/cli/commands/check-lexicon-intrinsics.test.ts +145 -0
- package/src/cli/commands/check-lexicon-intrinsics.ts +312 -0
- package/src/cli/commands/check-lexicon.test.ts +34 -0
- package/src/cli/commands/check-lexicon.ts +119 -1
- package/src/cli/commands/lint.ts +31 -3
- package/src/cli/commands/onboard.ts +1 -1
- package/src/cli/component-security-boundary.test.ts +170 -0
- package/src/cli/handlers/build.ts +24 -3
- package/src/cli/handlers/components.ts +9 -2
- package/src/cli/handlers/dev.ts +1 -1
- package/src/cli/handlers/graph.ts +7 -5
- package/src/cli/handlers/lifecycle.ts +2 -2
- package/src/cli/handlers/lint.ts +2 -0
- package/src/cli/handlers/misc.ts +2 -2
- package/src/cli/handlers/run-client.ts +1 -1
- package/src/cli/handlers/run.ts +20 -5
- package/src/cli/main.test.ts +22 -0
- package/src/cli/main.ts +39 -0
- package/src/cli/plugins.ts +20 -4
- package/src/cli/registry.ts +10 -0
- package/src/cli/security-boundary.test.ts +135 -0
- package/src/codegen/docs-sections.test.ts +61 -0
- package/src/codegen/docs-sections.ts +7 -3
- package/src/components/cli-support.ts +22 -10
- package/src/components/discover.ts +127 -25
- package/src/components/sandbox/driver.ts +114 -0
- package/src/components/sandbox/run.test.ts +185 -0
- package/src/components/sandbox/run.ts +177 -0
- package/src/composite.test.ts +21 -0
- package/src/composite.ts +20 -1
- package/src/config.ts +81 -0
- package/src/discovery/collect.ts +17 -3
- package/src/discovery/entity-wire-codec.ts +485 -0
- package/src/discovery/entity-wire.test.ts +240 -0
- package/src/discovery/entity-wire.ts +67 -0
- package/src/discovery/fold-import.test.ts +1026 -0
- package/src/discovery/fold-import.ts +1805 -0
- package/src/discovery/index.test.ts +191 -1
- package/src/discovery/index.ts +233 -1
- package/src/discovery/sandbox/bundle.ts +218 -0
- package/src/discovery/sandbox/child-errors.ts +65 -0
- package/src/discovery/sandbox/driver.ts +147 -0
- package/src/discovery/sandbox/run.test.ts +179 -0
- package/src/discovery/sandbox/run.ts +196 -0
- package/src/errors.ts +9 -1
- package/src/fold/fold.test.ts +535 -0
- package/src/fold/fold.ts +648 -0
- package/src/fold/subset-doc-parity.test.ts +183 -0
- package/src/fold/subset.test.ts +241 -0
- package/src/fold/subset.ts +302 -0
- package/src/index.ts +2 -0
- package/src/lexicon-output.ts +7 -2
- package/src/lexicon-schema.test.ts +14 -0
- package/src/lexicon-schema.ts +3 -1
- package/src/lexicon.ts +82 -1
- package/src/lifecycle/git.test.ts +10 -5
- package/src/lifecycle/release-ledger.test.ts +28 -0
- package/src/lifecycle/release-ledger.ts +11 -0
- package/src/lint/component-checks.ts +8 -1
- package/src/lint/rules/evl001-non-literal-expression.ts +26 -110
- package/src/lint/rules/evl003-dynamic-property-access.ts +11 -2
- package/src/params.test.ts +22 -0
- package/src/params.ts +66 -0
- package/src/provenance.ts +22 -0
|
@@ -27,6 +27,17 @@
|
|
|
27
27
|
* components the same way it can for `*.op.ts` files, and so discovery does
|
|
28
28
|
* not have to import (and risk side-effecting) every `.ts` file in the
|
|
29
29
|
* project looking for stray `Component`-shaped objects.
|
|
30
|
+
*
|
|
31
|
+
* chant #1051 — `discoverComponents` unconditionally `await import()`s every
|
|
32
|
+
* discovered file in the CLI's own process, the same exposure #1045 Phase 2
|
|
33
|
+
* closed for lexicon-resource discovery (`../discovery/index.ts`'s
|
|
34
|
+
* `discover({ sandbox: true })`). `{ sandbox: true }` here runs that import
|
|
35
|
+
* (and the duplicate-name collection below) together, isolated, in one
|
|
36
|
+
* sandboxed child process instead — see `./sandbox/driver.ts`/`./sandbox/
|
|
37
|
+
* run.ts`. Materially simpler than the entity path: a `Component` is plain
|
|
38
|
+
* JSON (no `AttrRef` cross-references), so the child can hand back its result
|
|
39
|
+
* with a bare `JSON.stringify` — no entity-wire codec needed (`projectToJson`,
|
|
40
|
+
* `./component.ts`, already proves the plain-JSON round-trip).
|
|
30
41
|
*/
|
|
31
42
|
|
|
32
43
|
import { readdir } from "node:fs/promises";
|
|
@@ -52,6 +63,26 @@ export interface ComponentDiscoveryResult {
|
|
|
52
63
|
errors: DiscoveryError[];
|
|
53
64
|
}
|
|
54
65
|
|
|
66
|
+
/** Options for {@link discoverComponents}. */
|
|
67
|
+
export interface ComponentDiscoveryOptions {
|
|
68
|
+
/**
|
|
69
|
+
* chant #1051 — opt-in: import every discovered `*.component.ts` file
|
|
70
|
+
* together, isolated, in one sandboxed child process (`./sandbox/run.ts`)
|
|
71
|
+
* instead of in this (the CLI's own) process. Mirrors `discover({ sandbox:
|
|
72
|
+
* true })` (`../discovery/index.ts`, chant #1045 Phase 2) for the parallel
|
|
73
|
+
* `*.component.ts` convention. Default `false` — behavior, including
|
|
74
|
+
* performance (no bundling, no child process, no IPC), is unchanged unless
|
|
75
|
+
* requested.
|
|
76
|
+
*/
|
|
77
|
+
sandbox?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One already-imported `*.component.ts` module — the input to {@link collectComponents}. */
|
|
81
|
+
export interface ImportedComponentModule {
|
|
82
|
+
file: string;
|
|
83
|
+
exports: Record<string, unknown>;
|
|
84
|
+
}
|
|
85
|
+
|
|
55
86
|
/**
|
|
56
87
|
* Recursively find every `*.component.ts` file under `path`, the same
|
|
57
88
|
* child-project boundary rule `findInfraFiles` (`../discovery/files.ts`)
|
|
@@ -106,36 +137,35 @@ async function findComponentFiles(path: string): Promise<string[]> {
|
|
|
106
137
|
}
|
|
107
138
|
|
|
108
139
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
140
|
+
* Collect every `Component`-shaped export from already-imported `modules`,
|
|
141
|
+
* enforcing the duplicate-`component.name` rule (across files, or within one
|
|
142
|
+
* file): a discovery error, matching `collectEntities`'s duplicate-export
|
|
143
|
+
* handling and `discoverOps`'s duplicate-Op-name handling — except the
|
|
144
|
+
* identity compared is the schema-level `name` field, since two different
|
|
145
|
+
* export bindings could otherwise declare the same component name and
|
|
146
|
+
* silently collide at deploy time. `existing.component !== value` — object
|
|
147
|
+
* identity, not deep equality — is what lets the SAME component re-exported
|
|
148
|
+
* under several bindings pass through without tripping the duplicate check.
|
|
149
|
+
*
|
|
150
|
+
* Split out from {@link discoverComponents} (#1051) precisely because that
|
|
151
|
+
* identity check has to run on the LIVE, still-in-memory export values —
|
|
152
|
+
* identity does not survive a process boundary. That means this function
|
|
153
|
+
* must run wherever `modules` were actually imported: in this process for
|
|
154
|
+
* the default (unsandboxed) path below, or inside the sandboxed child for
|
|
155
|
+
* `{ sandbox: true }` (`./sandbox/driver.ts`, which bundles and calls this
|
|
156
|
+
* same function), mirroring how `collectEntities` runs inside `../discovery/
|
|
157
|
+
* sandbox/driver.ts` for the lexicon-resource run-fallback set (chant #1045
|
|
158
|
+
* Phase 2).
|
|
119
159
|
*/
|
|
120
|
-
export
|
|
160
|
+
export function collectComponents(modules: readonly ImportedComponentModule[]): {
|
|
161
|
+
components: Map<string, DiscoveredComponent>;
|
|
162
|
+
errors: DiscoveryError[];
|
|
163
|
+
} {
|
|
121
164
|
const errors: DiscoveryError[] = [];
|
|
122
|
-
const sourceFiles = await findComponentFiles(path);
|
|
123
165
|
const components = new Map<string, DiscoveredComponent>();
|
|
124
166
|
const nameToFile = new Map<string, string>();
|
|
125
167
|
|
|
126
|
-
for (const filePath of
|
|
127
|
-
let exports: Record<string, unknown>;
|
|
128
|
-
try {
|
|
129
|
-
// Resolve to an absolute file:// URL before importing. A relative
|
|
130
|
-
// `filePath` (e.g. from `chant build --components --generate`, which passes
|
|
131
|
-
// a bare "." path) would otherwise be treated by `import()` as a bare
|
|
132
|
-
// package specifier and fail with "Cannot find package 'x.component.ts'".
|
|
133
|
-
exports = await import(pathToFileURL(resolve(filePath)).href);
|
|
134
|
-
} catch (err) {
|
|
135
|
-
errors.push(new DiscoveryError(filePath, err instanceof Error ? err.message : String(err), "import"));
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
|
|
168
|
+
for (const { file: filePath, exports } of modules) {
|
|
139
169
|
for (const [exportName, value] of Object.entries(exports)) {
|
|
140
170
|
if (!isComponent(value)) continue;
|
|
141
171
|
|
|
@@ -163,5 +193,77 @@ export async function discoverComponents(path: string): Promise<ComponentDiscove
|
|
|
163
193
|
}
|
|
164
194
|
}
|
|
165
195
|
|
|
196
|
+
return { components, errors };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Import every already-discovered `*.component.ts` file in THIS process (the
|
|
201
|
+
* unsandboxed default) and collect their `Component`-shaped exports.
|
|
202
|
+
*/
|
|
203
|
+
async function importAndCollectComponents(
|
|
204
|
+
sourceFiles: readonly string[],
|
|
205
|
+
): Promise<{ components: Map<string, DiscoveredComponent>; errors: DiscoveryError[] }> {
|
|
206
|
+
const modules: ImportedComponentModule[] = [];
|
|
207
|
+
const importErrors: DiscoveryError[] = [];
|
|
208
|
+
|
|
209
|
+
for (const filePath of sourceFiles) {
|
|
210
|
+
try {
|
|
211
|
+
// Resolve to an absolute file:// URL before importing. A relative
|
|
212
|
+
// `filePath` (e.g. from `chant build --components --generate`, which passes
|
|
213
|
+
// a bare "." path) would otherwise be treated by `import()` as a bare
|
|
214
|
+
// package specifier and fail with "Cannot find package 'x.component.ts'".
|
|
215
|
+
const exports = await import(pathToFileURL(resolve(filePath)).href);
|
|
216
|
+
modules.push({ file: filePath, exports });
|
|
217
|
+
} catch (err) {
|
|
218
|
+
importErrors.push(new DiscoveryError(filePath, err instanceof Error ? err.message : String(err), "import"));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const { components, errors: collectErrors } = collectComponents(modules);
|
|
223
|
+
return { components, errors: [...importErrors, ...collectErrors] };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Discover every `Component` declared under `path`: scan for `*.component.ts`
|
|
228
|
+
* files, import each, and collect every export whose value satisfies
|
|
229
|
+
* `isComponent` (any export name — not just `default` — so a file may
|
|
230
|
+
* declare several related components, matching how a resource file can
|
|
231
|
+
* export several `Declarable`s). See {@link collectComponents} for the
|
|
232
|
+
* duplicate-name rule.
|
|
233
|
+
*
|
|
234
|
+
* `{ sandbox: true }` (#1051) imports every file together, isolated, in one
|
|
235
|
+
* sandboxed child process instead (`./sandbox/run.ts`) — the same isolation
|
|
236
|
+
* `discover({ sandbox: true })` (`../discovery/index.ts`) gives lexicon
|
|
237
|
+
* resources, chant #1045 Phase 2.
|
|
238
|
+
*/
|
|
239
|
+
export async function discoverComponents(
|
|
240
|
+
path: string,
|
|
241
|
+
options?: ComponentDiscoveryOptions,
|
|
242
|
+
): Promise<ComponentDiscoveryResult> {
|
|
243
|
+
const sourceFiles = await findComponentFiles(path);
|
|
244
|
+
|
|
245
|
+
if (options?.sandbox && sourceFiles.length > 0) {
|
|
246
|
+
// Dynamic, not static — for the same reason `../discovery/index.ts`
|
|
247
|
+
// dynamically imports its own `./sandbox/run`: that module (transitively)
|
|
248
|
+
// imports `esbuild`, a large package with its own module-scope filesystem
|
|
249
|
+
// access, and this module is reachable from `@intentius/chant`'s package
|
|
250
|
+
// root, which project source commonly imports. A static import here would
|
|
251
|
+
// make `esbuild` bundled and eagerly evaluated by any build of a project
|
|
252
|
+
// that itself imports chant. A dynamic import is only ever actually
|
|
253
|
+
// reached here, at runtime, when a caller opts into `sandbox: true`.
|
|
254
|
+
try {
|
|
255
|
+
const { discoverComponentsSandboxed } = await import("./sandbox/run");
|
|
256
|
+
const result = await discoverComponentsSandboxed(sourceFiles, path);
|
|
257
|
+
return { components: result.components, sourceFiles, errors: result.errors };
|
|
258
|
+
} catch (err) {
|
|
259
|
+
return {
|
|
260
|
+
components: new Map(),
|
|
261
|
+
sourceFiles,
|
|
262
|
+
errors: [new DiscoveryError(path, err instanceof Error ? err.message : String(err), "resolution")],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const { components, errors } = await importAndCollectComponents(sourceFiles);
|
|
166
268
|
return { components, sourceFiles, errors };
|
|
167
269
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* chant #1051 — generates the source of the "driver" module that runs INSIDE
|
|
5
|
+
* the sandboxed child for component discovery. Mirrors `../../discovery/
|
|
6
|
+
* sandbox/driver.ts`'s shape (chant #1045 Phase 2) but is materially simpler:
|
|
7
|
+
* a `Component` is plain JSON (no `AttrRef` cross-references to resolve), so
|
|
8
|
+
* there is no equivalent of `resolveAttrRefs`/`encodeEntitySet` here — the
|
|
9
|
+
* driver hands back its collected components with a bare, structural
|
|
10
|
+
* `JSON.stringify` over the IPC channel (see `./run.ts`'s `send`).
|
|
11
|
+
*
|
|
12
|
+
* Like the entity driver, every file is imported via a LITERAL string
|
|
13
|
+
* specifier dynamic `import()` — not a variable — so esbuild's bundler can
|
|
14
|
+
* trace and inline it into ONE self-contained module graph (see `../../
|
|
15
|
+
* discovery/sandbox/driver.ts`'s doc for why that's verified to lower to a
|
|
16
|
+
* lazily-invoked local module initializer, not a runtime resolution). That
|
|
17
|
+
* single module graph is what lets `collectComponents`'s duplicate-name check
|
|
18
|
+
* (`../discover.ts`) run over real, live object identity — "the same
|
|
19
|
+
* component re-exported under two bindings" only detects correctly when both
|
|
20
|
+
* bindings really do resolve to the same in-memory object, which requires
|
|
21
|
+
* every `*.component.ts` file for one discovery call to share one module
|
|
22
|
+
* cache inside a single child (see `../discover.ts`'s `collectComponents` doc
|
|
23
|
+
* for the full identity-correctness argument).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const HERE = import.meta.dirname;
|
|
27
|
+
const COMPONENTS_DIR = join(HERE, "..");
|
|
28
|
+
|
|
29
|
+
/** Absolute paths to chant's OWN trusted modules the generated driver imports — resolved relative to THIS file's own location on disk, exactly like `../../discovery/sandbox/driver.ts` does for the entity path (works whether chant runs from the monorepo or a consumer's `node_modules`). */
|
|
30
|
+
const DISCOVER_MODULE = join(COMPONENTS_DIR, "discover.ts");
|
|
31
|
+
const CHILD_ERRORS_MODULE = join(COMPONENTS_DIR, "..", "discovery", "sandbox", "child-errors.ts");
|
|
32
|
+
|
|
33
|
+
export interface GenerateComponentDriverOptions {
|
|
34
|
+
/** Absolute paths to every discovered `*.component.ts` file for this build. */
|
|
35
|
+
files: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Escape a value for embedding as a JS/TS source-level literal. */
|
|
39
|
+
function lit(value: unknown): string {
|
|
40
|
+
return JSON.stringify(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generate the component driver module's full TypeScript source. Written to
|
|
45
|
+
* a tmp file and bundled (`../../discovery/sandbox/bundle.ts`'s `bundleDriver`
|
|
46
|
+
* — generic, reused as-is) before being handed to a sandboxed child — never
|
|
47
|
+
* executed directly by the parent process.
|
|
48
|
+
*/
|
|
49
|
+
export function generateComponentDriverSource(options: GenerateComponentDriverOptions): string {
|
|
50
|
+
const { files } = options;
|
|
51
|
+
|
|
52
|
+
const lines: string[] = [
|
|
53
|
+
`import { collectComponents } from ${lit(DISCOVER_MODULE)};`,
|
|
54
|
+
`import { classifyChildError } from ${lit(CHILD_ERRORS_MODULE)};`,
|
|
55
|
+
``,
|
|
56
|
+
`function send(payload) {`,
|
|
57
|
+
` if (typeof process.send === "function") process.send(payload);`,
|
|
58
|
+
` else console.log(JSON.stringify(payload));`,
|
|
59
|
+
`}`,
|
|
60
|
+
``,
|
|
61
|
+
`async function main() {`,
|
|
62
|
+
` const modules = [];`,
|
|
63
|
+
` const errors = [];`,
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
for (const file of files) {
|
|
67
|
+
// `file` (as discovered by `findComponentFiles`, `../discover.ts`) may be
|
|
68
|
+
// relative to whatever cwd-relative `path` `discoverComponents` was
|
|
69
|
+
// called with — `chant list --components examples/foo` never resolves
|
|
70
|
+
// its argument before discovery, matching the unsandboxed import loop's
|
|
71
|
+
// own tolerance for that (`pathToFileURL(resolve(filePath)).href`, see
|
|
72
|
+
// `../discover.ts`'s `importAndCollectComponents`). A bare relative
|
|
73
|
+
// string hits Node's *bare-specifier* (package) resolution instead of
|
|
74
|
+
// being treated as a path, so only the import target — never the `file`
|
|
75
|
+
// key `collectComponents` dedupes and reports by — is resolved to an
|
|
76
|
+
// absolute filesystem path here, in THIS (parent) process, before being
|
|
77
|
+
// embedded as a literal in the bundled child (mirrors `../../discovery/
|
|
78
|
+
// sandbox/driver.ts`'s own literal-specifier `import()`s, which are
|
|
79
|
+
// likewise plain absolute paths, not `file://` URLs — kept that way
|
|
80
|
+
// rather than converting to a URL since esbuild's bundler is only
|
|
81
|
+
// verified to trace/inline a literal filesystem-path specifier).
|
|
82
|
+
const absoluteFile = resolve(file);
|
|
83
|
+
lines.push(
|
|
84
|
+
` try {`,
|
|
85
|
+
` const mod = await import(${lit(absoluteFile)});`,
|
|
86
|
+
` modules.push({ file: ${lit(file)}, exports: mod });`,
|
|
87
|
+
` } catch (err) {`,
|
|
88
|
+
` errors.push(classifyChildError(${lit(file)}, err).toJSON());`,
|
|
89
|
+
` }`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
lines.push(
|
|
94
|
+
``,
|
|
95
|
+
` const collected = collectComponents(modules);`,
|
|
96
|
+
` for (const err of collected.errors) errors.push(err.toJSON());`,
|
|
97
|
+
``,
|
|
98
|
+
// Plain JSON: no entity-wire codec needed (see this module's doc + chant
|
|
99
|
+
// #1051's issue — `projectToJson`, ../component.ts, already proves the
|
|
100
|
+
// bare JSON.parse(JSON.stringify()) round-trip for a Component).
|
|
101
|
+
` const components = [];`,
|
|
102
|
+
` for (const [name, discovered] of collected.components) {`,
|
|
103
|
+
` components.push({ name, component: discovered.component, exportName: discovered.exportName, filePath: discovered.filePath });`,
|
|
104
|
+
` }`,
|
|
105
|
+
` send({ components, errors });`,
|
|
106
|
+
`}`,
|
|
107
|
+
``,
|
|
108
|
+
`main().catch((err) => {`,
|
|
109
|
+
` send({ components: [], errors: [classifyChildError("", err).toJSON()], fatal: true });`,
|
|
110
|
+
`});`,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdir, writeFile, rm, realpath } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { discoverComponentsSandboxed } from "./run";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* chant #1051 — proves the actual isolation properties for sandboxed
|
|
10
|
+
* component discovery (not just that it returns the right components),
|
|
11
|
+
* mirroring `../../discovery/sandbox/run.test.ts`'s coverage for the
|
|
12
|
+
* lexicon-resource run-fallback path (chant #1045 Phase 2): a hostile
|
|
13
|
+
* `*.component.ts` file cannot read outside the project directory, write
|
|
14
|
+
* anywhere, spawn a process, or read the ambient environment, and a
|
|
15
|
+
* permission denial names the file and the operation rather than leaking a
|
|
16
|
+
* raw `ERR_ACCESS_DENIED`.
|
|
17
|
+
*
|
|
18
|
+
* Fixtures are written to a fresh tmpdir per test, never into the source
|
|
19
|
+
* tree.
|
|
20
|
+
*/
|
|
21
|
+
describe("discoverComponentsSandboxed — isolation", () => {
|
|
22
|
+
let testDir: string;
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
testDir = await realpath(
|
|
26
|
+
await (async () => {
|
|
27
|
+
const dir = join(tmpdir(), `chant-component-sandbox-run-test-${Date.now()}-${Math.random()}`);
|
|
28
|
+
await mkdir(dir, { recursive: true });
|
|
29
|
+
return dir;
|
|
30
|
+
})(),
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(async () => {
|
|
35
|
+
await rm(testDir, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("denies filesystem reads outside the project directory, naming the file and the operation", async () => {
|
|
39
|
+
const file = join(testDir, "evil.component.ts");
|
|
40
|
+
await writeFile(
|
|
41
|
+
file,
|
|
42
|
+
`
|
|
43
|
+
import { readFileSync } from "node:fs";
|
|
44
|
+
readFileSync("/etc/hosts", "utf-8");
|
|
45
|
+
export const evil = { name: "evil", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "shell" }] }] };
|
|
46
|
+
`,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const result = await discoverComponentsSandboxed([file], testDir);
|
|
50
|
+
|
|
51
|
+
expect(result.components.size).toBe(0);
|
|
52
|
+
expect(result.errors).toHaveLength(1);
|
|
53
|
+
expect(result.errors[0].file).toBe(file);
|
|
54
|
+
expect(result.errors[0].type).toBe("permission");
|
|
55
|
+
expect(result.errors[0].message).toMatch(/FileSystemRead/);
|
|
56
|
+
expect(result.errors[0].message).toContain(file);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("denies filesystem writes anywhere", async () => {
|
|
60
|
+
const file = join(testDir, "evil.component.ts");
|
|
61
|
+
const targetPath = join(testDir, "..", "escaped.txt");
|
|
62
|
+
await writeFile(
|
|
63
|
+
file,
|
|
64
|
+
`
|
|
65
|
+
import { writeFileSync } from "node:fs";
|
|
66
|
+
writeFileSync(${JSON.stringify(targetPath)}, "pwned");
|
|
67
|
+
export const evil = { name: "evil", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "shell" }] }] };
|
|
68
|
+
`,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const result = await discoverComponentsSandboxed([file], testDir);
|
|
72
|
+
|
|
73
|
+
expect(result.errors).toHaveLength(1);
|
|
74
|
+
expect(result.errors[0].type).toBe("permission");
|
|
75
|
+
expect(result.errors[0].message).toMatch(/FileSystemWrite/);
|
|
76
|
+
expect(existsSync(targetPath)).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("denies spawning a child process", async () => {
|
|
80
|
+
const file = join(testDir, "evil.component.ts");
|
|
81
|
+
const markerPath = join(testDir, "spawned.txt");
|
|
82
|
+
await writeFile(
|
|
83
|
+
file,
|
|
84
|
+
`
|
|
85
|
+
import { execSync } from "node:child_process";
|
|
86
|
+
execSync(${JSON.stringify(`touch ${markerPath}`)});
|
|
87
|
+
export const evil = { name: "evil", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "shell" }] }] };
|
|
88
|
+
`,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const result = await discoverComponentsSandboxed([file], testDir);
|
|
92
|
+
|
|
93
|
+
expect(result.errors).toHaveLength(1);
|
|
94
|
+
expect(result.errors[0].type).toBe("permission");
|
|
95
|
+
expect(result.errors[0].message).toMatch(/ChildProcess/);
|
|
96
|
+
expect(existsSync(markerPath)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("scrubs the ambient environment — a real secret set on the parent's process.env is invisible to project source", async () => {
|
|
100
|
+
const file = join(testDir, "evil.component.ts");
|
|
101
|
+
await writeFile(
|
|
102
|
+
file,
|
|
103
|
+
`
|
|
104
|
+
const seen = process.env.CHANT_SANDBOX_TEST_SECRET;
|
|
105
|
+
if (seen !== undefined) {
|
|
106
|
+
throw new Error("ambient env leaked: " + seen);
|
|
107
|
+
}
|
|
108
|
+
export const evil = {
|
|
109
|
+
name: "evil",
|
|
110
|
+
dependsOn: [],
|
|
111
|
+
deploy: [{ phase: "Apply", steps: [{ kind: "shell", envKeyCount: Object.keys(process.env).length }] }],
|
|
112
|
+
};
|
|
113
|
+
`,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const previous = process.env.CHANT_SANDBOX_TEST_SECRET;
|
|
117
|
+
process.env.CHANT_SANDBOX_TEST_SECRET = "super-secret-should-not-cross-the-boundary";
|
|
118
|
+
try {
|
|
119
|
+
const result = await discoverComponentsSandboxed([file], testDir);
|
|
120
|
+
expect(result.errors).toEqual([]);
|
|
121
|
+
expect(result.components.has("evil")).toBe(true);
|
|
122
|
+
} finally {
|
|
123
|
+
if (previous === undefined) delete process.env.CHANT_SANDBOX_TEST_SECRET;
|
|
124
|
+
else process.env.CHANT_SANDBOX_TEST_SECRET = previous;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("the same component object re-exported under two bindings in one file is NOT a duplicate — the identity check runs on live objects, before any serialization", async () => {
|
|
129
|
+
// `collectComponents`'s dedupe check (`../discover.ts`) compares
|
|
130
|
+
// candidates with `existing.component !== value` — real object identity,
|
|
131
|
+
// not deep equality. That distinction only matters if the check runs
|
|
132
|
+
// over the LIVE, still-in-memory export values: had this instead run
|
|
133
|
+
// over a JSON-round-tripped copy (e.g. serialize each file's exports in
|
|
134
|
+
// the child, decode in the parent, THEN dedupe), `JSON.parse` never
|
|
135
|
+
// returns the same reference twice, so even this legitimate case — one
|
|
136
|
+
// object, two export bindings — would wrongly report a duplicate. This
|
|
137
|
+
// proves the sandboxed path preserves the unsandboxed behavior: the
|
|
138
|
+
// dedupe collector (`collectComponents`) runs bundled INSIDE the child,
|
|
139
|
+
// over the driver's own live `modules` array, before the one
|
|
140
|
+
// `JSON.stringify` at the very end (`./driver.ts`).
|
|
141
|
+
const file = join(testDir, "aliased.component.ts");
|
|
142
|
+
await writeFile(
|
|
143
|
+
file,
|
|
144
|
+
`
|
|
145
|
+
const base = {
|
|
146
|
+
name: "search-service",
|
|
147
|
+
dependsOn: [],
|
|
148
|
+
deploy: [{ phase: "Apply", steps: [{ kind: "shell" }] }],
|
|
149
|
+
};
|
|
150
|
+
export const searchService = base;
|
|
151
|
+
export const searchServiceAlias = base;
|
|
152
|
+
`,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const result = await discoverComponentsSandboxed([file], testDir);
|
|
156
|
+
|
|
157
|
+
expect(result.errors).toEqual([]);
|
|
158
|
+
expect(result.components.size).toBe(1);
|
|
159
|
+
expect(result.components.has("search-service")).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("a genuine duplicate — two DIFFERENT objects declaring the same component name — is still reported", async () => {
|
|
163
|
+
await writeFile(
|
|
164
|
+
join(testDir, "one.component.ts"),
|
|
165
|
+
`
|
|
166
|
+
export const svc = { name: "dup", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "shell" }] }] };
|
|
167
|
+
`,
|
|
168
|
+
);
|
|
169
|
+
await writeFile(
|
|
170
|
+
join(testDir, "two.component.ts"),
|
|
171
|
+
`
|
|
172
|
+
export const svcAgain = { name: "dup", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "shell" }] }] };
|
|
173
|
+
`,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const result = await discoverComponentsSandboxed(
|
|
177
|
+
[join(testDir, "one.component.ts"), join(testDir, "two.component.ts")],
|
|
178
|
+
testDir,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
182
|
+
expect(result.errors[0].type).toBe("resolution");
|
|
183
|
+
expect(result.errors[0].message).toMatch(/Duplicate component name "dup"/);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { fork } from "node:child_process";
|
|
2
|
+
import { realpathSync, rmSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { bundleDriver } from "../../discovery/sandbox/bundle";
|
|
5
|
+
import { classifyChildError } from "../../discovery/sandbox/child-errors";
|
|
6
|
+
import { DiscoveryError, type DiscoveryErrorType } from "../../errors";
|
|
7
|
+
import type { Component } from "../component";
|
|
8
|
+
import type { DiscoveredComponent } from "../discover";
|
|
9
|
+
import { generateComponentDriverSource } from "./driver";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* chant #1051 — imports every discovered `*.component.ts` file for a build
|
|
13
|
+
* TOGETHER, as one bundled module graph, inside one sandboxed child process,
|
|
14
|
+
* and returns the same shape `discoverComponents`'s own in-process import
|
|
15
|
+
* loop would have produced: named components plus any errors.
|
|
16
|
+
*
|
|
17
|
+
* Isolation mechanics are identical to `../../discovery/sandbox/run.ts`'s
|
|
18
|
+
* `runFallbackFilesSandboxed` (chant #1045 Phase 2 — see that module's doc
|
|
19
|
+
* for the full write-up, verified on Node v24.13.1): `--permission
|
|
20
|
+
* --allow-fs-read=<bundle dir>,<project dir>[,<trusted external package
|
|
21
|
+
* dirs>]`, a spawn-time-scrubbed `env`, and a bounded wait before the child
|
|
22
|
+
* is treated as hung. `bundleDriver` (`../../discovery/sandbox/bundle.ts`) is
|
|
23
|
+
* reused as-is — it is generic over what the driver does, so the same
|
|
24
|
+
* esbuild bundling (and the same `typescript`-stays-external carve-out) that
|
|
25
|
+
* serves the entity path serves this one too.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export interface ComponentSandboxRunResult {
|
|
29
|
+
/** Discovered components from this sandboxed import, keyed by `component.name` — functionally indistinguishable from what `discoverComponents`'s in-process path would have produced for this same file set. */
|
|
30
|
+
components: Map<string, DiscoveredComponent>;
|
|
31
|
+
/** Import/collection/permission errors, already chant-shaped (`../../discovery/sandbox/child-errors.ts` — a permission denial names the file and the operation, never a raw `ERR_ACCESS_DENIED`). */
|
|
32
|
+
errors: DiscoveryError[];
|
|
33
|
+
/** esbuild bundling wall-clock time. */
|
|
34
|
+
bundleMs: number;
|
|
35
|
+
/** Bundle size in bytes. */
|
|
36
|
+
bundleBytes: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ChildComponentEntry {
|
|
40
|
+
name: string;
|
|
41
|
+
component: Component;
|
|
42
|
+
exportName: string;
|
|
43
|
+
filePath: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ChildResponse {
|
|
47
|
+
components: ChildComponentEntry[];
|
|
48
|
+
errors: Array<{ file: string; message: string; type: DiscoveryErrorType }>;
|
|
49
|
+
fatal?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** How long to wait for the sandboxed child to report back before treating it as hung and killing it. Generous, matching the entity path's own budget. */
|
|
53
|
+
const CHILD_TIMEOUT_MS = 120_000;
|
|
54
|
+
|
|
55
|
+
function isChildResponse(value: unknown): value is ChildResponse {
|
|
56
|
+
return typeof value === "object" && value !== null && "components" in value && "errors" in value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Import `files` (every discovered `*.component.ts` file) together, isolated,
|
|
61
|
+
* in one sandboxed child process, and collect their `Component`-shaped
|
|
62
|
+
* exports — including the duplicate-name check, which runs INSIDE the child
|
|
63
|
+
* over live objects before anything is serialized (`../discover.ts`'s
|
|
64
|
+
* `collectComponents`, bundled into the driver).
|
|
65
|
+
*
|
|
66
|
+
* @param files - Absolute paths to every discovered `*.component.ts` file.
|
|
67
|
+
* @param buildRoot - The directory `discoverComponents` was pointed at — used
|
|
68
|
+
* to compute the project-directory read allowance, mirroring `../../
|
|
69
|
+
* discovery/sandbox/run.ts`.
|
|
70
|
+
*/
|
|
71
|
+
export async function discoverComponentsSandboxed(
|
|
72
|
+
files: readonly string[],
|
|
73
|
+
buildRoot: string,
|
|
74
|
+
): Promise<ComponentSandboxRunResult> {
|
|
75
|
+
if (files.length === 0) {
|
|
76
|
+
return { components: new Map(), errors: [], bundleMs: 0, bundleBytes: 0 };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const driverSource = generateComponentDriverSource({ files });
|
|
80
|
+
const { bundlePath, bundleDir, externalReadPaths, durationMs, bytes } = await bundleDriver(driverSource);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
let projectRealpath: string;
|
|
84
|
+
try {
|
|
85
|
+
projectRealpath = realpathSync(resolve(buildRoot));
|
|
86
|
+
} catch {
|
|
87
|
+
projectRealpath = resolve(buildRoot);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const response = await runChildProcess(bundlePath, bundleDir, projectRealpath, externalReadPaths);
|
|
91
|
+
|
|
92
|
+
const errors = (response.errors ?? []).map((e) => new DiscoveryError(e.file, e.message, e.type));
|
|
93
|
+
if (response.fatal) {
|
|
94
|
+
return { components: new Map(), errors, bundleMs: durationMs, bundleBytes: bytes };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const components = new Map<string, DiscoveredComponent>();
|
|
98
|
+
for (const entry of response.components ?? []) {
|
|
99
|
+
components.set(entry.name, {
|
|
100
|
+
component: entry.component,
|
|
101
|
+
exportName: entry.exportName,
|
|
102
|
+
filePath: entry.filePath,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return { components, errors, bundleMs: durationMs, bundleBytes: bytes };
|
|
106
|
+
} catch (err) {
|
|
107
|
+
return {
|
|
108
|
+
components: new Map(),
|
|
109
|
+
errors: [classifyChildError("", err, "import")],
|
|
110
|
+
bundleMs: durationMs,
|
|
111
|
+
bundleBytes: bytes,
|
|
112
|
+
};
|
|
113
|
+
} finally {
|
|
114
|
+
rmSync(bundleDir, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Fork the bundle under `--permission`, with a scrubbed environment, and resolve with its one IPC message (or reject on crash/timeout/fork error). Mirrors `../../discovery/sandbox/run.ts`'s `runChildProcess`. */
|
|
119
|
+
function runChildProcess(
|
|
120
|
+
bundlePath: string,
|
|
121
|
+
bundleDir: string,
|
|
122
|
+
projectRealpath: string,
|
|
123
|
+
externalReadPaths: readonly string[],
|
|
124
|
+
): Promise<ChildResponse> {
|
|
125
|
+
return new Promise((resolvePromise, reject) => {
|
|
126
|
+
const readAllowances = [bundleDir, projectRealpath, ...externalReadPaths].map(
|
|
127
|
+
(p) => `--allow-fs-read=${p}`,
|
|
128
|
+
);
|
|
129
|
+
const child = fork(bundlePath, [], {
|
|
130
|
+
execArgv: ["--permission", ...readAllowances],
|
|
131
|
+
// chant #1051 — Node's Permission Model does not gate `process.env`;
|
|
132
|
+
// scrubbing it here is the only way to keep the ambient environment out
|
|
133
|
+
// of untrusted project source's reach, exactly as the entity path does.
|
|
134
|
+
env: { PATH: process.env.PATH ?? "" },
|
|
135
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
let settled = false;
|
|
139
|
+
let stderrBuf = "";
|
|
140
|
+
|
|
141
|
+
const timeout = setTimeout(() => {
|
|
142
|
+
if (settled) return;
|
|
143
|
+
settled = true;
|
|
144
|
+
child.kill();
|
|
145
|
+
reject(new Error(`sandboxed component discovery timed out after ${CHILD_TIMEOUT_MS}ms`));
|
|
146
|
+
}, CHILD_TIMEOUT_MS);
|
|
147
|
+
|
|
148
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
149
|
+
stderrBuf += chunk.toString();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
child.on("message", (msg: unknown) => {
|
|
153
|
+
if (settled || !isChildResponse(msg)) return;
|
|
154
|
+
settled = true;
|
|
155
|
+
clearTimeout(timeout);
|
|
156
|
+
resolvePromise(msg);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
child.on("error", (err) => {
|
|
160
|
+
if (settled) return;
|
|
161
|
+
settled = true;
|
|
162
|
+
clearTimeout(timeout);
|
|
163
|
+
reject(err);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
child.on("exit", (code, signal) => {
|
|
167
|
+
if (settled) return;
|
|
168
|
+
settled = true;
|
|
169
|
+
clearTimeout(timeout);
|
|
170
|
+
reject(
|
|
171
|
+
new Error(
|
|
172
|
+
`sandboxed component discovery child exited before reporting results (code ${code}, signal ${signal})${stderrBuf.trim() ? `: ${stderrBuf.trim()}` : ""}`,
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|