@intentius/chant 0.124.26 → 0.124.27
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/op/activity-registry.d.ts +6 -3
- package/dist/op/activity-registry.d.ts.map +1 -1
- package/dist/op/activity-registry.js +7 -5
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/discover.js +14 -7
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/runtimes/local.js +4 -2
- package/dist/wave-runtime.mjs +220 -242
- package/package.json +1 -1
- package/src/cli/compiled-cli.e2e.test.ts +31 -6
- package/src/op/activity-registry.ts +7 -5
- package/src/op/discover-root.test.ts +39 -0
- package/src/op/discover.ts +15 -8
- package/src/op/runtimes/local.ts +4 -2
package/package.json
CHANGED
|
@@ -18,6 +18,7 @@ import { execFileSync, spawnSync } from "node:child_process";
|
|
|
18
18
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { tmpdir } from "node:os";
|
|
20
20
|
import { join, resolve } from "node:path";
|
|
21
|
+
import { pathToFileURL } from "node:url";
|
|
21
22
|
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
22
23
|
|
|
23
24
|
const coreRoot = resolve(import.meta.dirname, "..", "..");
|
|
@@ -37,12 +38,13 @@ const TRACER = `
|
|
|
37
38
|
import { registerHooks } from "node:module";
|
|
38
39
|
import { appendFileSync } from "node:fs";
|
|
39
40
|
const out = process.env.CHANT_TRACE_OUT;
|
|
40
|
-
const seen = { chantSrc: 0, chantDist: 0, tsx: 0 };
|
|
41
|
+
const seen = { chantSrc: 0, chantDist: 0, tsx: 0, typescript: 0, dist: [] };
|
|
41
42
|
registerHooks({
|
|
42
43
|
load(url, context, next) {
|
|
43
44
|
if (url.includes("/@intentius/chant/src/")) seen.chantSrc++;
|
|
44
|
-
else if (url.includes("/@intentius/chant/dist/")) seen.chantDist++;
|
|
45
|
+
else if (url.includes("/@intentius/chant/dist/")) { seen.chantDist++; seen.dist.push(url.slice(url.indexOf("/@intentius/chant/dist/") + 23)); }
|
|
45
46
|
else if (url.includes("/node_modules/tsx/")) seen.tsx++;
|
|
47
|
+
else if (url.includes("/node_modules/typescript/")) seen.typescript++;
|
|
46
48
|
return next(url, context);
|
|
47
49
|
},
|
|
48
50
|
});
|
|
@@ -59,14 +61,15 @@ interface Run {
|
|
|
59
61
|
status: number | null;
|
|
60
62
|
stdout: string;
|
|
61
63
|
stderr: string;
|
|
62
|
-
loaded
|
|
64
|
+
/** Counts of what loaded, plus each compiled module's path under `dist/`. */
|
|
65
|
+
loaded: { chantSrc: number; chantDist: number; tsx: number; typescript: number; dist: string[] };
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
function run(cwd: string, args: string[]): Run {
|
|
68
|
+
function run(cwd: string, args: string[], extraEnv: NodeJS.ProcessEnv = {}, command = chant): Run {
|
|
66
69
|
const traceOut = join(scratch, `trace-${Math.random().toString(36).slice(2)}.jsonl`);
|
|
67
|
-
const env: NodeJS.ProcessEnv = { ...process.env, NODE_OPTIONS: `--import ${tracer}`, CHANT_TRACE_OUT: traceOut };
|
|
70
|
+
const env: NodeJS.ProcessEnv = { ...process.env, ...extraEnv, NODE_OPTIONS: `--import ${tracer}`, CHANT_TRACE_OUT: traceOut };
|
|
68
71
|
delete env.CHANT_ENV;
|
|
69
|
-
const result = spawnSync(
|
|
72
|
+
const result = spawnSync(command, args, { cwd, env, encoding: "utf-8" });
|
|
70
73
|
const lines = readFileSync(traceOut, "utf-8").trim().split("\n");
|
|
71
74
|
return { status: result.status, stdout: result.stdout, stderr: result.stderr, loaded: JSON.parse(lines[0]!) as Run["loaded"] };
|
|
72
75
|
}
|
|
@@ -192,6 +195,28 @@ describe("the installed chant CLI (#3878)", () => {
|
|
|
192
195
|
expect(r.loaded).toMatchObject({ chantSrc: 0, tsx: 0 });
|
|
193
196
|
});
|
|
194
197
|
|
|
198
|
+
test("workspace status on a steward-index miss loads no tsx for a member without Op files (#3976)", () => {
|
|
199
|
+
// A cache dir of its own, so there is no index to read and the member's stewards are discovered.
|
|
200
|
+
const r = run(join(workspace, "app"), ["workspace", "status", "local", "--json"], { CHANT_CACHE_DIR: mkdtempSync(join(scratch, "cache-")) });
|
|
201
|
+
expect(r.status, r.stderr).toBe(0);
|
|
202
|
+
const status = JSON.parse(r.stdout) as { members: Array<{ name: string; stewards: unknown[] }> };
|
|
203
|
+
expect(status.members.map((m) => [m.name, m.stewards])).toEqual([["app", []]]);
|
|
204
|
+
expect(r.loaded).toMatchObject({ chantSrc: 0, tsx: 0 });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("importing the op entry loads the Op authoring surface, not lint, the LSP, codegen or typescript (#3982)", () => {
|
|
208
|
+
// Every *.op.ts imports @intentius/chant/op, so this is what a steward-index miss in `workspace status` pays per
|
|
209
|
+
// member with Op files. The base activities, which reach lint and typescript, load only when an Op runs.
|
|
210
|
+
const entry = join(project, "node_modules", "@intentius", "chant", "dist", "op", "index.js");
|
|
211
|
+
const script = `const op = await import(${JSON.stringify(pathToFileURL(entry).href)});\n` +
|
|
212
|
+
`if (typeof op.Op !== "function" || typeof op.factoryOp !== "function" || typeof op.declareSteward !== "function") process.exit(2);\n`;
|
|
213
|
+
const r = run(project, ["--input-type=module", "-e", script], {}, process.execPath);
|
|
214
|
+
expect(r.status, r.stderr).toBe(0);
|
|
215
|
+
expect(r.loaded).toMatchObject({ chantSrc: 0, tsx: 0, typescript: 0 });
|
|
216
|
+
const heavy = r.loaded.dist.filter((path) => /^(lint|lsp|codegen|mcp|audit|fold|cli)\//.test(path) || path === "index.js" || path === "op/activities/index.js");
|
|
217
|
+
expect(heavy).toEqual([]);
|
|
218
|
+
});
|
|
219
|
+
|
|
195
220
|
test("build loads the TypeScript lexicon and project through tsx, against the one compiled core", () => {
|
|
196
221
|
const r = run(project, ["build", "src", "-o", "out/render.json"]);
|
|
197
222
|
expect(r.status, r.stderr).toBe(0);
|
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The base activities are core's own (`./activities`): plain async functions
|
|
6
6
|
* taking a single args object, with no workflow engine anywhere in the call
|
|
7
|
-
* path. They
|
|
8
|
-
*
|
|
7
|
+
* path. They ship with core, so `loadActivities([])` works in a project that
|
|
8
|
+
* has installed nothing but chant. They are imported when activities are
|
|
9
|
+
* loaded, not when this module is: their implementations reach lint, codegen
|
|
10
|
+
* and `typescript`, and every `*.op.ts` imports `@intentius/chant/op`, which
|
|
11
|
+
* re-exports this module (#3982). Product-specific appliers stay
|
|
9
12
|
* in their owning lexicon (#809) and are pulled in dynamically per the
|
|
10
13
|
* project's configured lexicon list.
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
|
-
import * as baseActivities from "./activities";
|
|
14
16
|
import { ACTIVITY_PROFILES, type ActivityProfile } from "./activity-profiles";
|
|
15
17
|
import { importLexiconModule, importLexiconPackage } from "../lexicon-module";
|
|
16
18
|
|
|
@@ -63,13 +65,13 @@ function collectActivities(mod: Record<string, unknown>, into: Map<string, Activ
|
|
|
63
65
|
* lexicon's activity of the same name; otherwise a later lexicon's export
|
|
64
66
|
* replaces an earlier one's.
|
|
65
67
|
*
|
|
66
|
-
* Never throws: the base library
|
|
68
|
+
* Never throws: the base library ships with core, so there is no "no
|
|
67
69
|
* activities registered" state to report.
|
|
68
70
|
*/
|
|
69
71
|
export async function loadActivities(lexicons: string[] = []): Promise<Map<string, ActivityFn>> {
|
|
70
72
|
const activities = new Map<string, ActivityFn>();
|
|
71
73
|
|
|
72
|
-
collectActivities(
|
|
74
|
+
collectActivities((await import("./activities")) as unknown as Record<string, unknown>, activities);
|
|
73
75
|
|
|
74
76
|
for (const name of lexicons) {
|
|
75
77
|
try {
|
|
@@ -170,3 +170,42 @@ describe("discoverOps: an Op may be exported by name (#2171)", () => {
|
|
|
170
170
|
expect(errors[0]).toContain(`Duplicate Op name "twice" declared twice in`);
|
|
171
171
|
});
|
|
172
172
|
});
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* #3976. Reading the project's `exclude`/`include` evaluates its config,
|
|
176
|
+
* which registers tsx, so discovery reads them only when an Op file could be
|
|
177
|
+
* found. An `exclude` that is not a list is an error whenever the config is
|
|
178
|
+
* read, which is how these tests tell whether it was.
|
|
179
|
+
*/
|
|
180
|
+
describe("discoverOps reads the config's globs only when there is an Op file (#3976)", () => {
|
|
181
|
+
let checkout: string;
|
|
182
|
+
beforeEach(() => {
|
|
183
|
+
checkout = realpathSync(mkdtempSync(join(tmpdir(), "chant-op-globs-")));
|
|
184
|
+
fakeGitRoot = checkout;
|
|
185
|
+
mkdirSync(join(checkout, "src"), { recursive: true });
|
|
186
|
+
writeFileSync(join(checkout, "src", "infra.ts"), "export const x = 1;\n");
|
|
187
|
+
});
|
|
188
|
+
afterEach(() => rmSync(checkout, { recursive: true, force: true }));
|
|
189
|
+
|
|
190
|
+
test("a project with no Op file has its config left unread", async () => {
|
|
191
|
+
writeFileSync(join(checkout, "chant.config.json"), JSON.stringify({ lexicons: ["aws"], exclude: 42 }));
|
|
192
|
+
const { ops, errors } = await discoverOps({ cwd: checkout });
|
|
193
|
+
expect(errors).toEqual([]);
|
|
194
|
+
expect([...ops.keys()]).toEqual([]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("with an Op file the config is read", async () => {
|
|
198
|
+
writeFileSync(join(checkout, "chant.config.json"), JSON.stringify({ lexicons: ["aws"], exclude: 42 }));
|
|
199
|
+
writeFileSync(join(checkout, "src", "run.op.ts"), OP_FILE("run"));
|
|
200
|
+
await expect(discoverOps({ cwd: checkout })).rejects.toThrow(/exclude/);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("an Op file only `include` re-admits is still found", async () => {
|
|
204
|
+
writeFileSync(join(checkout, "chant.config.json"), JSON.stringify({ lexicons: ["aws"], include: [".ops"] }));
|
|
205
|
+
mkdirSync(join(checkout, ".ops"));
|
|
206
|
+
writeFileSync(join(checkout, ".ops", "hidden.op.ts"), OP_FILE("hidden"));
|
|
207
|
+
const { ops, errors } = await discoverOps({ cwd: checkout });
|
|
208
|
+
expect(errors).toEqual([]);
|
|
209
|
+
expect([...ops.keys()]).toEqual(["hidden"]);
|
|
210
|
+
});
|
|
211
|
+
});
|
package/src/op/discover.ts
CHANGED
|
@@ -57,22 +57,29 @@ async function findDiscoveryRoot(cwd?: string): Promise<string> {
|
|
|
57
57
|
return gitRoot;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
const isOpFile = (name: string, full: string): boolean =>
|
|
61
|
+
name.endsWith(".op.ts") && !name.endsWith(".test.ts") && !name.endsWith(".spec.ts") && !hasDiscoveryMarkerSync(full);
|
|
62
|
+
|
|
60
63
|
/**
|
|
61
64
|
* Every `*.op.ts` file below the Op root, through the one discovery walk
|
|
62
65
|
* (`../discovery/walk.ts`, #2527). The walk down stops at child projects
|
|
63
66
|
* whatever the root is, so a configless layout whose Op root is the git root
|
|
64
67
|
* no longer collects its sibling projects' Ops. The project's
|
|
65
68
|
* `exclude`/`include` globs and the skip marker apply.
|
|
69
|
+
*
|
|
70
|
+
* The globs come from evaluating the project config, which registers tsx
|
|
71
|
+
* (#3976), so they are resolved only when an Op file could be found. A walk
|
|
72
|
+
* that includes `**` first reaches every path any `include` could re-admit,
|
|
73
|
+
* and stops at the first Op file. When it finds none, no globs could find
|
|
74
|
+
* one either.
|
|
66
75
|
*/
|
|
67
76
|
async function collectOpFiles(root: string): Promise<string[]> {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
name.endsWith(".op.ts") && !name.endsWith(".test.ts") && !name.endsWith(".spec.ts") && !hasDiscoveryMarkerSync(full),
|
|
75
|
-
});
|
|
77
|
+
const excludeDirs = await workspaceMemberDirs(root);
|
|
78
|
+
// A limit of 0 ends the walk at the first file it accepts.
|
|
79
|
+
const any = { max: 0, truncated: false };
|
|
80
|
+
walkDiscovery({ walker: "ops", root, globs: { root, exclude: [], include: ["**"] }, excludeDirs, limit: any, accept: isOpFile });
|
|
81
|
+
if (!any.truncated) return [];
|
|
82
|
+
return walkDiscovery({ walker: "ops", root, globs: await resolveDiscoveryGlobs(root), excludeDirs, accept: isOpFile });
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
/** The `OpConfig` behind an exported value, or `undefined` when the value is not an Op. An Op entity carries its config on `.props` (`./resource.ts`); `name` and `phases` are what every consumer of a discovered Op reads. */
|
package/src/op/runtimes/local.ts
CHANGED
|
@@ -25,7 +25,6 @@ import { runOpLocally, OpRunFailure, type OpRunResult } from "../local-executor"
|
|
|
25
25
|
import { loadActivityContracts } from "../activity-contract-registry";
|
|
26
26
|
import { resumeGatesOf } from "../gate-checkpoint";
|
|
27
27
|
import { stepHeartbeatMs } from "../step-console";
|
|
28
|
-
import { runComponents } from "../../components/cli-support";
|
|
29
28
|
import { discoverOps } from "../discover";
|
|
30
29
|
import { readRunLedger, runEnvOf, DEFAULT_RUN_ENV } from "../../lifecycle/run-ledger";
|
|
31
30
|
import type { OpConfig } from "../types";
|
|
@@ -237,7 +236,10 @@ export function createLocalOpRuntime(opts: { projectPath?: string } = {}): OpRun
|
|
|
237
236
|
);
|
|
238
237
|
},
|
|
239
238
|
|
|
240
|
-
runComponents(path, selector, options) {
|
|
239
|
+
async runComponents(path, selector, options) {
|
|
240
|
+
// Imported here, not at the top: the components driver is most of core's components tree, and this module is
|
|
241
|
+
// re-exported by `@intentius/chant/op`, which every `*.op.ts` imports (#3982).
|
|
242
|
+
const { runComponents } = await import("../../components/cli-support");
|
|
241
243
|
return runComponents(path, selector, options);
|
|
242
244
|
},
|
|
243
245
|
};
|