@intentius/chant 0.19.1 → 0.21.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 +71 -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/codegen/docs-types.d.ts +2 -0
- package/dist/codegen/docs-types.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/declarable.d.ts +16 -0
- package/dist/declarable.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 +239 -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 +299 -0
- package/dist/fold/fold.d.ts.map +1 -0
- package/dist/fold/foldable-helpers.d.ts +121 -0
- package/dist/fold/foldable-helpers.d.ts.map +1 -0
- package/dist/fold/subset.d.ts +134 -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 +4 -2
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +131 -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/dist/runtime.d.ts +10 -1
- package/dist/runtime.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 +179 -0
- package/src/cli/commands/check-lexicon-intrinsics.ts +348 -0
- package/src/cli/commands/check-lexicon.test.ts +34 -0
- package/src/cli/commands/check-lexicon.ts +137 -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 +67 -0
- package/src/codegen/docs-sections.ts +7 -3
- package/src/codegen/docs-types.ts +2 -0
- 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/declarable.ts +20 -0
- package/src/discovery/collect.ts +17 -3
- package/src/discovery/entity-wire-codec.ts +487 -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 +1598 -0
- package/src/discovery/fold-import.ts +1998 -0
- package/src/discovery/index.test.ts +191 -1
- package/src/discovery/index.ts +242 -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 +812 -0
- package/src/fold/fold.ts +805 -0
- package/src/fold/foldable-helpers.ts +171 -0
- package/src/fold/subset-doc-parity.test.ts +210 -0
- package/src/fold/subset.test.ts +352 -0
- package/src/fold/subset.ts +383 -0
- package/src/index.ts +2 -0
- package/src/lexicon-output.ts +7 -2
- package/src/lexicon-schema.test.ts +57 -0
- package/src/lexicon-schema.ts +8 -1
- package/src/lexicon.ts +132 -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
- package/src/runtime.ts +11 -2
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { mkdir, writeFile, rm, symlink, realpath } from "node:fs/promises";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { join, dirname, resolve } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* chant #1045 Phase 2 (post-merge finding) — regression test for a real
|
|
11
|
+
* bypass: `resolveProjectLexicons` (`./plugins.ts`) used to fall back to a
|
|
12
|
+
* full, unsandboxed `discover()` call whenever a project's `chant.config.ts`
|
|
13
|
+
* didn't list `lexicons` explicitly — importing and RUNNING every source
|
|
14
|
+
* file in the CLI's own process, with no `--fold`/`--sandbox` involved at
|
|
15
|
+
* all, before the caller's own (possibly folded, possibly sandboxed)
|
|
16
|
+
* discovery ever ran. `resolveProjectLexicons` sits underneath nearly every
|
|
17
|
+
* CLI command (`build`, `lint`, `doctor`, `import`, `graph`, the MCP server),
|
|
18
|
+
* so this reached far past `chant build --fold`.
|
|
19
|
+
*
|
|
20
|
+
* This is deliberately an end-to-end test of the real CLI entry point
|
|
21
|
+
* (`main.ts`, spawned as a genuine child process — the same way `bin/chant`
|
|
22
|
+
* invokes it), NOT a call to `build()`/`buildCommand()` directly: calling
|
|
23
|
+
* either of those skips the exact code path the bug lived in
|
|
24
|
+
* (`main.ts`'s `loadPluginsOrExit` → `resolveProjectLexicons`), which is
|
|
25
|
+
* precisely how the corpus differential and Phase 2's other tests — all of
|
|
26
|
+
* which call `build()` — passed while this stayed open.
|
|
27
|
+
*
|
|
28
|
+
* Fixture lives in a fresh tmpdir per test, never in the source tree.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const mainTsPath = resolve(thisDir, "main.ts");
|
|
33
|
+
const repoRoot = resolve(thisDir, "../../../..");
|
|
34
|
+
|
|
35
|
+
describe("CLI end-to-end — resolveProjectLexicons must not execute project source", () => {
|
|
36
|
+
let testDir: string;
|
|
37
|
+
let exfilMarkerPath: string;
|
|
38
|
+
|
|
39
|
+
beforeEach(async () => {
|
|
40
|
+
testDir = await realpath(
|
|
41
|
+
await (async () => {
|
|
42
|
+
const dir = join(tmpdir(), `chant-security-boundary-test-${Date.now()}-${Math.random()}`);
|
|
43
|
+
await mkdir(join(dir, "src"), { recursive: true });
|
|
44
|
+
return dir;
|
|
45
|
+
})(),
|
|
46
|
+
);
|
|
47
|
+
exfilMarkerPath = join(testDir, "exfil-happened.txt");
|
|
48
|
+
|
|
49
|
+
// A real `node_modules/@intentius/chant-lexicon-k8s` symlink — same shape
|
|
50
|
+
// this monorepo's own workspace linking produces — so the fixture's bare
|
|
51
|
+
// `from "@intentius/chant-lexicon-k8s"` import resolves exactly the way a
|
|
52
|
+
// real installed project's would, and `detectLexicons`'s text scan (which
|
|
53
|
+
// matches that literal specifier) has something genuine to find.
|
|
54
|
+
await mkdir(join(testDir, "node_modules", "@intentius"), { recursive: true });
|
|
55
|
+
await symlink(
|
|
56
|
+
join(repoRoot, "lexicons", "k8s"),
|
|
57
|
+
join(testDir, "node_modules", "@intentius", "chant-lexicon-k8s"),
|
|
58
|
+
"dir",
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// No `lexicons` field — the exact condition that sends
|
|
62
|
+
// `resolveProjectLexicons` down the detection fallback.
|
|
63
|
+
await writeFile(join(testDir, "chant.config.ts"), "export default {};\n");
|
|
64
|
+
|
|
65
|
+
// A hostile source file: reads a file outside the project and the
|
|
66
|
+
// ambient environment at module top level (before its one Declarable
|
|
67
|
+
// export), and — if it got to run — writes a marker file so the test can
|
|
68
|
+
// tell "executed" apart from "folded" without depending on stdout
|
|
69
|
+
// capture alone.
|
|
70
|
+
await writeFile(
|
|
71
|
+
join(testDir, "src", "infra.ts"),
|
|
72
|
+
[
|
|
73
|
+
'import { readFileSync, writeFileSync } from "node:fs";',
|
|
74
|
+
'import { Namespace } from "@intentius/chant-lexicon-k8s";',
|
|
75
|
+
"",
|
|
76
|
+
"const stolen = readFileSync(\"/etc/hosts\", \"utf-8\");",
|
|
77
|
+
`writeFileSync(${JSON.stringify(exfilMarkerPath)}, "exfil: " + stolen.slice(0, 20) + " env=" + Object.keys(process.env).length);`,
|
|
78
|
+
'console.error(">>> EXFIL " + stolen.slice(0, 20) + " env=" + Object.keys(process.env).length);',
|
|
79
|
+
"",
|
|
80
|
+
'export const ns = new Namespace({ metadata: { name: "x" } });',
|
|
81
|
+
"",
|
|
82
|
+
].join("\n"),
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
afterEach(async () => {
|
|
87
|
+
await rm(testDir, { recursive: true, force: true });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/** Spawn the real CLI (`main.ts`, exactly as `bin/chant` invokes it via `npx tsx`) and capture its output. */
|
|
91
|
+
function runCli(args: string[]): { status: number | null; stdout: string; stderr: string } {
|
|
92
|
+
const result = spawnSync("npx", ["tsx", mainTsPath, ...args], {
|
|
93
|
+
cwd: repoRoot,
|
|
94
|
+
encoding: "utf-8",
|
|
95
|
+
timeout: 30_000,
|
|
96
|
+
env: { ...process.env, CHANT_SECRET_FOR_TEST: "should-never-leak" },
|
|
97
|
+
});
|
|
98
|
+
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
test("`chant build --fold` on a project with no `lexicons` config does not execute the hostile file", () => {
|
|
102
|
+
const { status, stdout, stderr } = runCli(["build", testDir, "--fold"]);
|
|
103
|
+
|
|
104
|
+
expect(stderr).not.toMatch(/EXFIL/);
|
|
105
|
+
expect(stdout).not.toMatch(/EXFIL/);
|
|
106
|
+
expect(existsSync(exfilMarkerPath), "hostile file wrote its marker — it executed").toBe(false);
|
|
107
|
+
|
|
108
|
+
// The build still has to actually work — this isn't proving safety by
|
|
109
|
+
// making the build fail outright.
|
|
110
|
+
expect(status).toBe(0);
|
|
111
|
+
expect(stdout).toContain("kind: Namespace");
|
|
112
|
+
|
|
113
|
+
// The fold decision for the hostile file should say what's now true:
|
|
114
|
+
// this file really did fold with zero module execution.
|
|
115
|
+
expect(stderr).toMatch(/\[fold:fold\] src\/infra\.ts/);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("`chant build --fold --sandbox` also does not execute the hostile file (both layers closed)", () => {
|
|
119
|
+
const { status, stdout, stderr } = runCli(["build", testDir, "--fold", "--sandbox"]);
|
|
120
|
+
|
|
121
|
+
expect(stderr).not.toMatch(/EXFIL/);
|
|
122
|
+
expect(stdout).not.toMatch(/EXFIL/);
|
|
123
|
+
expect(existsSync(exfilMarkerPath)).toBe(false);
|
|
124
|
+
expect(status).toBe(0);
|
|
125
|
+
expect(stdout).toContain("kind: Namespace");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("`chant lint` (which also resolves lexicons this way) does not execute the hostile file", () => {
|
|
129
|
+
const { stdout, stderr } = runCli(["lint", testDir]);
|
|
130
|
+
|
|
131
|
+
expect(stderr).not.toMatch(/EXFIL/);
|
|
132
|
+
expect(stdout).not.toMatch(/EXFIL/);
|
|
133
|
+
expect(existsSync(exfilMarkerPath), "hostile file wrote its marker — it executed").toBe(false);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { generateIntrinsics } from "./docs-sections";
|
|
3
|
+
import { intrinsicFolds } from "../lexicon";
|
|
4
|
+
import type { DocsConfig, ManifestJSON } from "./docs-types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* chant #1062 (epic #1019) — the per-lexicon intrinsic foldability matrix.
|
|
8
|
+
* `generateIntrinsics`'s "Folds?" column must come from {@link intrinsicFolds}
|
|
9
|
+
* (`../lexicon.ts`), the SAME predicate `fold()` uses to decide whether a
|
|
10
|
+
* registered tag actually folds — not a second `isTag`-shaped guess that
|
|
11
|
+
* could silently disagree with it. This test locks in that the column
|
|
12
|
+
* exists, has the right value per intrinsic, and tracks `intrinsicFolds`
|
|
13
|
+
* rather than restating its logic.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const config: DocsConfig = {
|
|
17
|
+
name: "test",
|
|
18
|
+
displayName: "Test",
|
|
19
|
+
description: "A test lexicon",
|
|
20
|
+
distDir: "/tmp/does-not-matter",
|
|
21
|
+
outDir: "/tmp/does-not-matter",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function manifestWith(intrinsics: ManifestJSON["intrinsics"]): ManifestJSON {
|
|
25
|
+
return { name: "test", version: "0.0.0", intrinsics };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("generateIntrinsics — Folds? column", () => {
|
|
29
|
+
test("a registered tagged-template intrinsic folds", () => {
|
|
30
|
+
const page = generateIntrinsics(config, manifestWith([{ name: "Sub", isTag: true }]));
|
|
31
|
+
expect(page).toContain("| Function | Description | Output Key | Tag? | Folds? |");
|
|
32
|
+
expect(page).toContain("| `Sub` | — | `Sub` | Yes | Yes |");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("a plain-call intrinsic its lexicon never opted in does not fold", () => {
|
|
36
|
+
const page = generateIntrinsics(config, manifestWith([{ name: "Ref", isTag: false }]));
|
|
37
|
+
expect(page).toContain("| `Ref` | — | `Ref` | No | No |");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("a plain-call intrinsic opted into call-form folding does fold (chant #1044)", () => {
|
|
41
|
+
const page = generateIntrinsics(config, manifestWith([{ name: "Ref", isTag: false, foldsAsCall: true }]));
|
|
42
|
+
expect(page).toContain("| `Ref` | — | `Ref` | No | Yes |");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("an intrinsic with isTag omitted does not fold (same as isTag: false)", () => {
|
|
46
|
+
const page = generateIntrinsics(config, manifestWith([{ name: "reference", outputKey: "!reference" }]));
|
|
47
|
+
expect(page).toContain("| `reference` | — | `!reference` | No | No |");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("Folds? always matches intrinsicFolds() for every row, never a restated copy", () => {
|
|
51
|
+
const intrinsics: NonNullable<ManifestJSON["intrinsics"]> = [
|
|
52
|
+
{ name: "Sub", isTag: true },
|
|
53
|
+
{ name: "Ref" },
|
|
54
|
+
{ name: "GetAtt", isTag: false },
|
|
55
|
+
{ name: "Join", isTag: false, foldsAsCall: true },
|
|
56
|
+
];
|
|
57
|
+
const page = generateIntrinsics(config, manifestWith(intrinsics));
|
|
58
|
+
for (const fn of intrinsics) {
|
|
59
|
+
const expected = intrinsicFolds(fn) ? "Yes" : "No";
|
|
60
|
+
const row = page.split("\n").find((line) => line.startsWith(`| \`${fn.name}\` |`));
|
|
61
|
+
expect(row, `no row rendered for ${fn.name}`).toBeDefined();
|
|
62
|
+
const cells = (row as string).split("|").map((c) => c.trim());
|
|
63
|
+
// | `name` | description | outputKey | Tag? | Folds? | → ["", "`name`", desc, key, tag, folds, ""]
|
|
64
|
+
expect(cells[5]).toBe(expected);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { escapeMdx } from "./docs-file-markers";
|
|
8
|
+
import { intrinsicFolds } from "../lexicon";
|
|
8
9
|
import type { DocsConfig, ManifestJSON, MetaEntry, RuleMeta } from "./docs-types";
|
|
9
10
|
|
|
10
11
|
export function generateOverview(
|
|
@@ -94,14 +95,17 @@ export function generateIntrinsics(
|
|
|
94
95
|
"",
|
|
95
96
|
`The ${config.displayName} lexicon provides **${intrinsics.length}** intrinsic functions.`,
|
|
96
97
|
"",
|
|
97
|
-
|
|
98
|
-
"
|
|
98
|
+
`**Tag?** shows how an intrinsic is authored: a genuine tagged template (\`Sub\\\`...\\\`\`) or a plain function call (\`Ref(...)\`). **Folds?** shows whether [\`chant build --fold\`](/chant/concepts/typescript-as-data/#folded-vs-run) can reduce a use of this intrinsic today, without running the file — generated directly from this lexicon's registration, not restated by hand. A tagged template folds once it is registered. A plain call folds only where this lexicon has opted that intrinsic in one at a time (\`foldsAsCall\`), so a \`No\` in this column means this intrinsic has not been opted in, not that calls in general cannot fold.`,
|
|
99
|
+
"",
|
|
100
|
+
"| Function | Description | Output Key | Tag? | Folds? |",
|
|
101
|
+
"|----------|-------------|------------|------|--------|",
|
|
99
102
|
];
|
|
100
103
|
|
|
101
104
|
for (const fn of intrinsics) {
|
|
102
105
|
const tag = fn.isTag ? "Yes" : "No";
|
|
106
|
+
const folds = intrinsicFolds(fn) ? "Yes" : "No";
|
|
103
107
|
lines.push(
|
|
104
|
-
`| \`${fn.name}\` | ${escapeMdx(fn.description ?? "—")} | \`${fn.outputKey ?? fn.name}\` | ${tag} |`,
|
|
108
|
+
`| \`${fn.name}\` | ${escapeMdx(fn.description ?? "—")} | \`${fn.outputKey ?? fn.name}\` | ${tag} | ${folds} |`,
|
|
105
109
|
);
|
|
106
110
|
}
|
|
107
111
|
|
|
@@ -55,6 +55,8 @@ export interface ManifestJSON {
|
|
|
55
55
|
description?: string;
|
|
56
56
|
outputKey?: string;
|
|
57
57
|
isTag?: boolean;
|
|
58
|
+
/** chant #1044 — the call-form fold opt-in, as published in the lexicon's manifest. Absent in any manifest built before #1044, which reads the same as "not opted in". */
|
|
59
|
+
foldsAsCall?: boolean;
|
|
58
60
|
}>;
|
|
59
61
|
pseudoParameters?: Record<string, string>;
|
|
60
62
|
}
|
|
@@ -61,8 +61,8 @@ export interface ListComponentsResult {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/** Discover components under `path` and shape them for `chant list --components`. */
|
|
64
|
-
export async function listComponents(path: string): Promise<ListComponentsResult> {
|
|
65
|
-
const result = await discoverComponents(path);
|
|
64
|
+
export async function listComponents(path: string, sandbox?: boolean): Promise<ListComponentsResult> {
|
|
65
|
+
const result = await discoverComponents(path, { sandbox });
|
|
66
66
|
if (result.errors.length > 0) {
|
|
67
67
|
return { success: false, components: [], errors: result.errors.map((e) => e.message) };
|
|
68
68
|
}
|
|
@@ -100,8 +100,8 @@ export interface DescribeComponentResult {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/** Find and project one named component, for `chant describe <name> --components`. */
|
|
103
|
-
export async function describeComponent(path: string, name: string): Promise<DescribeComponentResult> {
|
|
104
|
-
const result = await discoverComponents(path);
|
|
103
|
+
export async function describeComponent(path: string, name: string, sandbox?: boolean): Promise<DescribeComponentResult> {
|
|
104
|
+
const result = await discoverComponents(path, { sandbox });
|
|
105
105
|
if (result.errors.length > 0) {
|
|
106
106
|
return { success: false, component: name, output: result.errors.map((e) => e.message).join("\n") };
|
|
107
107
|
}
|
|
@@ -139,8 +139,8 @@ export interface ComponentGraphResult {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/** Compute the components' dependency graph under `path`, for `chant graph --components`. */
|
|
142
|
-
export async function computeComponentGraph(path: string): Promise<ComponentGraphResult> {
|
|
143
|
-
const result = await discoverComponents(path);
|
|
142
|
+
export async function computeComponentGraph(path: string, sandbox?: boolean): Promise<ComponentGraphResult> {
|
|
143
|
+
const result = await discoverComponents(path, { sandbox });
|
|
144
144
|
if (result.errors.length > 0) {
|
|
145
145
|
return { success: false, order: [], waves: [], edges: [], error: result.errors.map((e) => e.message).join("\n") };
|
|
146
146
|
}
|
|
@@ -220,6 +220,7 @@ export async function generateComponentsPipeline(
|
|
|
220
220
|
path: string,
|
|
221
221
|
lexicon: GenerateLexicon,
|
|
222
222
|
options?: ComponentPipelineOptions,
|
|
223
|
+
sandbox?: boolean,
|
|
223
224
|
): Promise<GenerateComponentsResult> {
|
|
224
225
|
const plugin = await loadLexiconPlugin(lexicon);
|
|
225
226
|
if (!plugin?.generateComponentPipeline) {
|
|
@@ -229,7 +230,7 @@ export async function generateComponentsPipeline(
|
|
|
229
230
|
};
|
|
230
231
|
}
|
|
231
232
|
|
|
232
|
-
const result = await discoverComponents(path);
|
|
233
|
+
const result = await discoverComponents(path, { sandbox });
|
|
233
234
|
if (result.errors.length > 0) {
|
|
234
235
|
return { success: false, error: result.errors.map((e) => e.message).join("\n") };
|
|
235
236
|
}
|
|
@@ -295,6 +296,13 @@ export function findComponentGate(component: DriverComponent): { signalName: str
|
|
|
295
296
|
export interface RunComponentsOptions {
|
|
296
297
|
/** Target environment name, threaded into every capability's `DeployContext.env` (default: "local"). */
|
|
297
298
|
env?: string;
|
|
299
|
+
/**
|
|
300
|
+
* chant #1051 — opt-in: discover `*.component.ts` files in a sandboxed
|
|
301
|
+
* child process (`chant run --components <name|all> --sandbox`) instead of
|
|
302
|
+
* in the CLI's own process. See `discoverComponents`'s `sandbox` option
|
|
303
|
+
* (../discover.ts).
|
|
304
|
+
*/
|
|
305
|
+
sandbox?: boolean;
|
|
298
306
|
/** Additional capability plugin package names to load on top of the built-in starter set (see `buildCapabilityRegistry`). */
|
|
299
307
|
capabilityPlugins?: string[];
|
|
300
308
|
/**
|
|
@@ -392,8 +400,12 @@ export interface ResolvedComponentTargets {
|
|
|
392
400
|
* without also inheriting the local executor's pre-flight gate rejection
|
|
393
401
|
* (gates are exactly what the durable path exists to support).
|
|
394
402
|
*/
|
|
395
|
-
export async function resolveComponentTargets(
|
|
396
|
-
|
|
403
|
+
export async function resolveComponentTargets(
|
|
404
|
+
path: string,
|
|
405
|
+
selector: string,
|
|
406
|
+
sandbox?: boolean,
|
|
407
|
+
): Promise<ResolvedComponentTargets> {
|
|
408
|
+
const result = await discoverComponents(path, { sandbox });
|
|
397
409
|
if (result.errors.length > 0) {
|
|
398
410
|
return { success: false, targets: [], error: result.errors.map((e) => e.message).join("\n") };
|
|
399
411
|
}
|
|
@@ -421,7 +433,7 @@ export async function runComponents(
|
|
|
421
433
|
selector: string,
|
|
422
434
|
options: RunComponentsOptions = {},
|
|
423
435
|
): Promise<RunComponentsResult> {
|
|
424
|
-
const resolved = await resolveComponentTargets(path, selector);
|
|
436
|
+
const resolved = await resolveComponentTargets(path, selector, options.sandbox);
|
|
425
437
|
if (!resolved.success) {
|
|
426
438
|
return { success: false, selected: [], error: resolved.error };
|
|
427
439
|
}
|
|
@@ -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
|
+
}
|