@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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { discover } from "./index";
|
|
3
3
|
import { mkdir, writeFile, rm } from "node:fs/promises";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { join, dirname, resolve } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { DECLARABLE_MARKER } from "../declarable";
|
|
7
8
|
|
|
8
9
|
describe("discover", () => {
|
|
@@ -270,3 +271,192 @@ describe("discover", () => {
|
|
|
270
271
|
expect(deps2?.size).toBe(0);
|
|
271
272
|
});
|
|
272
273
|
});
|
|
274
|
+
|
|
275
|
+
describe("discover — fold mode (#1022, epic #1019)", () => {
|
|
276
|
+
let testDir: string;
|
|
277
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
278
|
+
const runtimePath = resolve(thisDir, "../runtime");
|
|
279
|
+
const compositePath = resolve(thisDir, "../composite");
|
|
280
|
+
|
|
281
|
+
beforeEach(async () => {
|
|
282
|
+
testDir = join(tmpdir(), `chant-discover-fold-test-${Date.now()}-${Math.random()}`);
|
|
283
|
+
await mkdir(testDir, { recursive: true });
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
afterEach(async () => {
|
|
287
|
+
await rm(testDir, { recursive: true, force: true });
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("default (flag omitted) leaves foldDecisions empty and behavior unchanged", async () => {
|
|
291
|
+
await writeFile(
|
|
292
|
+
join(testDir, "app.ts"),
|
|
293
|
+
`
|
|
294
|
+
export const entity = {
|
|
295
|
+
entityType: "Entity",
|
|
296
|
+
[Symbol.for("chant.declarable")]: true,
|
|
297
|
+
};
|
|
298
|
+
`,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const result = await discover(testDir);
|
|
302
|
+
|
|
303
|
+
expect(result.entities.size).toBe(1);
|
|
304
|
+
expect(result.foldDecisions).toEqual([]);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("folds a leaf-only module with zero execution (throw sentinel never fires)", async () => {
|
|
308
|
+
await writeFile(
|
|
309
|
+
join(testDir, "resources.ts"),
|
|
310
|
+
`
|
|
311
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
312
|
+
export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
313
|
+
`,
|
|
314
|
+
);
|
|
315
|
+
await writeFile(
|
|
316
|
+
join(testDir, "main.ts"),
|
|
317
|
+
`
|
|
318
|
+
import { Bucket } from "./resources";
|
|
319
|
+
throw new Error("must never execute — sentinel for #1022 fold verification");
|
|
320
|
+
export const bucket = new Bucket({ name: "my-bucket" });
|
|
321
|
+
`,
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
// Sanity check: running this directory the normal way really does fire
|
|
325
|
+
// the sentinel (a collected import error), proving fold isn't just
|
|
326
|
+
// accidentally skipping a no-op file.
|
|
327
|
+
const ran = await discover(testDir);
|
|
328
|
+
expect(ran.errors.some((e) => e.message.includes("must never execute"))).toBe(true);
|
|
329
|
+
|
|
330
|
+
const result = await discover(testDir, { fold: true });
|
|
331
|
+
|
|
332
|
+
expect(result.errors).toEqual([]);
|
|
333
|
+
expect(result.entities.size).toBe(1);
|
|
334
|
+
expect(result.entities.has("bucket")).toBe(true);
|
|
335
|
+
const bucket = result.entities.get("bucket")!;
|
|
336
|
+
expect((bucket as unknown as { lexicon: string }).lexicon).toBe("aws");
|
|
337
|
+
expect((bucket as unknown as { entityType: string }).entityType).toBe("Test::Bucket");
|
|
338
|
+
|
|
339
|
+
expect(result.foldDecisions).toHaveLength(2);
|
|
340
|
+
const mainDecision = result.foldDecisions.find((d) => d.file.endsWith("main.ts"));
|
|
341
|
+
expect(mainDecision?.mode).toBe("fold");
|
|
342
|
+
expect(mainDecision?.resourceCount).toBe(1);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// chant #1023 (epic #1019 Phase 5): a bare composite factory call now
|
|
346
|
+
// folds too — the factory is resolved through the file's imports and
|
|
347
|
+
// invoked for real with statically-folded props, exactly as a resource
|
|
348
|
+
// constructor already was (#1022). Fold and run must still agree
|
|
349
|
+
// byte-for-byte on the expanded entities (this is the unit-level version
|
|
350
|
+
// of the #1025 fold-vs-run differential).
|
|
351
|
+
test("a module instantiating a composite folds too; output is unchanged", async () => {
|
|
352
|
+
await writeFile(
|
|
353
|
+
join(testDir, "composites.ts"),
|
|
354
|
+
`
|
|
355
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
356
|
+
import { Composite } from ${JSON.stringify(compositePath)};
|
|
357
|
+
const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
358
|
+
export const MyStack = Composite<{ name: string }>((props) => {
|
|
359
|
+
const bucket = new Bucket({ name: props.name });
|
|
360
|
+
return { bucket };
|
|
361
|
+
}, "MyStack");
|
|
362
|
+
`,
|
|
363
|
+
);
|
|
364
|
+
await writeFile(
|
|
365
|
+
join(testDir, "stack.ts"),
|
|
366
|
+
`
|
|
367
|
+
import { MyStack } from "./composites";
|
|
368
|
+
export const stack = MyStack({ name: "composite-bucket" });
|
|
369
|
+
`,
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const withoutFold = await discover(testDir);
|
|
373
|
+
const withFold = await discover(testDir, { fold: true });
|
|
374
|
+
|
|
375
|
+
expect(withFold.errors).toEqual([]);
|
|
376
|
+
expect(withFold.entities.size).toBe(withoutFold.entities.size);
|
|
377
|
+
expect([...withFold.entities.keys()].sort()).toEqual([...withoutFold.entities.keys()].sort());
|
|
378
|
+
expect(withFold.entities.has("stackBucket")).toBe(true);
|
|
379
|
+
const folded = withFold.entities.get("stackBucket")! as unknown as { props: { name: string } };
|
|
380
|
+
const run = withoutFold.entities.get("stackBucket")! as unknown as { props: { name: string } };
|
|
381
|
+
expect(folded.props).toEqual(run.props);
|
|
382
|
+
|
|
383
|
+
const stackDecision = withFold.foldDecisions.find((d) => d.file.endsWith("stack.ts"));
|
|
384
|
+
expect(stackDecision?.mode).toBe("fold");
|
|
385
|
+
expect(stackDecision?.resourceCount).toBe(1);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("a composite factory defined locally (not resolvable via import) still falls back to run; output is unchanged", async () => {
|
|
389
|
+
await writeFile(
|
|
390
|
+
join(testDir, "stack.ts"),
|
|
391
|
+
`
|
|
392
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
393
|
+
import { Composite } from ${JSON.stringify(compositePath)};
|
|
394
|
+
const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
395
|
+
const LocalStack = Composite<{ name: string }>((props) => {
|
|
396
|
+
const bucket = new Bucket({ name: props.name });
|
|
397
|
+
return { bucket };
|
|
398
|
+
}, "LocalStack");
|
|
399
|
+
export const stack = LocalStack({ name: "composite-bucket" });
|
|
400
|
+
`,
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const withoutFold = await discover(testDir);
|
|
404
|
+
const withFold = await discover(testDir, { fold: true });
|
|
405
|
+
|
|
406
|
+
expect(withFold.errors).toEqual([]);
|
|
407
|
+
expect(withFold.entities.size).toBe(withoutFold.entities.size);
|
|
408
|
+
expect([...withFold.entities.keys()].sort()).toEqual([...withoutFold.entities.keys()].sort());
|
|
409
|
+
expect(withFold.entities.has("stackBucket")).toBe(true);
|
|
410
|
+
const folded = withFold.entities.get("stackBucket")! as unknown as { props: { name: string } };
|
|
411
|
+
const run = withoutFold.entities.get("stackBucket")! as unknown as { props: { name: string } };
|
|
412
|
+
expect(folded.props).toEqual(run.props);
|
|
413
|
+
|
|
414
|
+
const stackDecision = withFold.foldDecisions.find((d) => d.file.endsWith("stack.ts"));
|
|
415
|
+
expect(stackDecision?.mode).toBe("run");
|
|
416
|
+
expect(stackDecision?.reason).toContain("LocalStack");
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// chant #1020 — the full `discover()` pipeline, not just `tryFoldFile` in
|
|
420
|
+
// isolation: two files, one importing a resource attribute from the
|
|
421
|
+
// other, both fold, and `resolveAttrRefs` (the same identity-matching pass
|
|
422
|
+
// the run path always used) must assign the cross-file AttrRef the
|
|
423
|
+
// correct logical name with ZERO special-casing — proof the shared
|
|
424
|
+
// `FoldSession` this module wires into every top-level `tryFoldFile` call
|
|
425
|
+
// really does give `alb.ts` and `network.ts` the same `vpc` object.
|
|
426
|
+
test("cross-file resource reference resolves end-to-end: both files fold and the AttrRef gets the right logical name", async () => {
|
|
427
|
+
await writeFile(
|
|
428
|
+
join(testDir, "resources.ts"),
|
|
429
|
+
`
|
|
430
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
431
|
+
export const Vpc = createResource("Test::Vpc", "aws", { vpcId: "VpcId" });
|
|
432
|
+
export const Alb = createResource("Test::Alb", "aws", { albArn: "AlbArn" });
|
|
433
|
+
`,
|
|
434
|
+
);
|
|
435
|
+
await writeFile(
|
|
436
|
+
join(testDir, "network.ts"),
|
|
437
|
+
`
|
|
438
|
+
import { Vpc } from "./resources";
|
|
439
|
+
export const vpc = new Vpc({ cidr: "10.0.0.0/16" });
|
|
440
|
+
`,
|
|
441
|
+
);
|
|
442
|
+
await writeFile(
|
|
443
|
+
join(testDir, "alb.ts"),
|
|
444
|
+
`
|
|
445
|
+
import { Alb } from "./resources";
|
|
446
|
+
import { vpc } from "./network";
|
|
447
|
+
export const alb = new Alb({ vpcId: vpc.vpcId });
|
|
448
|
+
`,
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
const result = await discover(testDir, { fold: true });
|
|
452
|
+
|
|
453
|
+
expect(result.errors).toEqual([]);
|
|
454
|
+
expect(result.foldDecisions.every((d) => d.mode === "fold")).toBe(true);
|
|
455
|
+
expect(result.entities.has("vpc")).toBe(true);
|
|
456
|
+
expect(result.entities.has("alb")).toBe(true);
|
|
457
|
+
|
|
458
|
+
const alb = result.entities.get("alb")! as unknown as { props: { vpcId: { getLogicalName(): string | undefined; attribute: string } } };
|
|
459
|
+
expect(alb.props.vpcId.getLogicalName()).toBe("vpc");
|
|
460
|
+
expect(alb.props.vpcId.attribute).toBe("VpcId");
|
|
461
|
+
});
|
|
462
|
+
});
|
package/src/discovery/index.ts
CHANGED
|
@@ -1,10 +1,86 @@
|
|
|
1
1
|
import type { Declarable } from "../declarable";
|
|
2
2
|
import type { DiscoveryError } from "../errors";
|
|
3
|
+
import type { IntrinsicDef } from "../lexicon";
|
|
3
4
|
import { findInfraFiles } from "./files";
|
|
4
5
|
import { importModule } from "./import";
|
|
5
6
|
import { collectEntities } from "./collect";
|
|
6
7
|
import { resolveAttrRefs } from "./resolve";
|
|
7
8
|
import { buildDependencyGraph } from "./graph";
|
|
9
|
+
import { tryFoldFile, planFoldTaint, createFoldSession } from "./fold-import";
|
|
10
|
+
import { getProvenance } from "../provenance";
|
|
11
|
+
import type { BuildParamProvenance } from "../provenance";
|
|
12
|
+
import { buildParamValues } from "../build-params";
|
|
13
|
+
import { setBuildParams } from "../params";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Per-file fold-vs-run outcome (chant #1022, epic #1019), populated only
|
|
17
|
+
* when {@link DiscoveryOptions.fold} was requested. Lets a caller (`chant
|
|
18
|
+
* build --fold`) report which files skipped module execution and which
|
|
19
|
+
* fell back to the run path, and why.
|
|
20
|
+
*/
|
|
21
|
+
export interface FoldDecision {
|
|
22
|
+
/** Absolute path of the source file this decision covers. */
|
|
23
|
+
file: string;
|
|
24
|
+
/** "fold" — folded statically, zero execution. "run" — imported/executed as before. */
|
|
25
|
+
mode: "fold" | "run";
|
|
26
|
+
/** Why the file fell back to run. Present only when `mode === "run"` and fold was requested. */
|
|
27
|
+
reason?: string;
|
|
28
|
+
/** Number of entities the fold produced. Present only when `mode === "fold"`. */
|
|
29
|
+
resourceCount?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Optional inputs to {@link discover}.
|
|
34
|
+
*/
|
|
35
|
+
export interface DiscoveryOptions {
|
|
36
|
+
/**
|
|
37
|
+
* chant #1022/#1023 (epic #1019) — opt-in: for each source file, try to
|
|
38
|
+
* fold it to `Declarable`/`CompositeInstance` entities statically (no
|
|
39
|
+
* module execution) before falling back to importing/running it. Anything
|
|
40
|
+
* the folder can't represent (a non-`new`, non-composite-call export, a
|
|
41
|
+
* cross-file-only reference, …) falls back per-file — see
|
|
42
|
+
* {@link planFoldTaint} for the one case where a file that WOULD fold in
|
|
43
|
+
* isolation is still forced back to run, to keep fold and run from ever
|
|
44
|
+
* disagreeing about a shared entity's identity. Default `false` — behavior
|
|
45
|
+
* is unchanged unless requested.
|
|
46
|
+
*/
|
|
47
|
+
fold?: boolean;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* chant #1039 — lexicon-registered intrinsic tags (e.g. AWS's `Sub`) to
|
|
51
|
+
* recognize while folding. Threaded down to `tryFoldFile`/the static
|
|
52
|
+
* folder so a registered tagged template folds instead of falling back to
|
|
53
|
+
* run. Only meaningful when {@link fold} is set; ignored otherwise.
|
|
54
|
+
* Default: none (an intrinsic-using file still folds up to that point,
|
|
55
|
+
* then falls back to run at the unregistered tag).
|
|
56
|
+
*/
|
|
57
|
+
intrinsics?: IntrinsicDef[];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* chant #1045 Phase 2 — opt-in: whatever would otherwise reach the
|
|
61
|
+
* in-process `importModule` step (every file, when {@link fold} isn't set;
|
|
62
|
+
* only the per-file run-fallback remainder, when it is) instead runs
|
|
63
|
+
* together, isolated, in one sandboxed child process — see
|
|
64
|
+
* `./sandbox/run.ts`. Folded files are unaffected: fold already executes
|
|
65
|
+
* zero of a file's own top-level code, so it stays in this process exactly
|
|
66
|
+
* as it does today. Default `false` — behavior, including performance
|
|
67
|
+
* (no bundling, no child process, no IPC), is unchanged unless requested.
|
|
68
|
+
*/
|
|
69
|
+
sandbox?: boolean;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* chant #1064 — this build's resolved build-time parameter values (see
|
|
73
|
+
* ../build-params.ts's `resolveBuildParams`, driven by the CLI's
|
|
74
|
+
* `--param`/`--params-file`/declared `env` mapping/`chant.config.ts`
|
|
75
|
+
* defaults). Populated into `../params.ts`'s shared `params` object
|
|
76
|
+
* (`setBuildParams`, below) before any project file is imported or folded,
|
|
77
|
+
* and threaded into the fold session so a `params.<name>` reference
|
|
78
|
+
* resolves to a literal instead of an unresolved identifier. Default: none
|
|
79
|
+
* — `params` stays empty, matching a project that declares no
|
|
80
|
+
* `buildParams` at all.
|
|
81
|
+
*/
|
|
82
|
+
buildParams?: BuildParamProvenance[];
|
|
83
|
+
}
|
|
8
84
|
|
|
9
85
|
/**
|
|
10
86
|
* Result of the discovery process
|
|
@@ -18,6 +94,11 @@ export interface DiscoveryResult {
|
|
|
18
94
|
sourceFiles: string[];
|
|
19
95
|
/** Array of errors encountered during discovery */
|
|
20
96
|
errors: DiscoveryError[];
|
|
97
|
+
/**
|
|
98
|
+
* Per-file fold-vs-run decisions (#1022). Empty unless
|
|
99
|
+
* {@link DiscoveryOptions.fold} was set.
|
|
100
|
+
*/
|
|
101
|
+
foldDecisions: FoldDecision[];
|
|
21
102
|
}
|
|
22
103
|
|
|
23
104
|
/**
|
|
@@ -26,11 +107,25 @@ export interface DiscoveryResult {
|
|
|
26
107
|
* a dependency graph.
|
|
27
108
|
*
|
|
28
109
|
* @param path - The directory path to discover entities in
|
|
110
|
+
* @param options - Optional discovery behavior, e.g. {@link DiscoveryOptions.fold}
|
|
29
111
|
* @returns DiscoveryResult with entities, dependencies, sourceFiles, and errors
|
|
30
112
|
*/
|
|
31
|
-
export async function discover(path: string): Promise<DiscoveryResult> {
|
|
113
|
+
export async function discover(path: string, options?: DiscoveryOptions): Promise<DiscoveryResult> {
|
|
32
114
|
const errors: DiscoveryError[] = [];
|
|
33
115
|
const sourceFiles: string[] = [];
|
|
116
|
+
const foldDecisions: FoldDecision[] = [];
|
|
117
|
+
|
|
118
|
+
// chant #1064 — populate the shared build-time-parameters object BEFORE any
|
|
119
|
+
// project file is imported or folded, so both paths observe the identical
|
|
120
|
+
// values: the fold path substitutes them directly (see
|
|
121
|
+
// fold-import.ts's `buildExternals`), and a run(-fallback) file that
|
|
122
|
+
// imports "@intentius/chant/params" for real sees them too, since
|
|
123
|
+
// `setBuildParams` mutates the shared object in place rather than
|
|
124
|
+
// rebinding it. Unconditional (not just when `buildParams` is set) so a
|
|
125
|
+
// stale value from a PRIOR `discover()` call in the same process (tests,
|
|
126
|
+
// `--watch`) never leaks into a build that supplied none.
|
|
127
|
+
const buildParamValuesMap = buildParamValues(options?.buildParams ?? []);
|
|
128
|
+
setBuildParams(buildParamValuesMap);
|
|
34
129
|
|
|
35
130
|
// Step 1: Scan for TypeScript files
|
|
36
131
|
const files = await findInfraFiles(path);
|
|
@@ -38,8 +133,63 @@ export async function discover(path: string): Promise<DiscoveryResult> {
|
|
|
38
133
|
|
|
39
134
|
// Step 2: Import all modules
|
|
40
135
|
const modules: Array<{ file: string; exports: Record<string, unknown> }> = [];
|
|
136
|
+
// chant #1045 Phase 2 — files that would reach the in-process importModule()
|
|
137
|
+
// call below are instead queued here when options.sandbox is set, and run
|
|
138
|
+
// together afterward in one isolated child (see the merge step after
|
|
139
|
+
// collectEntities).
|
|
140
|
+
const sandboxFiles: string[] = [];
|
|
141
|
+
|
|
142
|
+
// Fold path (#1022/#1023): try the static folder on EVERY file first,
|
|
143
|
+
// independently, before committing any decision. A file that folds
|
|
144
|
+
// successfully in isolation must still be forced back to run if some
|
|
145
|
+
// OTHER file that itself falls back to run imports it (directly or
|
|
146
|
+
// transitively) — otherwise the run-fallback file's real re-import
|
|
147
|
+
// produces a SECOND, non-identical copy of the same entities this file
|
|
148
|
+
// already folded, and cross-file AttrRefs between them can never resolve
|
|
149
|
+
// (see {@link planFoldTaint}'s doc for the full story). This is only
|
|
150
|
+
// knowable after every file's fold has been attempted, hence the two
|
|
151
|
+
// passes instead of committing per-file as they're visited.
|
|
152
|
+
const foldAttempts = new Map<string, Awaited<ReturnType<typeof tryFoldFile>>>();
|
|
153
|
+
// chant #1020 — ONE session, shared by every top-level `tryFoldFile` call
|
|
154
|
+
// below AND by any cross-file reference one file's fold attempt makes into
|
|
155
|
+
// another (see fold-import.ts's `FoldSession` doc): this is what guarantees
|
|
156
|
+
// a project file imported by several others is folded exactly once, so
|
|
157
|
+
// every referrer shares the identical constructed Declarable/
|
|
158
|
+
// CompositeInstance objects rather than each building its own copy.
|
|
159
|
+
const foldSession = options?.fold ? createFoldSession(options.intrinsics, buildParamValuesMap) : undefined;
|
|
160
|
+
if (options?.fold) {
|
|
161
|
+
for (const file of files) {
|
|
162
|
+
foldAttempts.set(file, await tryFoldFile(file, options.intrinsics, foldSession));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const taintedFiles = options?.fold
|
|
166
|
+
? await planFoldTaint(
|
|
167
|
+
files,
|
|
168
|
+
new Map(files.map((file) => [file, foldAttempts.get(file)?.ok === true])),
|
|
169
|
+
)
|
|
170
|
+
: new Set<string>();
|
|
41
171
|
|
|
42
172
|
for (const file of files) {
|
|
173
|
+
if (options?.fold) {
|
|
174
|
+
const folded = foldAttempts.get(file)!;
|
|
175
|
+
if (folded.ok && !taintedFiles.has(file)) {
|
|
176
|
+
const exportsObj: Record<string, unknown> = {};
|
|
177
|
+
for (const [name, value] of folded.entities) exportsObj[name] = value;
|
|
178
|
+
modules.push({ file, exports: exportsObj });
|
|
179
|
+
foldDecisions.push({ file, mode: "fold", resourceCount: folded.entities.length });
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const reason = !folded.ok
|
|
183
|
+
? folded.reason
|
|
184
|
+
: `would fold in isolation, but a file that imports it (directly or transitively) falls back to run — folding independently would create a duplicate, non-identical instance`;
|
|
185
|
+
foldDecisions.push({ file, mode: "run", reason });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (options?.sandbox) {
|
|
189
|
+
sandboxFiles.push(file);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
43
193
|
try {
|
|
44
194
|
const exports = await importModule(file);
|
|
45
195
|
modules.push({ file, exports });
|
|
@@ -88,9 +238,90 @@ export async function discover(path: string): Promise<DiscoveryResult> {
|
|
|
88
238
|
dependencies: new Map(),
|
|
89
239
|
sourceFiles,
|
|
90
240
|
errors,
|
|
241
|
+
foldDecisions,
|
|
91
242
|
};
|
|
92
243
|
}
|
|
93
244
|
|
|
245
|
+
// chant #1045 Phase 2 — run the queued sandbox files together, isolated, in
|
|
246
|
+
// one child process, and merge its already-named, already-ref-resolved
|
|
247
|
+
// entities in. The child performs its OWN collectEntities/resolveAttrRefs
|
|
248
|
+
// over just this subset (see ./sandbox/run.ts) rather than sharing live
|
|
249
|
+
// objects with the fold-only `entities` map above — a real object can't
|
|
250
|
+
// cross a process boundary, and `planFoldTaint` already guarantees no
|
|
251
|
+
// folded file is ever referenced by a run-fallback one (or vice versa), so
|
|
252
|
+
// the two subsets never share cross-references. A name collision between
|
|
253
|
+
// them is therefore always a genuine duplicate (never the same object
|
|
254
|
+
// legitimately re-exported), reported exactly like collectEntities' own
|
|
255
|
+
// same-key check above rather than silently overwritten. (One narrower gap
|
|
256
|
+
// than the single unified collectEntities call this replaces: cross-
|
|
257
|
+
// directory stack-prefix disambiguation, #932, is computed separately for
|
|
258
|
+
// each subset, so a bare-name collision that spans a folded directory and a
|
|
259
|
+
// run-fallback directory won't be disambiguated the same way a single
|
|
260
|
+
// combined call would. Not hit by any corpus entry today.)
|
|
261
|
+
if (options?.sandbox && sandboxFiles.length > 0) {
|
|
262
|
+
try {
|
|
263
|
+
// Dynamic, not static — `./sandbox/run` imports `esbuild`, a large CJS
|
|
264
|
+
// package with its own module-scope filesystem access (the same class
|
|
265
|
+
// of thing `entity-wire.ts`'s split from `entity-wire-codec.ts` avoids
|
|
266
|
+
// for `typescript`). `discover()` is re-exported from the package root
|
|
267
|
+
// (`@intentius/chant`), and project source commonly imports that root
|
|
268
|
+
// — a STATIC top-level import here would make `esbuild` transitively
|
|
269
|
+
// reachable, and therefore BUNDLED AND EAGERLY EVALUATED, by every
|
|
270
|
+
// project file the sandboxed child imports that happens to import
|
|
271
|
+
// chant itself. A dynamic import is only ever actually reached here,
|
|
272
|
+
// at runtime, when a caller opts into `sandbox: true` — never from
|
|
273
|
+
// inside a bundled project file (project code never calls `discover()`
|
|
274
|
+
// itself), so esbuild only ever bundles it as inert, un-evaluated code
|
|
275
|
+
// when it's pulled in transitively.
|
|
276
|
+
const { runFallbackFilesSandboxed } = await import("./sandbox/run");
|
|
277
|
+
const sandboxResult = await runFallbackFilesSandboxed(sandboxFiles, path);
|
|
278
|
+
errors.push(...sandboxResult.errors);
|
|
279
|
+
for (const [name, entity] of sandboxResult.entities) {
|
|
280
|
+
if (entities.has(name)) {
|
|
281
|
+
const { DiscoveryError: DiscoveryErrorClass } = await import("../errors");
|
|
282
|
+
const file = sandboxResult.provenanceByName[name] ?? path;
|
|
283
|
+
errors.push(new DiscoveryErrorClass(file, `Duplicate export name "${name}" found`, "resolution"));
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
entities.set(name, entity);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Re-order the merged map to match original file discovery order.
|
|
290
|
+
// Without this, every fold entity (inserted above via the parent's own
|
|
291
|
+
// collectEntities call) sorts before every sandboxed entity (appended
|
|
292
|
+
// by the loop just above) — a fold-block-then-run-block order, not the
|
|
293
|
+
// per-file INTERLEAVED order a single unified collectEntities call
|
|
294
|
+
// over `files` would produce. Several downstream consumers iterate the
|
|
295
|
+
// entities map directly and are order-sensitive (e.g. a serializer's
|
|
296
|
+
// auto-detected cross-lexicon `Parameters`), so this isn't cosmetic —
|
|
297
|
+
// uncorrected, it's real, if harmless (still a valid template),
|
|
298
|
+
// byte-level drift. Ordered by each entity's OWN provenance
|
|
299
|
+
// (`../provenance.ts`) rather than by which loop produced it: a fold
|
|
300
|
+
// entity's provenance is read directly (a live object, still in this
|
|
301
|
+
// process); a sandboxed entity's provenance didn't survive the wire —
|
|
302
|
+
// `encodeEntitySet` deliberately drops build metadata, not declared
|
|
303
|
+
// configuration — so `sandboxResult.provenanceByName` (a side channel
|
|
304
|
+
// the child computes for exactly this, see ./sandbox/run.ts) is used
|
|
305
|
+
// instead. `Array.prototype.sort` is stable (guaranteed since ES2019),
|
|
306
|
+
// so entities from the same file (already correctly ordered by
|
|
307
|
+
// whichever collectEntities call produced them) keep their relative
|
|
308
|
+
// order — only which FILE's block comes first changes.
|
|
309
|
+
const fileOrder = new Map(files.map((file, i) => [file, i]));
|
|
310
|
+
const withIndex = [...entities.entries()].map(([name, entity]) => {
|
|
311
|
+
const sourceFile = getProvenance(entity)?.sourceFile ?? sandboxResult.provenanceByName[name];
|
|
312
|
+
const index = sourceFile !== undefined ? fileOrder.get(sourceFile) : undefined;
|
|
313
|
+
return { name, entity, index: index ?? Number.MAX_SAFE_INTEGER };
|
|
314
|
+
});
|
|
315
|
+
withIndex.sort((a, b) => a.index - b.index);
|
|
316
|
+
entities = new Map(withIndex.map(({ name, entity }) => [name, entity]));
|
|
317
|
+
} catch (error) {
|
|
318
|
+
const { DiscoveryError: DiscoveryErrorClass } = await import("../errors");
|
|
319
|
+
errors.push(
|
|
320
|
+
new DiscoveryErrorClass(path, error instanceof Error ? error.message : String(error), "resolution"),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
94
325
|
// Step 4: Resolve AttrRefs
|
|
95
326
|
try {
|
|
96
327
|
resolveAttrRefs(entities);
|
|
@@ -128,5 +359,6 @@ export async function discover(path: string): Promise<DiscoveryResult> {
|
|
|
128
359
|
dependencies,
|
|
129
360
|
sourceFiles,
|
|
130
361
|
errors,
|
|
362
|
+
foldDecisions,
|
|
131
363
|
};
|
|
132
364
|
}
|