@intentius/chant 0.124.31 → 0.124.32
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/bin/chant +7 -2
- package/dist/cli/commands/check-lexicon-plugin.d.ts +7 -4
- package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-plugin.js +10 -7
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.js +18 -12
- package/dist/cli/commands/import.d.ts +10 -0
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/import.js +37 -0
- package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/project.js +4 -3
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/onboard.js +8 -4
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +2 -1
- package/dist/cli/dependencies.d.ts +18 -0
- package/dist/cli/dependencies.d.ts.map +1 -0
- package/dist/cli/dependencies.js +120 -0
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +23 -4
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +12 -4
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/graph.js +16 -6
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lint.js +6 -0
- package/dist/cli/handlers/operator.d.ts +2 -2
- package/dist/cli/handlers/operator.js +3 -3
- package/dist/cli/handlers/pr.d.ts +14 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +488 -32
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +3 -1
- package/dist/cli/handlers/run.d.ts +13 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +38 -15
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +44 -16
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +15 -0
- package/dist/cli/registry.d.ts +13 -4
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/registry.js +6 -2
- package/dist/codegen/generate-post-synth-barrel.js +2 -2
- package/dist/codegen/lexicon-regen.d.ts +4 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/lexicon-regen.js +1 -1
- package/dist/codegen/pinned-upgrade.js +2 -2
- package/dist/components/capability.d.ts +44 -3
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/capability.js +21 -0
- package/dist/components/changed-files.d.ts +7 -1
- package/dist/components/changed-files.d.ts.map +1 -1
- package/dist/components/changed-files.js +9 -1
- package/dist/components/driver.d.ts +38 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +47 -6
- package/dist/components/fan-out-record.d.ts +6 -1
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/fan-out-record.js +24 -0
- package/dist/components/fan-out-run.d.ts +20 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +21 -1
- package/dist/components/pr-pipeline.d.ts +65 -2
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +107 -15
- package/dist/components/pr-run.d.ts +127 -29
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +290 -70
- package/dist/components/wave-plan.d.ts +5 -2
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +12 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-import.js +16 -6
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.js +6 -0
- package/dist/discovery/specifier-resolve.d.ts.map +1 -1
- package/dist/discovery/specifier-resolve.js +8 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/fold.js +68 -1
- package/dist/fold/subset.d.ts +37 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/fold/subset.js +88 -0
- package/dist/lexicon-module.d.ts +10 -0
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon-module.js +53 -8
- package/dist/lexicon-plugin-helpers.d.ts.map +1 -1
- package/dist/lexicon-plugin-helpers.js +6 -4
- package/dist/lexicon.d.ts +2 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/affected.d.ts.map +1 -1
- package/dist/lifecycle/affected.js +7 -1
- package/dist/lifecycle/release-ledger.d.ts +21 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +24 -0
- package/dist/lifecycle/work-dir.d.ts +45 -0
- package/dist/lifecycle/work-dir.d.ts.map +1 -0
- package/dist/lifecycle/work-dir.js +78 -0
- package/dist/lint/discover.d.ts +17 -9
- package/dist/lint/discover.d.ts.map +1 -1
- package/dist/lint/discover.js +62 -54
- package/dist/module-source.d.ts +57 -0
- package/dist/module-source.d.ts.map +1 -0
- package/dist/module-source.js +104 -0
- package/dist/op/activities/factory.js +1 -1
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/discover.js +13 -3
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +3 -0
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +20 -5
- package/dist/op/operator.d.ts +2 -2
- package/dist/op/operator.js +2 -2
- package/dist/op/steward.d.ts +6 -4
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/op/steward.js +5 -3
- package/dist/op/work-lease-run.d.ts +20 -7
- package/dist/op/work-lease-run.d.ts.map +1 -1
- package/dist/op/work-lease-run.js +56 -12
- package/dist/pr-forge.d.ts +10 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +39 -0
- package/dist/pr-loop.d.ts +141 -4
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +122 -6
- package/dist/pr-report-url.d.ts +2 -2
- package/dist/pr-report-url.d.ts.map +1 -1
- package/dist/pr-report-url.js +5 -5
- package/dist/ts-loader.d.ts +38 -4
- package/dist/ts-loader.d.ts.map +1 -1
- package/dist/ts-loader.js +190 -16
- package/dist/wave-runtime.mjs +208 -208
- package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts +34 -0
- package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/fountain-retired-kinds.js +176 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -1
- package/dist/workspace/chant-migrations.js +2 -0
- package/dist/workspace/checks/boxes.d.ts +6 -6
- package/dist/workspace/checks/boxes.js +9 -9
- package/dist/workspace/ci-green-forge.d.ts +6 -4
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green.d.ts +28 -12
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +49 -21
- package/dist/workspace/ci-list.schema.json +12 -6
- package/dist/workspace/ci-report-url.d.ts +18 -0
- package/dist/workspace/ci-report-url.d.ts.map +1 -0
- package/dist/workspace/ci-report-url.js +33 -0
- package/dist/workspace/ci-run-index.d.ts +12 -0
- package/dist/workspace/ci-run-index.d.ts.map +1 -1
- package/dist/workspace/ci-run-index.js +6 -4
- package/dist/workspace/ci-run-index.schema.json +5 -3
- package/dist/workspace/ci-run.d.ts +75 -2
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +195 -53
- package/dist/workspace/ci-runner-key.d.ts +16 -3
- package/dist/workspace/ci-runner-key.d.ts.map +1 -1
- package/dist/workspace/ci-runner-key.js +30 -10
- package/dist/workspace/ci-serve.d.ts +54 -4
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +175 -26
- package/dist/workspace/declaration.d.ts +8 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +11 -2
- package/dist/workspace/declaration.schema.json +7 -2
- package/dist/workspace/export.d.ts +1 -1
- package/dist/workspace/export.d.ts.map +1 -1
- package/dist/workspace/export.js +7 -3
- package/dist/workspace/ls.d.ts +6 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +1 -0
- package/dist/workspace/ls.schema.json +7 -1
- package/dist/workspace/pin-integrity.d.ts.map +1 -1
- package/dist/workspace/pin-integrity.js +3 -2
- package/dist/workspace/pr-report.schema.json +74 -7
- package/dist/workspace/reason-codes.d.ts +1 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +2 -2
- package/dist/workspace/run-event.schema.json +3 -1
- package/dist/workspace/status.schema.json +3 -1
- package/dist/workspace/write-scope.d.ts +9 -0
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/dist/workspace/write-scope.js +11 -1
- package/package.json +1 -1
- package/src/__fixtures__/pr-loop/pr-apply-wave-waiting.note.golden.md +48 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +3 -3
- package/src/cli/commands/audit.test.ts +2 -2
- package/src/cli/commands/check-lexicon-plugin.test.ts +9 -0
- package/src/cli/commands/check-lexicon-plugin.ts +11 -8
- package/src/cli/commands/check-lexicon.test.ts +1 -1
- package/src/cli/commands/check-lexicon.ts +16 -11
- package/src/cli/commands/import.ts +40 -0
- package/src/cli/commands/init-lexicon/templates/project.ts +4 -3
- package/src/cli/commands/init-lexicon.test.ts +8 -5
- package/src/cli/commands/onboard.test.ts +45 -0
- package/src/cli/commands/onboard.ts +8 -4
- package/src/cli/commands/update.test.ts +6 -0
- package/src/cli/commands/update.ts +4 -1
- package/src/cli/compiled-cli.e2e.test.ts +76 -11
- package/src/cli/dependencies.e2e.test.ts +101 -0
- package/src/cli/dependencies.test.ts +93 -0
- package/src/cli/dependencies.ts +122 -0
- package/src/cli/handlers/build.test.ts +69 -0
- package/src/cli/handlers/build.ts +20 -4
- package/src/cli/handlers/fan-out.test.ts +73 -0
- package/src/cli/handlers/fan-out.ts +11 -4
- package/src/cli/handlers/graph.test.ts +76 -0
- package/src/cli/handlers/graph.ts +19 -12
- package/src/cli/handlers/lint.test.ts +42 -0
- package/src/cli/handlers/lint.ts +6 -0
- package/src/cli/handlers/operator.ts +3 -3
- package/src/cli/handlers/pr-waves.test.ts +449 -0
- package/src/cli/handlers/pr.test.ts +134 -0
- package/src/cli/handlers/pr.ts +523 -31
- package/src/cli/handlers/promote.test.ts +26 -1
- package/src/cli/handlers/promote.ts +3 -2
- package/src/cli/handlers/run-components-exit.test.ts +99 -0
- package/src/cli/handlers/run-signer.test.ts +79 -0
- package/src/cli/handlers/run.ts +34 -16
- package/src/cli/main.test.ts +18 -0
- package/src/cli/main.ts +43 -16
- package/src/cli/parse-args.ts +12 -0
- package/src/cli/registry.ts +19 -6
- package/src/codegen/generate-post-synth-barrel.ts +2 -2
- package/src/codegen/lexicon-regen.ts +1 -1
- package/src/codegen/pinned-upgrade.ts +2 -2
- package/src/codegen/post-synth-barrel-guards.test.ts +6 -11
- package/src/components/capability.ts +51 -3
- package/src/components/changed-files.test.ts +16 -1
- package/src/components/changed-files.ts +9 -1
- package/src/components/driver.test.ts +36 -1
- package/src/components/driver.ts +76 -4
- package/src/components/fan-out-record.ts +29 -1
- package/src/components/fan-out-run.test.ts +35 -0
- package/src/components/fan-out-run.ts +40 -1
- package/src/components/pr-pipeline.test.ts +100 -0
- package/src/components/pr-pipeline.ts +132 -17
- package/src/components/pr-run.test.ts +323 -18
- package/src/components/pr-run.ts +354 -75
- package/src/components/wave-plan.ts +16 -4
- package/src/discovery/fold-host-method.test.ts +87 -0
- package/src/discovery/fold-import.ts +15 -5
- package/src/discovery/fold-package-composite.test.ts +27 -0
- package/src/discovery/fold-specifiers.test.ts +8 -0
- package/src/discovery/sandbox/bundle.ts +6 -0
- package/src/discovery/specifier-resolve.ts +8 -2
- package/src/fold/fold.test.ts +72 -0
- package/src/fold/fold.ts +71 -0
- package/src/fold/subset.test.ts +31 -0
- package/src/fold/subset.ts +99 -0
- package/src/lexicon-dist.e2e.test.ts +254 -0
- package/src/lexicon-module.test.ts +47 -0
- package/src/lexicon-module.ts +49 -7
- package/src/lexicon-plugin-helpers.ts +6 -4
- package/src/lexicon.ts +2 -0
- package/src/lifecycle/affected.test.ts +7 -0
- package/src/lifecycle/affected.ts +6 -1
- package/src/lifecycle/release-ledger.ts +34 -0
- package/src/lifecycle/work-dir.test.ts +77 -0
- package/src/lifecycle/work-dir.ts +82 -0
- package/src/lint/discover.test.ts +46 -2
- package/src/lint/discover.ts +55 -49
- package/src/module-source.test.ts +85 -0
- package/src/module-source.ts +112 -0
- package/src/op/activities/factory.ts +1 -1
- package/src/op/discover-root.test.ts +31 -2
- package/src/op/discover.ts +14 -4
- package/src/op/gate.test.ts +30 -0
- package/src/op/gate.ts +3 -0
- package/src/op/op-waves-run.ts +20 -5
- package/src/op/op-waves.test.ts +49 -0
- package/src/op/operator.ts +2 -2
- package/src/op/steward.ts +6 -4
- package/src/op/work-lease-run.test.ts +88 -2
- package/src/op/work-lease-run.ts +52 -12
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.test.ts +42 -0
- package/src/pr-forge.ts +40 -0
- package/src/pr-loop-bundle.test.ts +2 -1
- package/src/pr-loop.test.ts +117 -1
- package/src/pr-loop.ts +252 -10
- package/src/pr-report-url.ts +5 -5
- package/src/terraform/__fixtures__/advise.test.ts +21 -2
- package/src/ts-loader.test.ts +89 -1
- package/src/ts-loader.ts +180 -13
- package/src/workspace/changes.test.ts +1 -1
- package/src/workspace/chant-migrations/fountain-retired-kinds.test.ts +33 -0
- package/src/workspace/chant-migrations/fountain-retired-kinds.ts +190 -0
- package/src/workspace/chant-migrations.e2e.test.ts +5 -0
- package/src/workspace/chant-migrations.ts +2 -0
- package/src/workspace/checks/boxes.ts +9 -9
- package/src/workspace/ci-green-forge.ts +6 -4
- package/src/workspace/ci-green-runs.test.ts +53 -11
- package/src/workspace/ci-green.test.ts +1 -1
- package/src/workspace/ci-green.ts +50 -22
- package/src/workspace/ci-list.schema.json +12 -6
- package/src/workspace/ci-report-url.test.ts +220 -0
- package/src/workspace/ci-report-url.ts +33 -0
- package/src/workspace/ci-run-index.schema.json +5 -3
- package/src/workspace/ci-run-index.test.ts +42 -3
- package/src/workspace/ci-run-index.ts +18 -4
- package/src/workspace/ci-run.test.ts +185 -7
- package/src/workspace/ci-run.ts +224 -54
- package/src/workspace/ci-runner-key.test.ts +55 -17
- package/src/workspace/ci-runner-key.ts +36 -11
- package/src/workspace/ci-serve.test.ts +150 -6
- package/src/workspace/ci-serve.ts +169 -25
- package/src/workspace/declaration.schema.json +7 -2
- package/src/workspace/declaration.test.ts +23 -0
- package/src/workspace/declaration.ts +19 -3
- package/src/workspace/export.ts +6 -3
- package/src/workspace/ls.schema.json +7 -1
- package/src/workspace/ls.ts +7 -2
- package/src/workspace/pin-integrity.test.ts +5 -0
- package/src/workspace/pin-integrity.ts +3 -2
- package/src/workspace/pr-report.schema.json +74 -7
- package/src/workspace/reason-codes.ts +2 -2
- package/src/workspace/run-event.schema.json +3 -1
- package/src/workspace/status.schema.json +3 -1
- package/src/workspace/write-scope.test.ts +29 -1
- package/src/workspace/write-scope.ts +12 -1
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3981 — lexicons loaded from compiled JS, with plain node.
|
|
3
|
+
*
|
|
4
|
+
* Lexicon packages publish compiled `dist/*.js` as each export's `default`.
|
|
5
|
+
* This compiles core and a set of real lexicons (aws and helm, which find
|
|
6
|
+
* their lint rules by directory scan, and the lexicons they import) with tsc
|
|
7
|
+
* into a scratch `node_modules`, with each lexicon's own package.json. A
|
|
8
|
+
* plain node process with no
|
|
9
|
+
* loader then loads each plugin through the compiled core, and must find the
|
|
10
|
+
* same lint rules, post-synth checks, skills and generated data as the same
|
|
11
|
+
* plugins loaded from source, without loading tsx or any TypeScript module.
|
|
12
|
+
* A `chant build` and `chant lint` of a project against the compiled aws
|
|
13
|
+
* lexicon must work and load none of the lexicon's TypeScript.
|
|
14
|
+
*
|
|
15
|
+
* End to end because it runs tsc and spawns node.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
19
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { tmpdir } from "node:os";
|
|
21
|
+
import { join, resolve } from "node:path";
|
|
22
|
+
import { pathToFileURL } from "node:url";
|
|
23
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
24
|
+
|
|
25
|
+
const coreRoot = resolve(import.meta.dirname, "..");
|
|
26
|
+
const repo = resolve(coreRoot, "..", "..");
|
|
27
|
+
const repoModules = join(repo, "node_modules");
|
|
28
|
+
|
|
29
|
+
/** Compiled and counted. aws and helm discover their lint rules from a directory; the rest are what they import. */
|
|
30
|
+
const LEXICONS = ["aws", "helm", "k8s", "grafana", "otel", "prometheus"];
|
|
31
|
+
|
|
32
|
+
let scratch: string;
|
|
33
|
+
let modules: string;
|
|
34
|
+
let coreDist: string;
|
|
35
|
+
let project: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Loads each named plugin through core and prints what it found. argv[2] is a
|
|
39
|
+
* JSON object: `core` (the URL of core's ts-loader and cli/plugins modules,
|
|
40
|
+
* without extension), `ext` (`.js` or `.ts`), `names`, and `taggable` (the
|
|
41
|
+
* URL of the aws lexicon's taggable module).
|
|
42
|
+
*/
|
|
43
|
+
const COUNT = `
|
|
44
|
+
import { registerHooks } from "node:module";
|
|
45
|
+
const seen = { tsx: 0, typeScript: [] };
|
|
46
|
+
registerHooks({
|
|
47
|
+
load(url, context, next) {
|
|
48
|
+
const path = url.replace(/[?#].*$/, "");
|
|
49
|
+
if (path.includes("/node_modules/tsx/")) seen.tsx++;
|
|
50
|
+
else if (/\\.[cm]?tsx?$/.test(path)) seen.typeScript.push(path);
|
|
51
|
+
return next(url, context);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
const args = JSON.parse(process.argv[2]);
|
|
55
|
+
const { installCompiledCli } = await import(args.core + "/ts-loader" + args.ext);
|
|
56
|
+
installCompiledCli();
|
|
57
|
+
const { loadPlugin } = await import(args.core + "/cli/plugins" + args.ext);
|
|
58
|
+
const plugins = {};
|
|
59
|
+
for (const name of args.names) {
|
|
60
|
+
const plugin = await loadPlugin(name);
|
|
61
|
+
const catalogs = {};
|
|
62
|
+
for (const resource of plugin.mcpResources?.() ?? []) {
|
|
63
|
+
if (!resource.uri.endsWith(":resource-catalog")) continue;
|
|
64
|
+
try { catalogs[resource.uri] = JSON.parse(await resource.handler()).length ?? 0; } catch (err) { catalogs[resource.uri] = String(err); }
|
|
65
|
+
}
|
|
66
|
+
plugins[name] = {
|
|
67
|
+
catalogs,
|
|
68
|
+
lintRules: (plugin.lintRules?.() ?? []).map((r) => r.id).sort(),
|
|
69
|
+
postSynthChecks: (plugin.postSynthChecks?.() ?? []).map((c) => c.id).sort(),
|
|
70
|
+
skills: (plugin.skills?.() ?? []).filter((s) => s.content.length > 0).map((s) => s.name).sort(),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const { loadTaggableResources } = await import(args.taggable);
|
|
74
|
+
const compiled = globalThis[Symbol.for("chant.compiledCli")];
|
|
75
|
+
process.stdout.write(JSON.stringify({
|
|
76
|
+
plugins,
|
|
77
|
+
taggable: loadTaggableResources().size,
|
|
78
|
+
tsxRegistered: compiled ? compiled.tsx : null,
|
|
79
|
+
seen,
|
|
80
|
+
}));
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
interface Counted {
|
|
84
|
+
plugins: Record<string, { catalogs: Record<string, number | string>; lintRules: string[]; postSynthChecks: string[]; skills: string[] }>;
|
|
85
|
+
taggable: number;
|
|
86
|
+
tsxRegistered: boolean | null;
|
|
87
|
+
seen: { tsx: number; typeScript: string[] };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function tsc(project: string, outDir: string): void {
|
|
91
|
+
execFileSync(join(repo, "scripts", "tsc7.sh"), ["-p", project, "--outDir", outDir, "--sourceMap", "false", "--declaration", "false", "--declarationMap", "false"], {
|
|
92
|
+
cwd: repo,
|
|
93
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
94
|
+
});
|
|
95
|
+
execFileSync(process.execPath, [join(repoModules, "tsc-alias", "dist", "bin", "index.js"), "-p", project, "--outDir", outDir], {
|
|
96
|
+
cwd: repo,
|
|
97
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function count(script: string, args: object, nodeArgs: string[] = []): Counted {
|
|
102
|
+
const r = spawnSync(process.execPath, [...nodeArgs, script, JSON.stringify(args)], { cwd: scratch, encoding: "utf-8", maxBuffer: 16 * 1024 * 1024 });
|
|
103
|
+
if (r.status !== 0) throw new Error(`count script failed (${r.status}):\n${r.stderr}`);
|
|
104
|
+
return JSON.parse(r.stdout) as Counted;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
beforeAll(() => {
|
|
108
|
+
scratch = mkdtempSync(join(tmpdir(), "chant-lexicon-dist-"));
|
|
109
|
+
modules = join(scratch, "node_modules");
|
|
110
|
+
mkdirSync(join(modules, "@intentius"), { recursive: true });
|
|
111
|
+
for (const name of readdirSync(repoModules)) {
|
|
112
|
+
if (name === "@intentius" || name === ".bin") continue;
|
|
113
|
+
symlinkSync(join(repoModules, name), join(modules, name));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Core as an install has it: its source (the package exports name it, and
|
|
117
|
+
// the compiled CLI redirects each import of it to dist), and dist.
|
|
118
|
+
const core = join(modules, "@intentius", "chant");
|
|
119
|
+
mkdirSync(core);
|
|
120
|
+
cpSync(join(coreRoot, "package.json"), join(core, "package.json"));
|
|
121
|
+
cpSync(join(coreRoot, "src"), join(core, "src"), { recursive: true, filter: (p) => !/\.test\.ts$/.test(p) });
|
|
122
|
+
coreDist = join(core, "dist");
|
|
123
|
+
tsc(join(coreRoot, "tsconfig.build.json"), coreDist);
|
|
124
|
+
mkdirSync(join(core, "scripts"));
|
|
125
|
+
cpSync(join(coreRoot, "scripts", "copy-assets.mjs"), join(core, "scripts", "copy-assets.mjs"));
|
|
126
|
+
execFileSync(process.execPath, [join(core, "scripts", "copy-assets.mjs")], { stdio: "ignore" });
|
|
127
|
+
|
|
128
|
+
for (const name of LEXICONS) {
|
|
129
|
+
const root = join(repo, "lexicons", name);
|
|
130
|
+
const pkg = join(modules, "@intentius", `chant-lexicon-${name}`);
|
|
131
|
+
mkdirSync(pkg);
|
|
132
|
+
cpSync(join(root, "package.json"), join(pkg, "package.json"));
|
|
133
|
+
// src/ is published beside dist/, and holds what a module reads beside itself.
|
|
134
|
+
symlinkSync(join(root, "src"), join(pkg, "src"));
|
|
135
|
+
if (existsSync(join(root, "node_modules"))) symlinkSync(join(root, "node_modules"), join(pkg, "node_modules"));
|
|
136
|
+
// The bundle's dist/ (meta.json, manifest.json), then the compiled modules.
|
|
137
|
+
if (existsSync(join(root, "dist"))) cpSync(join(root, "dist"), join(pkg, "dist"), { recursive: true });
|
|
138
|
+
tsc(join(root, "tsconfig.build.json"), join(pkg, "dist"));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
writeFileSync(join(scratch, "count.mjs"), COUNT);
|
|
142
|
+
|
|
143
|
+
project = join(scratch, "project");
|
|
144
|
+
mkdirSync(join(project, "src"), { recursive: true });
|
|
145
|
+
writeFileSync(join(project, "package.json"), JSON.stringify({ name: "lexicon-dist-e2e", private: true, type: "module" }));
|
|
146
|
+
writeFileSync(join(project, "chant.config.ts"), `export default { lexicons: ["aws"] };\n`);
|
|
147
|
+
writeFileSync(
|
|
148
|
+
join(project, "src", "infra.ts"),
|
|
149
|
+
`import { Queue } from "@intentius/chant-lexicon-aws";\n` +
|
|
150
|
+
`export const taskQueue = new Queue({ QueueName: "lexicon-dist-e2e", Tags: [{ Key: "suite", Value: "lexicon-dist" }], SqsManagedSseEnabled: true });\n`,
|
|
151
|
+
);
|
|
152
|
+
}, 600_000);
|
|
153
|
+
|
|
154
|
+
afterAll(() => {
|
|
155
|
+
if (scratch) rmSync(scratch, { recursive: true, force: true });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("lexicons loaded from compiled JS (#3981)", () => {
|
|
159
|
+
let fromSource: Counted;
|
|
160
|
+
let fromDist: Counted;
|
|
161
|
+
|
|
162
|
+
beforeAll(() => {
|
|
163
|
+
const tsxLoader = pathToFileURL(join(repoModules, "tsx", "dist", "loader.mjs")).href;
|
|
164
|
+
fromSource = count(
|
|
165
|
+
join(scratch, "count.mjs"),
|
|
166
|
+
{
|
|
167
|
+
core: pathToFileURL(join(coreRoot, "src")).href,
|
|
168
|
+
ext: ".ts",
|
|
169
|
+
names: LEXICONS,
|
|
170
|
+
taggable: pathToFileURL(join(repo, "lexicons", "aws", "src", "taggable.ts")).href,
|
|
171
|
+
},
|
|
172
|
+
// The repository's lexicons through their `development` export, src/*.ts, as bin/chant runs from source.
|
|
173
|
+
["--conditions=development", "--import", tsxLoader],
|
|
174
|
+
);
|
|
175
|
+
fromDist = count(join(scratch, "count.mjs"), {
|
|
176
|
+
core: pathToFileURL(coreDist).href,
|
|
177
|
+
ext: ".js",
|
|
178
|
+
names: LEXICONS,
|
|
179
|
+
taggable: "@intentius/chant-lexicon-aws/taggable",
|
|
180
|
+
});
|
|
181
|
+
}, 300_000);
|
|
182
|
+
|
|
183
|
+
test("plain node loads every plugin with no tsx and no TypeScript module", () => {
|
|
184
|
+
expect(fromDist.tsxRegistered).toBe(false);
|
|
185
|
+
expect(fromDist.seen.tsx).toBe(0);
|
|
186
|
+
expect(fromDist.seen.typeScript).toEqual([]);
|
|
187
|
+
// The comparison run really loads the lexicons' source.
|
|
188
|
+
expect(fromSource.seen.typeScript.some((path) => path.includes("/lexicons/aws/src/"))).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("the compiled plugins find the same lint rules as the source", () => {
|
|
192
|
+
for (const name of LEXICONS) {
|
|
193
|
+
expect(fromDist.plugins[name]!.lintRules, name).toEqual(fromSource.plugins[name]!.lintRules);
|
|
194
|
+
}
|
|
195
|
+
// The two that scan a directory for their rules found some.
|
|
196
|
+
expect(fromDist.plugins.aws!.lintRules.length).toBeGreaterThan(0);
|
|
197
|
+
expect(fromDist.plugins.helm!.lintRules.length).toBeGreaterThan(0);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("the compiled plugins find the same post-synth checks as the source", () => {
|
|
201
|
+
for (const name of LEXICONS) {
|
|
202
|
+
expect(fromDist.plugins[name]!.postSynthChecks, name).toEqual(fromSource.plugins[name]!.postSynthChecks);
|
|
203
|
+
}
|
|
204
|
+
expect(fromDist.plugins.aws!.postSynthChecks.length).toBeGreaterThan(0);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("the compiled plugins read their skills from src/", () => {
|
|
208
|
+
for (const name of LEXICONS) {
|
|
209
|
+
expect(fromDist.plugins[name]!.skills, name).toEqual(fromSource.plugins[name]!.skills);
|
|
210
|
+
}
|
|
211
|
+
expect(fromDist.plugins.aws!.skills.length).toBeGreaterThan(0);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("a compiled module reads the generated JSON under src/", () => {
|
|
215
|
+
expect(fromSource.taggable).toBeGreaterThan(0);
|
|
216
|
+
expect(fromDist.taggable).toBe(fromSource.taggable);
|
|
217
|
+
for (const name of LEXICONS) {
|
|
218
|
+
expect(fromDist.plugins[name]!.catalogs, name).toEqual(fromSource.plugins[name]!.catalogs);
|
|
219
|
+
for (const size of Object.values(fromDist.plugins[name]!.catalogs)) expect(typeof size, `${name}: ${size}`).toBe("number");
|
|
220
|
+
}
|
|
221
|
+
expect(fromDist.plugins.aws!.catalogs["aws:resource-catalog"]).toBeGreaterThan(0);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("build and lint run against the compiled aws lexicon and load none of its TypeScript", () => {
|
|
225
|
+
const tracer = join(scratch, "trace.mjs");
|
|
226
|
+
writeFileSync(
|
|
227
|
+
tracer,
|
|
228
|
+
`import { registerHooks } from "node:module";\nimport { appendFileSync } from "node:fs";\n` +
|
|
229
|
+
`const seen = [];\n` +
|
|
230
|
+
`registerHooks({ load(url, context, next) { if (/\\/(lexicons|chant-lexicon-[a-z0-9]+)\\/src\\/.*\\.ts$/.test(url)) seen.push(url); return next(url, context); } });\n` +
|
|
231
|
+
`process.on("exit", () => appendFileSync(process.env.TRACE_OUT, JSON.stringify(seen) + "\\n"));\n`,
|
|
232
|
+
);
|
|
233
|
+
const main = join(coreDist, "cli", "main.js");
|
|
234
|
+
const run = (args: string[]) => {
|
|
235
|
+
const out = join(scratch, `trace-${args[0]}.jsonl`);
|
|
236
|
+
const env: NodeJS.ProcessEnv = { ...process.env, TRACE_OUT: out };
|
|
237
|
+
delete env.CHANT_ENV;
|
|
238
|
+
const r = spawnSync(process.execPath, ["--import", pathToFileURL(tracer).href, main, ...args], { cwd: project, encoding: "utf-8", env });
|
|
239
|
+
return { ...r, lexiconTypeScript: JSON.parse(readFileSync(out, "utf-8").trim().split("\n")[0]!) as string[] };
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const built = run(["build", "src", "-o", "out/template.json"]);
|
|
243
|
+
expect(built.status, built.stderr).toBe(0);
|
|
244
|
+
const template = JSON.parse(readFileSync(join(project, "out", "template.json"), "utf-8")) as {
|
|
245
|
+
Resources: Record<string, { Type: string; Properties: { QueueName?: string } }>;
|
|
246
|
+
};
|
|
247
|
+
expect(Object.values(template.Resources).map((r) => [r.Type, r.Properties.QueueName])).toEqual([["AWS::SQS::Queue", "lexicon-dist-e2e"]]);
|
|
248
|
+
expect(built.lexiconTypeScript).toEqual([]);
|
|
249
|
+
|
|
250
|
+
const linted = run(["lint", "src"]);
|
|
251
|
+
expect(linted.status, linted.stdout + linted.stderr).toBe(0);
|
|
252
|
+
expect(linted.lexiconTypeScript).toEqual([]);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
@@ -201,3 +201,50 @@ describe("lexicons declared by module path (#2520)", () => {
|
|
|
201
201
|
expect(typeof evaluator.evaluateGatePolicy).toBe("function");
|
|
202
202
|
});
|
|
203
203
|
});
|
|
204
|
+
|
|
205
|
+
describe("tsx only for a lexicon that resolves to TypeScript (#3981)", () => {
|
|
206
|
+
const STATE = Symbol.for("chant.compiledCli");
|
|
207
|
+
const global = globalThis as { [STATE]?: { tsx: boolean } };
|
|
208
|
+
let dir: string;
|
|
209
|
+
|
|
210
|
+
beforeEach(async () => {
|
|
211
|
+
dir = await mkdtemp(join(tmpdir(), "chant-lexicon-js-"));
|
|
212
|
+
// Stand in for the compiled CLI before it has registered tsx.
|
|
213
|
+
global[STATE] = { tsx: false };
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
afterEach(async () => {
|
|
217
|
+
delete global[STATE];
|
|
218
|
+
await rm(dir, { recursive: true, force: true });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
async function jsPackage(root: string, name: string): Promise<void> {
|
|
222
|
+
const pkg = join(root, "node_modules", "@intentius", `chant-lexicon-${name}`);
|
|
223
|
+
await mkdir(join(pkg, "dist"), { recursive: true });
|
|
224
|
+
await writeFile(join(pkg, "package.json"), JSON.stringify({ name: `@intentius/chant-lexicon-${name}`, type: "module", exports: { ".": "./dist/index.js" } }));
|
|
225
|
+
await writeFile(join(pkg, "dist", "index.js"), `export const marker = ${JSON.stringify(name)};\n`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
test("a compiled package the project installs imports with plain node", async () => {
|
|
229
|
+
await jsPackage(dir, "jsonly");
|
|
230
|
+
const { importLexiconPackage } = await import("./lexicon-module");
|
|
231
|
+
const mod = await importLexiconPackage("@intentius/chant-lexicon-jsonly", dir);
|
|
232
|
+
expect(mod.marker).toBe("jsonly");
|
|
233
|
+
expect(global[STATE]).toEqual({ tsx: false });
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("a compiled module named by absolute path imports with plain node", async () => {
|
|
237
|
+
const path = join(dir, "lexicon.mjs");
|
|
238
|
+
await writeFile(path, `export const marker = "by-path";\n`);
|
|
239
|
+
const { importLexiconPackage } = await import("./lexicon-module");
|
|
240
|
+
const mod = await importLexiconPackage(path, dir);
|
|
241
|
+
expect(mod.marker).toBe("by-path");
|
|
242
|
+
expect(global[STATE]).toEqual({ tsx: false });
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("a package missing from the project too rethrows the first error and registers nothing", async () => {
|
|
246
|
+
const { importLexiconPackage } = await import("./lexicon-module");
|
|
247
|
+
await expect(importLexiconPackage("@intentius/chant-lexicon-not-installed-3981", dir)).rejects.toThrow(/chant-lexicon-not-installed-3981/);
|
|
248
|
+
expect(global[STATE]).toEqual({ tsx: false });
|
|
249
|
+
});
|
|
250
|
+
});
|
package/src/lexicon-module.ts
CHANGED
|
@@ -27,8 +27,9 @@
|
|
|
27
27
|
import { createRequire } from "node:module";
|
|
28
28
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
29
29
|
import { pathToFileURL } from "node:url";
|
|
30
|
-
import { ensureTypeScriptLoader } from "./ts-loader";
|
|
30
|
+
import { ensureTypeScriptLoader, ensureTypeScriptLoaderFor, typeScriptLoaderPending } from "./ts-loader";
|
|
31
31
|
import { esModuleNamespace } from "./module-interop";
|
|
32
|
+
import { isJavaScriptModule } from "./module-source";
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* One `lexicons` entry: a package-backed name, or a name plus the module that
|
|
@@ -180,6 +181,9 @@ export function resetLexiconModules(): void {
|
|
|
180
181
|
* `typeScript: false` imports it with plain node, without registering tsx
|
|
181
182
|
* (#3276): tsx's ESM hooks run on a thread of their own, which costs the
|
|
182
183
|
* compiled CLI about 200 MB, so a caller whose module is JavaScript says so.
|
|
184
|
+
* Left out, the module's extension decides (#3981): a `.js` or `.mjs` module
|
|
185
|
+
* imports with plain node, and anything else (`.ts`, or no extension, which
|
|
186
|
+
* only tsx's resolver completes) registers tsx first.
|
|
183
187
|
*/
|
|
184
188
|
export async function importLexiconModule(
|
|
185
189
|
name: string,
|
|
@@ -188,7 +192,8 @@ export async function importLexiconModule(
|
|
|
188
192
|
const path = modulePaths.get(name);
|
|
189
193
|
if (path === undefined) return undefined;
|
|
190
194
|
try {
|
|
191
|
-
|
|
195
|
+
const typeScript = options.typeScript ?? !isJavaScriptModule(path);
|
|
196
|
+
if (typeScript) ensureTypeScriptLoader();
|
|
192
197
|
return esModuleNamespace((await import(path)) as Record<string, unknown>);
|
|
193
198
|
} catch (err) {
|
|
194
199
|
throw new Error(
|
|
@@ -258,6 +263,18 @@ export function resolveFromProject(spec: string, fromDir: string = process.cwd()
|
|
|
258
263
|
}
|
|
259
264
|
}
|
|
260
265
|
|
|
266
|
+
/**
|
|
267
|
+
* #3981 — where `spec` resolves from this module, as `import(spec)` here
|
|
268
|
+
* would resolve it. Throws as the import would when it cannot be resolved.
|
|
269
|
+
* An absolute path is its own file URL.
|
|
270
|
+
*/
|
|
271
|
+
function resolveImport(spec: string): string {
|
|
272
|
+
if (isAbsolute(spec)) return pathToFileURL(spec).href;
|
|
273
|
+
const resolveMeta = (import.meta as { resolve?: (specifier: string) => string }).resolve;
|
|
274
|
+
if (typeof resolveMeta === "function") return resolveMeta(spec);
|
|
275
|
+
return pathToFileURL(createRequire(import.meta.url).resolve(spec)).href;
|
|
276
|
+
}
|
|
277
|
+
|
|
261
278
|
/**
|
|
262
279
|
* chant#2845 — import a lexicon package, or one of its subpaths, by its bare
|
|
263
280
|
* specifier. chant's own install is tried first, as a bare `import()` always
|
|
@@ -266,16 +283,41 @@ export function resolveFromProject(spec: string, fromDir: string = process.cwd()
|
|
|
266
283
|
* that installs its lexicons in its own node_modules, it is resolved from
|
|
267
284
|
* `fromDir` (the project) and imported from there. Any other failure, and a
|
|
268
285
|
* package the project doesn't have either, rethrows the first error.
|
|
286
|
+
*
|
|
287
|
+
* #3981 — tsx is registered only when the module resolves to TypeScript. A
|
|
288
|
+
* lexicon that ships compiled JS imports with plain node. The module is
|
|
289
|
+
* resolved before it is imported, because a failed import is cached: one
|
|
290
|
+
* tried without tsx cannot be tried again with it. A resolution that fails
|
|
291
|
+
* for any reason but a missing package registers tsx, whose resolver may
|
|
292
|
+
* still complete it.
|
|
269
293
|
*/
|
|
270
294
|
export async function importLexiconPackage(spec: string, fromDir: string = process.cwd()): Promise<Record<string, unknown>> {
|
|
295
|
+
const pkg = packageNameOf(spec);
|
|
296
|
+
if (typeScriptLoaderPending()) {
|
|
297
|
+
let resolved: string | undefined;
|
|
298
|
+
try {
|
|
299
|
+
resolved = resolveImport(spec);
|
|
300
|
+
} catch (err) {
|
|
301
|
+
if (pkg === undefined || !isPackageNotFound(err, pkg)) {
|
|
302
|
+
ensureTypeScriptLoader();
|
|
303
|
+
} else {
|
|
304
|
+
return importFromProject(spec, fromDir, err);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (resolved !== undefined) ensureTypeScriptLoaderFor(resolved);
|
|
308
|
+
}
|
|
271
309
|
try {
|
|
272
|
-
ensureTypeScriptLoader();
|
|
273
310
|
return esModuleNamespace((await import(spec)) as Record<string, unknown>);
|
|
274
311
|
} catch (err) {
|
|
275
|
-
const pkg = packageNameOf(spec);
|
|
276
312
|
if (pkg === undefined || !isPackageNotFound(err, pkg)) throw err;
|
|
277
|
-
|
|
278
|
-
if (resolved === undefined) throw err;
|
|
279
|
-
return esModuleNamespace((await import(pathToFileURL(resolved).href)) as Record<string, unknown>);
|
|
313
|
+
return importFromProject(spec, fromDir, err);
|
|
280
314
|
}
|
|
281
315
|
}
|
|
316
|
+
|
|
317
|
+
/** The project's copy of `spec`, for {@link importLexiconPackage}. Rethrows `err` when the project has none. */
|
|
318
|
+
async function importFromProject(spec: string, fromDir: string, err: unknown): Promise<Record<string, unknown>> {
|
|
319
|
+
const resolved = resolveFromProject(spec, fromDir);
|
|
320
|
+
if (resolved === undefined) throw err;
|
|
321
|
+
ensureTypeScriptLoaderFor(resolved);
|
|
322
|
+
return esModuleNamespace((await import(pathToFileURL(resolved).href)) as Record<string, unknown>);
|
|
323
|
+
}
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { readFileSync } from "fs";
|
|
10
|
-
import { join
|
|
11
|
-
import {
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { moduleSourceDir } from "./module-source";
|
|
12
12
|
import type { SkillDefinition } from "./lexicon";
|
|
13
13
|
import type { McpToolContribution, McpResourceContribution } from "./mcp/types";
|
|
14
14
|
import type { Serializer } from "./serializer";
|
|
@@ -44,7 +44,8 @@ export function createSkillsLoader(
|
|
|
44
44
|
specs: SkillFileSpec[],
|
|
45
45
|
): () => SkillDefinition[] {
|
|
46
46
|
return () => {
|
|
47
|
-
|
|
47
|
+
// Skills are markdown beside the source; a compiled plugin finds them under src/ (#3981).
|
|
48
|
+
const skillsDir = join(moduleSourceDir(importMetaUrl), "skills");
|
|
48
49
|
return specs.map(({ file, ...meta }) => {
|
|
49
50
|
try {
|
|
50
51
|
const content = readFileSync(join(skillsDir, file), "utf-8");
|
|
@@ -123,7 +124,8 @@ export function createCatalogResource(
|
|
|
123
124
|
description,
|
|
124
125
|
mimeType: "application/json",
|
|
125
126
|
async handler(): Promise<string> {
|
|
126
|
-
|
|
127
|
+
// Generated JSON sits beside the source; a compiled plugin finds it under src/ (#3981).
|
|
128
|
+
const dir = moduleSourceDir(importMetaUrl);
|
|
127
129
|
const lexicon = JSON.parse(
|
|
128
130
|
readFileSync(join(dir, "generated", lexiconJsonFile), "utf-8"),
|
|
129
131
|
) as Record<string, { resourceType: string; kind: string }>;
|
package/src/lexicon.ts
CHANGED
|
@@ -554,6 +554,8 @@ export interface ComponentPipelineOptions {
|
|
|
554
554
|
* `wave-<k>` runs `chant components fan-out --wave-gate <gate> --wave <k>`
|
|
555
555
|
* and hands the attempt record to the next job. See
|
|
556
556
|
* `components/gated-wave-pipeline.ts`. Unset, the pipeline is unchanged.
|
|
557
|
+
* Beside `prLoop` (#4003) it gives the pull request's apply a gate per
|
|
558
|
+
* wave instead: one apply job per wave (`prLoopWithWaves`).
|
|
557
559
|
*/
|
|
558
560
|
gatedWaves?: GatedWavePipelineOptions;
|
|
559
561
|
/**
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import type { StackGraph } from "../build";
|
|
15
15
|
import type { Serializer } from "../serializer";
|
|
16
16
|
import type { Declarable } from "../declarable";
|
|
17
|
+
import { EMPTY_TREE_SHA } from "../components/changed-files";
|
|
17
18
|
|
|
18
19
|
const execFileAsync = promisify(execFile);
|
|
19
20
|
|
|
@@ -136,6 +137,12 @@ describe("affectedStacks — baseDir (caller-supplied)", () => {
|
|
|
136
137
|
const r = await affectedStacks({ projectPath: head, baseDir: base, serializers: [fakeSerializer] });
|
|
137
138
|
expect(r.changed).toEqual([]);
|
|
138
139
|
});
|
|
140
|
+
|
|
141
|
+
test("the empty tree as the base (a root commit, #4004) builds nothing: every head stack is changed", async () => {
|
|
142
|
+
const head = srcDir("head", widget("Widget"));
|
|
143
|
+
const r = await affectedStacks({ projectPath: head, baseRef: EMPTY_TREE_SHA, serializers: [fakeSerializer] });
|
|
144
|
+
expect(r.changed).toEqual(["fake"]);
|
|
145
|
+
});
|
|
139
146
|
});
|
|
140
147
|
|
|
141
148
|
// A second lexicon, so a stack that serializes through two of them can be shown
|
|
@@ -37,6 +37,7 @@ import { tmpdir } from "node:os";
|
|
|
37
37
|
import { build, type StackGraph } from "../build";
|
|
38
38
|
import { getPrimaryOutput } from "../lint/post-synth";
|
|
39
39
|
import type { Serializer } from "../serializer";
|
|
40
|
+
import { EMPTY_TREE_SHA } from "../components/changed-files";
|
|
40
41
|
import type { Declarable } from "../declarable";
|
|
41
42
|
|
|
42
43
|
const execFileAsync = promisify(execFile);
|
|
@@ -258,7 +259,9 @@ export interface AffectedStacksOptions {
|
|
|
258
259
|
*/
|
|
259
260
|
export async function affectedStacks(opts: AffectedStacksOptions): Promise<AffectedResult> {
|
|
260
261
|
const projectPath = resolve(opts.projectPath);
|
|
261
|
-
|
|
262
|
+
// The empty tree (#4004) builds nothing, so it needs no worktree.
|
|
263
|
+
const emptyBase = !opts.baseDir && opts.baseRef === EMPTY_TREE_SHA;
|
|
264
|
+
const needsWorktree = Boolean(opts.headRef) || Boolean(opts.baseRef && !opts.baseDir && !emptyBase);
|
|
262
265
|
const repoRoot = needsWorktree ? await gitTopLevel(projectPath) : projectPath;
|
|
263
266
|
// Canonicalize via realpath so the worktree-relative path is correct even when
|
|
264
267
|
// the temp dir is a symlink (macOS /var → /private/var) and `git
|
|
@@ -293,6 +296,8 @@ export async function affectedStacks(opts: AffectedStacksOptions): Promise<Affec
|
|
|
293
296
|
let baseMap: Map<string, string>;
|
|
294
297
|
if (opts.baseDir) {
|
|
295
298
|
baseMap = (await readArtifacts(resolve(opts.baseDir), false)).artifacts;
|
|
299
|
+
} else if (emptyBase) {
|
|
300
|
+
baseMap = new Map();
|
|
296
301
|
} else if (opts.baseRef) {
|
|
297
302
|
baseMap = await withWorktree(repoRoot, opts.baseRef, async (dir) => (await readArtifacts(join(dir, relProject), false)).artifacts);
|
|
298
303
|
} else {
|
|
@@ -58,11 +58,23 @@ export interface RunOrigin {
|
|
|
58
58
|
repo?: string;
|
|
59
59
|
/** Resolved link to the run, when the environment provided enough to build one. */
|
|
60
60
|
url?: string;
|
|
61
|
+
/**
|
|
62
|
+
* The forge's id for the job within the run (#4156): `GITHUB_JOB` on GitHub
|
|
63
|
+
* and Forgejo, the job's key in the workflow, and `CI_JOB_ID` on GitLab.
|
|
64
|
+
* Only `chant ci run` in a forge job writes it ({@link resolveCiJob}),
|
|
65
|
+
* from {@link RUN_ORIGIN_JOB_FLOOR}.
|
|
66
|
+
*/
|
|
67
|
+
jobId?: string;
|
|
68
|
+
/** The run's attempt, `GITHUB_RUN_ATTEMPT` on GitHub and Forgejo (#4156). GitLab gives a retried job a new `jobId` instead. */
|
|
69
|
+
attempt?: number;
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
/** The first chant that writes `runOrigin.forge: "chant"`, for a job `chant ci run` ran (#4094). */
|
|
64
73
|
export const RUN_ORIGIN_CHANT_FLOOR = "0.124.30";
|
|
65
74
|
|
|
75
|
+
/** The first chant that writes `runOrigin.jobId` and `runOrigin.attempt`, for a forge job that ran `chant ci run` (#4156). */
|
|
76
|
+
export const RUN_ORIGIN_JOB_FLOOR = "0.124.32";
|
|
77
|
+
|
|
66
78
|
/**
|
|
67
79
|
* The gate resolution that admitted a deploy (#4020): the gate's address in the
|
|
68
80
|
* gate ledger (`_gates/<op>.jsonl`, `op` and `gate`) and when the resolution
|
|
@@ -310,6 +322,28 @@ export function resolveCiRun(env: NodeJS.ProcessEnv = process.env): { runId: str
|
|
|
310
322
|
return undefined;
|
|
311
323
|
}
|
|
312
324
|
|
|
325
|
+
/**
|
|
326
|
+
* The CI run the environment names, as {@link resolveCiRun} has it, with the
|
|
327
|
+
* job within the run and the run's attempt when the forge gives them
|
|
328
|
+
* (#4156). `chant ci run` in a forge job records this as its run's origin;
|
|
329
|
+
* a release record keeps {@link resolveCiRun}'s shape.
|
|
330
|
+
*/
|
|
331
|
+
export function resolveCiJob(env: NodeJS.ProcessEnv = process.env): { runId: string; runOrigin: RunOrigin } | undefined {
|
|
332
|
+
const ci = resolveCiRun(env);
|
|
333
|
+
if (!ci) return undefined;
|
|
334
|
+
const github = ci.runOrigin.forge === "github";
|
|
335
|
+
const jobId = github ? env.GITHUB_JOB : env.CI_JOB_ID;
|
|
336
|
+
const attempt = github ? Number(env.GITHUB_RUN_ATTEMPT) : NaN;
|
|
337
|
+
return {
|
|
338
|
+
runId: ci.runId,
|
|
339
|
+
runOrigin: {
|
|
340
|
+
...ci.runOrigin,
|
|
341
|
+
...(jobId ? { jobId } : {}),
|
|
342
|
+
...(Number.isInteger(attempt) && attempt > 0 ? { attempt } : {}),
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
313
347
|
/**
|
|
314
348
|
* Append one immutable release record to the `<env>` ledger on the
|
|
315
349
|
* `chant/lifecycle` orphan branch. Throws `InvalidReleaseRecordError` before
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #2893: work worktrees live at `.chant/work/[<member>/]<item>` in the
|
|
3
|
+
* checkout, excluded through `<git-common-dir>/info/exclude`, and the
|
|
4
|
+
* hand-written walkers skip them.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, test } from "vitest";
|
|
7
|
+
import { withTestDir } from "@intentius/chant-test-utils";
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { excludeWorkDir, isWorkDir, removeEmptyWorkDirs, workWorktreePath, legacyWorkWorktreePath, WORK_DIR_EXCLUDE } from "./work-dir";
|
|
11
|
+
import { listFiles } from "../workspace/export";
|
|
12
|
+
|
|
13
|
+
describe("the work directory", () => {
|
|
14
|
+
test("a worktree is .chant/work/[<member>/]<item> in the checkout; older versions made it under the git directory", () => {
|
|
15
|
+
expect(workWorktreePath("/repo", [], "W-1")).toBe(join("/repo", ".chant", "work", "W-1"));
|
|
16
|
+
expect(workWorktreePath("/repo", ["app"], "W-1")).toBe(join("/repo", ".chant", "work", "app", "W-1"));
|
|
17
|
+
expect(legacyWorkWorktreePath("/repo/.git", ["app"], "W-1")).toBe(join("/repo/.git", "chant-work", "app", "W-1"));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("isWorkDir is .chant/work at the walk's root or below it, and nothing else in .chant", () => {
|
|
21
|
+
expect(isWorkDir(".chant/work")).toBe(true);
|
|
22
|
+
expect(isWorkDir("members/app/.chant/work")).toBe(true);
|
|
23
|
+
expect(isWorkDir(".chant")).toBe(false);
|
|
24
|
+
expect(isWorkDir(".chant/factory")).toBe(false);
|
|
25
|
+
expect(isWorkDir("x.chant/work")).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("the exclude line is written once, after any last line without a newline, and not when git already ignores the directory", async () => {
|
|
29
|
+
await withTestDir(async (dir) => {
|
|
30
|
+
const common = join(dir, "git");
|
|
31
|
+
expect(excludeWorkDir(common, false)).toBe(true);
|
|
32
|
+
expect(readFileSync(join(common, "info", "exclude"), "utf-8")).toBe(`${WORK_DIR_EXCLUDE}\n`);
|
|
33
|
+
expect(excludeWorkDir(common, false)).toBe(false);
|
|
34
|
+
expect(readFileSync(join(common, "info", "exclude"), "utf-8")).toBe(`${WORK_DIR_EXCLUDE}\n`);
|
|
35
|
+
|
|
36
|
+
const other = join(dir, "other");
|
|
37
|
+
mkdirSync(join(other, "info"), { recursive: true });
|
|
38
|
+
writeFileSync(join(other, "info", "exclude"), "*.log");
|
|
39
|
+
expect(excludeWorkDir(other, false)).toBe(true);
|
|
40
|
+
expect(readFileSync(join(other, "info", "exclude"), "utf-8")).toBe(`*.log\n${WORK_DIR_EXCLUDE}\n`);
|
|
41
|
+
|
|
42
|
+
const ignored = join(dir, "ignored");
|
|
43
|
+
expect(excludeWorkDir(ignored, true)).toBe(false);
|
|
44
|
+
expect(existsSync(join(ignored, "info", "exclude"))).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("removing a worktree takes the empty directories above it up to .chant, and stops at one that holds anything", async () => {
|
|
49
|
+
await withTestDir(async (dir) => {
|
|
50
|
+
// The worktree itself is gone already: removed, its parent left empty.
|
|
51
|
+
const tree = workWorktreePath(dir, ["app"], "W-1");
|
|
52
|
+
mkdirSync(dirname(tree), { recursive: true });
|
|
53
|
+
mkdirSync(workWorktreePath(dir, [], "W-2"), { recursive: true });
|
|
54
|
+
removeEmptyWorkDirs(tree);
|
|
55
|
+
expect(existsSync(dirname(tree))).toBe(false);
|
|
56
|
+
expect(existsSync(join(dir, ".chant", "work", "W-2"))).toBe(true);
|
|
57
|
+
|
|
58
|
+
const only = join(dir, "only");
|
|
59
|
+
mkdirSync(join(only, ".chant", "work"), { recursive: true });
|
|
60
|
+
removeEmptyWorkDirs(workWorktreePath(only, [], "W-3"));
|
|
61
|
+
expect(existsSync(join(only, ".chant"))).toBe(false);
|
|
62
|
+
expect(existsSync(only)).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("walkers skip .chant/work", () => {
|
|
68
|
+
test("export's file list outside git leaves out a work worktree and keeps the rest of .chant", async () => {
|
|
69
|
+
await withTestDir(async (dir) => {
|
|
70
|
+
for (const p of ["a.ts", ".chant/workspace.lock.json", ".chant/work/W-1/a.ts", "m/.chant/work/W-2/a.ts"]) {
|
|
71
|
+
mkdirSync(dirname(join(dir, p)), { recursive: true });
|
|
72
|
+
writeFileSync(join(dir, p), "x");
|
|
73
|
+
}
|
|
74
|
+
expect(listFiles(dir)).toEqual([".chant/workspace.lock.json", "a.ts"]);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|