@intentius/chant 0.23.0 → 0.25.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.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts +8 -0
- 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 +8 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/config-import.d.ts +33 -0
- package/dist/config-import.d.ts.map +1 -0
- package/dist/config-sandbox.d.ts +47 -0
- package/dist/config-sandbox.d.ts.map +1 -0
- package/dist/config.d.ts +29 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/entity-wire-codec.d.ts +1 -1
- package/dist/discovery/entity-wire-codec.d.ts.map +1 -1
- package/dist/discovery/sandbox/config-run.d.ts +24 -0
- package/dist/discovery/sandbox/config-run.d.ts.map +1 -0
- package/dist/discovery/sandbox/config-wire.d.ts +68 -0
- package/dist/discovery/sandbox/config-wire.d.ts.map +1 -0
- package/dist/discovery/sandbox/driver.d.ts +20 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -1
- package/dist/discovery/sandbox/fork.d.ts +52 -0
- package/dist/discovery/sandbox/fork.d.ts.map +1 -0
- package/dist/discovery/sandbox/run.d.ts +10 -20
- package/dist/discovery/sandbox/run.d.ts.map +1 -1
- package/dist/lexicon-output.d.ts +62 -6
- package/dist/lexicon-output.d.ts.map +1 -1
- package/dist/lint/config.d.ts +7 -12
- package/dist/lint/config.d.ts.map +1 -1
- package/dist/project-root.d.ts +51 -0
- package/dist/project-root.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/build.test.ts +19 -0
- package/src/build.ts +21 -10
- package/src/cli/commands/build.ts +42 -6
- package/src/cli/handlers/build.test.ts +11 -9
- package/src/cli/handlers/build.ts +7 -2
- package/src/cli/handlers/run.test.ts +31 -0
- package/src/cli/handlers/run.ts +15 -0
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +27 -3
- package/src/cli/plugins.ts +10 -2
- package/src/config-import.ts +43 -0
- package/src/config-sandbox.ts +138 -0
- package/src/config.ts +37 -5
- package/src/discovery/entity-wire-codec.ts +21 -12
- package/src/discovery/entity-wire.test.ts +26 -0
- package/src/discovery/sandbox/config-boundary.test.ts +239 -0
- package/src/discovery/sandbox/config-run.ts +130 -0
- package/src/discovery/sandbox/config-wire.test.ts +110 -0
- package/src/discovery/sandbox/config-wire.ts +174 -0
- package/src/discovery/sandbox/driver.ts +68 -0
- package/src/discovery/sandbox/fork.ts +110 -0
- package/src/discovery/sandbox/run.ts +28 -85
- package/src/lexicon-output.test.ts +137 -1
- package/src/lexicon-output.ts +112 -13
- package/src/lint/config.test.ts +9 -4
- package/src/lint/config.ts +15 -27
- package/src/lint/policy.ts +5 -5
- package/src/project-root.test.ts +105 -0
- package/src/project-root.ts +78 -0
package/dist/lint/config.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { Severity, RuleConfig } from "./rule.js";
|
|
3
|
+
export { findProjectRoot } from "../project-root.js";
|
|
3
4
|
export declare const LintConfigSchema: z.ZodObject<{
|
|
4
5
|
rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
|
|
5
6
|
error: "error";
|
|
@@ -75,18 +76,6 @@ export declare function parseRuleConfig(value: RuleConfig): ParsedRuleConfig;
|
|
|
75
76
|
* Default configuration with all rules enabled at strict preset severities
|
|
76
77
|
*/
|
|
77
78
|
export declare const DEFAULT_CONFIG: LintConfig;
|
|
78
|
-
/**
|
|
79
|
-
* Walk up from `startDir` to the nearest ancestor holding a chant project
|
|
80
|
-
* config (`chant.config.ts` or `chant.config.json`). Returns that directory, or
|
|
81
|
-
* `startDir` unchanged when none is found before the filesystem root.
|
|
82
|
-
*
|
|
83
|
-
* Linting a subpath (`chant graph src --format ir`, `chant lint src/lib`) must
|
|
84
|
-
* still see the project-root config: its `lint.overrides` globs are written
|
|
85
|
-
* project-root-relative (`src/lib/**`), and a rule set scoped only to the lint
|
|
86
|
-
* arg would silently drop them. Config discovery therefore anchors on the
|
|
87
|
-
* project root, not the path being linted.
|
|
88
|
-
*/
|
|
89
|
-
export declare function findProjectRoot(startDir: string): string;
|
|
90
79
|
/**
|
|
91
80
|
* Load lint configuration from a directory.
|
|
92
81
|
*
|
|
@@ -94,6 +83,12 @@ export declare function findProjectRoot(startDir: string): string;
|
|
|
94
83
|
* then falls back to `chant.config.json` (legacy LintConfig format).
|
|
95
84
|
* Returns default configuration if neither exists.
|
|
96
85
|
*
|
|
86
|
+
* chant #1113 — the `chant.config.ts` branch executes project-authored code,
|
|
87
|
+
* so it goes through `../config-sandbox.ts` like every other config load
|
|
88
|
+
* rather than `require`-ing the file itself. Unarmed (which is every `chant
|
|
89
|
+
* lint` invocation today — `lint` has no `--sandbox` flag) that is the
|
|
90
|
+
* identical `createRequire` path this used before, moved one module over.
|
|
91
|
+
*
|
|
97
92
|
* @param dir - Directory path to search for config file
|
|
98
93
|
* @returns Loaded and merged configuration, or default config if not found
|
|
99
94
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lint/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lint/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AASnD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAiBlD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAS3B,CAAC;AAkGH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,wCAAwC;IACxC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACnC,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,gDAAgD;IAChD,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,gBAAgB,CAsBnE;AAED;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,UAG5B,CAAC;AA0HF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAkClD;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAkBpG"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared upward config-discovery walk (chant #1117).
|
|
3
|
+
*
|
|
4
|
+
* Before this, `chant build <subdir>` and `lint.policies`
|
|
5
|
+
* (`./lint/policy.ts`'s `evaluateProjectPolicies`) each searched only the
|
|
6
|
+
* build directory and its immediate parent for `chant.config.ts`/`.json`,
|
|
7
|
+
* while `chant lint`/`chant graph` (`./lint/config.ts`'s old, file-local
|
|
8
|
+
* `findProjectRoot`) already walked all the way up. A project with a deeper
|
|
9
|
+
* `src/<stack>` layout — `chant build src/<stack>` two or more levels below
|
|
10
|
+
* the project root — silently never found the root config: `buildParams`'
|
|
11
|
+
* declared `env:` mappings went inert, `ownership`/`lint.policies`/etc quietly
|
|
12
|
+
* fell back to defaults, and nothing warned (loomster#162: `LOOM_TIER`/
|
|
13
|
+
* `LOOM_ENV` inert under every `npm run synth:*` for two releases).
|
|
14
|
+
*
|
|
15
|
+
* `findProjectConfig` is the one walk every config-discovery call site now
|
|
16
|
+
* shares. It stops at the first of:
|
|
17
|
+
*
|
|
18
|
+
* 1. A directory holding `chant.config.ts` or `chant.config.json` — found.
|
|
19
|
+
* 2. A directory holding `.git` or `package.json` with no chant config of its
|
|
20
|
+
* own — the project boundary. Discovery must never wander past the actual
|
|
21
|
+
* project into an unrelated ancestor directory just because this project
|
|
22
|
+
* happens not to declare a config (a stray `chant.config.ts` two levels
|
|
23
|
+
* above an unrelated git repo must never be picked up).
|
|
24
|
+
* 3. `startDir` itself, unchanged, if the walk reaches the filesystem root
|
|
25
|
+
* without ever finding a config OR a boundary marker. This is not just a
|
|
26
|
+
* "give up gracefully" nicety — several callers (`resolveProjectLexicons`
|
|
27
|
+
* -> `findInfraFiles`) scope a real directory walk off this function's
|
|
28
|
+
* result; if a rootless/marker-less start dir (a bare tmpdir, as chant's
|
|
29
|
+
* own test suites use) resolved all the way to `/`, that downstream walk
|
|
30
|
+
* would scan the entire filesystem instead of failing fast. Falling back
|
|
31
|
+
* to `startDir` keeps every caller's blast radius local no matter how far
|
|
32
|
+
* up the walk had to look.
|
|
33
|
+
*/
|
|
34
|
+
export interface ProjectConfigSearch {
|
|
35
|
+
/** The resolved project root: the config's directory, the boundary directory, or the (resolved) `startDir` when neither was found. */
|
|
36
|
+
dir: string;
|
|
37
|
+
/** Absolute path to the discovered `chant.config.ts`/`.json`, if any. */
|
|
38
|
+
configPath?: string;
|
|
39
|
+
}
|
|
40
|
+
/** Walk up from `startDir` (inclusive) to the nearest chant config or project boundary. See {@link ProjectConfigSearch}. */
|
|
41
|
+
export declare function findProjectConfig(startDir: string): ProjectConfigSearch;
|
|
42
|
+
/**
|
|
43
|
+
* Walk up from `startDir` to the nearest ancestor holding a chant project
|
|
44
|
+
* config (`chant.config.ts` or `chant.config.json`), the `.git`/`package.json`
|
|
45
|
+
* project boundary, or `startDir` itself when neither is found. Thin wrapper
|
|
46
|
+
* over {@link findProjectConfig} for callers that only need the directory
|
|
47
|
+
* (e.g. `chant lint`'s rule/plugin resolution, which just needs *a* stable
|
|
48
|
+
* root to resolve relative paths against).
|
|
49
|
+
*/
|
|
50
|
+
export declare function findProjectRoot(startDir: string): string;
|
|
51
|
+
//# sourceMappingURL=project-root.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-root.d.ts","sourceRoot":"","sources":["../src/project-root.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,WAAW,mBAAmB;IAClC,sIAAsI;IACtI,GAAG,EAAE,MAAM,CAAC;IACZ,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,4HAA4H;AAC5H,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAqBvE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAExD"}
|
package/package.json
CHANGED
package/src/build.test.ts
CHANGED
|
@@ -467,6 +467,25 @@ describe("detectCrossLexiconRefs", () => {
|
|
|
467
467
|
expect(collected[0].getOutputValue()).toEqual({ "Fn::Sub": "http://example.com/path" });
|
|
468
468
|
});
|
|
469
469
|
|
|
470
|
+
// chant #1121 — `output(<already-resolved value>, name)`, exactly the
|
|
471
|
+
// shape of a top-level `export const oParamName = output(data.Name,
|
|
472
|
+
// "oParamName")`, must reach the serializer as a plain `Value`, never a
|
|
473
|
+
// `Fn::GetAtt` pointing at the output's own logical id.
|
|
474
|
+
test("literal-valued output() emits its value verbatim, not a self-referencing Fn::GetAtt", () => {
|
|
475
|
+
const literalOutput = output("fold-output-repro", "oParamName");
|
|
476
|
+
|
|
477
|
+
const entities = new Map<string, Declarable>([
|
|
478
|
+
["oParamName", literalOutput as unknown as Declarable],
|
|
479
|
+
]);
|
|
480
|
+
|
|
481
|
+
const collected = collectLexiconOutputs(entities);
|
|
482
|
+
expect(collected).toHaveLength(1);
|
|
483
|
+
expect(collected[0].outputName).toBe("oParamName");
|
|
484
|
+
expect(collected[0].sourceEntity).toBe("");
|
|
485
|
+
expect(collected[0].sourceAttribute).toBeNull();
|
|
486
|
+
expect(collected[0].getOutputValue()).toBe("fold-output-repro");
|
|
487
|
+
});
|
|
488
|
+
|
|
470
489
|
test("deduplicates when same cross-lexicon ref appears in multiple entities", () => {
|
|
471
490
|
const alphaBucket = {
|
|
472
491
|
lexicon: "alpha",
|
package/src/build.ts
CHANGED
|
@@ -296,18 +296,28 @@ export function collectLexiconOutputs(
|
|
|
296
296
|
for (const [name, entity] of entities) {
|
|
297
297
|
if (isLexiconOutput(entity as unknown)) {
|
|
298
298
|
const lexiconOutput = entity as unknown as LexiconOutput;
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
299
|
+
// A literal-valued output (chant #1121) has no source entity at all —
|
|
300
|
+
// it isn't a reference to anything, so it must not fall back to the
|
|
301
|
+
// output's OWN map key the way an AttrRef whose parent didn't resolve
|
|
302
|
+
// would. Leaving `sourceEntity` empty keeps `getOutputValue()`'s
|
|
303
|
+
// (never-reached, for a literal) `Fn::GetAtt` fallback from ever being
|
|
304
|
+
// handed a bogus source, and keeps consumers that read `sourceEntity`
|
|
305
|
+
// directly (e.g. `graph-ir.ts`'s cross-stack export, the build
|
|
306
|
+
// manifest) from reporting the output as its own source.
|
|
307
|
+
if (lexiconOutput._literalValue === null) {
|
|
308
|
+
// Resolve source entity name from the WeakRef parent identity
|
|
309
|
+
const parent = lexiconOutput._sourceParent?.deref();
|
|
310
|
+
let sourceName = name;
|
|
311
|
+
if (parent) {
|
|
312
|
+
for (const [entityName, e] of entities) {
|
|
313
|
+
if (e === parent) {
|
|
314
|
+
sourceName = entityName;
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
307
317
|
}
|
|
308
318
|
}
|
|
319
|
+
lexiconOutput._setSourceEntity(sourceName);
|
|
309
320
|
}
|
|
310
|
-
lexiconOutput._setSourceEntity(sourceName);
|
|
311
321
|
outputs.push(lexiconOutput);
|
|
312
322
|
continue;
|
|
313
323
|
}
|
|
@@ -317,7 +327,8 @@ export function collectLexiconOutputs(
|
|
|
317
327
|
const prevLength = outputs.length;
|
|
318
328
|
walk(entity.props);
|
|
319
329
|
for (let i = prevLength; i < outputs.length; i++) {
|
|
320
|
-
|
|
330
|
+
// Same #1121 guard as above — a literal has no source entity to name.
|
|
331
|
+
if (!outputs[i].sourceEntity && outputs[i]._literalValue === null) {
|
|
321
332
|
outputs[i]._setSourceEntity(name);
|
|
322
333
|
}
|
|
323
334
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { build } from "../../build";
|
|
2
|
-
import {
|
|
2
|
+
import { loadChantConfigUpward, resolveOwnershipMarker, resolveFoldEnabled, resolveSandboxEnabled } from "../../config";
|
|
3
3
|
import { resolveCliBuildParams } from "../build-params-cli";
|
|
4
4
|
import type { Serializer, SerializerResult } from "../../serializer";
|
|
5
5
|
import type { LexiconPlugin } from "../../lexicon";
|
|
@@ -128,11 +128,14 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
128
128
|
// Resolve the path
|
|
129
129
|
const infraPath = resolve(options.path);
|
|
130
130
|
|
|
131
|
-
// Resolve opt-in ownership marking from project config
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
// Resolve opt-in ownership marking from project config. chant #1117 — walks
|
|
132
|
+
// up from the infra dir to the project root (`loadChantConfigUpward`), not
|
|
133
|
+
// just the infra dir's immediate parent: a project whose stacks live two or
|
|
134
|
+
// more levels below `chant.config.ts` (loomster's `src/<stack>` layout)
|
|
135
|
+
// otherwise never finds the root config at all, and every declared
|
|
136
|
+
// `buildParams`/`ownership`/`lint.policies` setting silently falls back to
|
|
137
|
+
// its default.
|
|
138
|
+
const loaded = await loadChantConfigUpward(infraPath);
|
|
136
139
|
const config = loaded.config;
|
|
137
140
|
const ownership = resolveOwnershipMarker(config);
|
|
138
141
|
|
|
@@ -155,6 +158,19 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
155
158
|
// as fold, resolved independently.
|
|
156
159
|
const sandbox = resolveSandboxEnabled(config, options.sandbox);
|
|
157
160
|
|
|
161
|
+
// #1113 — the bootstrap limit, surfaced rather than left implicit. Reading
|
|
162
|
+
// `build.sandbox` out of `chant.config.ts` requires evaluating that file, so
|
|
163
|
+
// a config-only opt-in cannot have covered its own evaluation; only the CLI
|
|
164
|
+
// flag, known before any config is touched, arms `../config-sandbox.ts`.
|
|
165
|
+
// Say so instead of letting two different boundaries share one word.
|
|
166
|
+
if (sandbox && !options.sandbox && loaded.configPath?.endsWith(".ts")) {
|
|
167
|
+
warnings.push(
|
|
168
|
+
formatWarning({
|
|
169
|
+
message: `build.sandbox is enabled by ${loaded.configPath}, but that file was itself evaluated in this process — reading the setting requires running the config. Pass --sandbox on the command line to evaluate chant.config.ts inside the boundary too.`,
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
158
174
|
// #1064 (factored into ../build-params-cli.ts's resolveCliBuildParams by
|
|
159
175
|
// #1108, so the component deploy driver runs the identical sequence) —
|
|
160
176
|
// resolve declared build-time parameters (chant.config.ts's buildParams)
|
|
@@ -174,6 +190,26 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
174
190
|
return { success: false, resourceCount: 0, fileCount: 0, errors, warnings };
|
|
175
191
|
}
|
|
176
192
|
|
|
193
|
+
// chant #1117 — a project that declares buildParams but resolves NONE of
|
|
194
|
+
// them for this build is the exact shape that let loomster#162 live for two
|
|
195
|
+
// releases: the discovered config wasn't the one the project author
|
|
196
|
+
// expected (a stale --path, a workspace boundary that stopped the walk
|
|
197
|
+
// short), or every declared parameter's `env:` var went unset, and either
|
|
198
|
+
// way every `params.<name>` read silently falls back to `undefined` — with
|
|
199
|
+
// no error (an all-`required: false` declaration resolves successfully to
|
|
200
|
+
// an empty set). Warn, naming the config path this build actually
|
|
201
|
+
// discovered, so a mismatch is visible instead of silent.
|
|
202
|
+
if (
|
|
203
|
+
Object.keys(config.buildParams ?? {}).length > 0 &&
|
|
204
|
+
paramsResolution.provenance.length === 0
|
|
205
|
+
) {
|
|
206
|
+
warnings.push(
|
|
207
|
+
formatWarning({
|
|
208
|
+
message: `chant.config.ts declares buildParams${loaded.configPath ? ` (${loaded.configPath})` : ""}, but none resolved for this build — every params.<name> read will be undefined. Pass --param/--params-file, or check that this is the config you expect.`,
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
177
213
|
// #1039 — thread each loaded plugin's registered intrinsics (e.g. AWS's
|
|
178
214
|
// `Sub`) through to the fold path, so a file using a registered intrinsic
|
|
179
215
|
// tagged template folds instead of unconditionally falling back to run.
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
* run --components` (see ../handlers/run.test.ts's "build-time parameters"
|
|
9
9
|
* describe block for the equivalent local/`--temporal` coverage).
|
|
10
10
|
*
|
|
11
|
-
* Mocks `generateComponentsPipeline` and `
|
|
12
|
-
*
|
|
11
|
+
* Mocks `generateComponentsPipeline` and `loadChantConfigUpward` (chant
|
|
12
|
+
* #1117 — `runGenerateComponents` walks up to the project root now, same as
|
|
13
|
+
* `chant build` proper, instead of reading `args.path` alone) and exercises
|
|
14
|
+
* the public `runBuild` dispatcher (`runGenerateComponents` itself isn't
|
|
13
15
|
* exported), mirroring `run.test.ts`'s style of driving the handler through
|
|
14
16
|
* its `CommandContext` entrypoint rather than reaching into private helpers.
|
|
15
17
|
*/
|
|
@@ -17,14 +19,14 @@ import { describe, test, expect, vi, beforeEach } from "vitest";
|
|
|
17
19
|
import type { ParsedArgs } from "../registry";
|
|
18
20
|
|
|
19
21
|
const generateComponentsPipelineMock = vi.fn();
|
|
20
|
-
const
|
|
22
|
+
const loadChantConfigUpwardMock = vi.fn();
|
|
21
23
|
|
|
22
24
|
vi.mock("../../components/cli-support", () => ({
|
|
23
25
|
generateComponentsPipeline: (...args: unknown[]) => generateComponentsPipelineMock(...args),
|
|
24
26
|
}));
|
|
25
27
|
vi.mock("../../config", async () => {
|
|
26
28
|
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
27
|
-
return { ...actual,
|
|
29
|
+
return { ...actual, loadChantConfigUpward: (...args: unknown[]) => loadChantConfigUpwardMock(...args) };
|
|
28
30
|
});
|
|
29
31
|
|
|
30
32
|
const { runBuild } = await import("./build");
|
|
@@ -54,7 +56,7 @@ function makeStderrSpy() {
|
|
|
54
56
|
describe("runBuild --components --generate (chant #1108 build-time parameters)", () => {
|
|
55
57
|
beforeEach(() => {
|
|
56
58
|
generateComponentsPipelineMock.mockReset();
|
|
57
|
-
|
|
59
|
+
loadChantConfigUpwardMock.mockReset().mockResolvedValue({ config: {} });
|
|
58
60
|
});
|
|
59
61
|
|
|
60
62
|
test("no declared buildParams → generateComponentsPipeline is called with an empty provenance array", async () => {
|
|
@@ -69,7 +71,7 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
|
|
|
69
71
|
});
|
|
70
72
|
|
|
71
73
|
test("chant.config.ts's declared buildParams resolve, log, and are forwarded to generateComponentsPipeline", async () => {
|
|
72
|
-
|
|
74
|
+
loadChantConfigUpwardMock.mockResolvedValue({
|
|
73
75
|
config: { buildParams: { tier: { type: "string", default: "light" } } },
|
|
74
76
|
});
|
|
75
77
|
generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "stages: []", stages: [], jobs: [] });
|
|
@@ -91,7 +93,7 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
|
|
|
91
93
|
});
|
|
92
94
|
|
|
93
95
|
test("--param overrides a declared default", async () => {
|
|
94
|
-
|
|
96
|
+
loadChantConfigUpwardMock.mockResolvedValue({
|
|
95
97
|
config: { buildParams: { tier: { type: "string", default: "light" } } },
|
|
96
98
|
});
|
|
97
99
|
generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "stages: []", stages: [], jobs: [] });
|
|
@@ -115,7 +117,7 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
|
|
|
115
117
|
});
|
|
116
118
|
|
|
117
119
|
test("an unresolved required build-time parameter → exit 1, never reaches generateComponentsPipeline", async () => {
|
|
118
|
-
|
|
120
|
+
loadChantConfigUpwardMock.mockResolvedValue({
|
|
119
121
|
config: { buildParams: { tier: { type: "string" } } },
|
|
120
122
|
});
|
|
121
123
|
const stderr = makeStderrSpy();
|
|
@@ -128,7 +130,7 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
|
|
|
128
130
|
});
|
|
129
131
|
|
|
130
132
|
test("an enum violation on --param → exit 1, never reaches generateComponentsPipeline", async () => {
|
|
131
|
-
|
|
133
|
+
loadChantConfigUpwardMock.mockResolvedValue({
|
|
132
134
|
config: { buildParams: { tier: { type: "string", enum: ["light", "production"] } } },
|
|
133
135
|
});
|
|
134
136
|
const stderr = makeStderrSpy();
|
|
@@ -4,7 +4,7 @@ import { buildCommand, buildCommandWatch, printErrors, printWarnings, resolveBui
|
|
|
4
4
|
import { formatError, formatInfo, formatSuccess, formatBold } from "../format";
|
|
5
5
|
import type { CommandContext } from "../registry";
|
|
6
6
|
import { generateComponentsPipeline } from "../../components/cli-support";
|
|
7
|
-
import {
|
|
7
|
+
import { loadChantConfigUpward, type ChantConfig } from "../../config";
|
|
8
8
|
import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -23,12 +23,17 @@ import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
|
23
23
|
* Before this, `params.*` (`@intentius/chant/params`) was always `{}` under
|
|
24
24
|
* this command too — generate mode shares `discoverComponents` with `chant
|
|
25
25
|
* run --components`, so it had the identical gap.
|
|
26
|
+
*
|
|
27
|
+
* chant #1117 — loads config by walking up from `args.path` to the project
|
|
28
|
+
* root (`loadChantConfigUpward`), same as `chant build` proper, instead of
|
|
29
|
+
* `args.path` alone: a components-only project built from a subdirectory
|
|
30
|
+
* otherwise never sees the root `chant.config.ts`'s `buildParams` either.
|
|
26
31
|
*/
|
|
27
32
|
async function runGenerateComponents(ctx: CommandContext): Promise<number> {
|
|
28
33
|
const { args } = ctx;
|
|
29
34
|
const lexicon = args.generate as string;
|
|
30
35
|
|
|
31
|
-
const { config } = await
|
|
36
|
+
const { config } = await loadChantConfigUpward(resolve(args.path)).catch(() => ({ config: {} as ChantConfig }));
|
|
32
37
|
const paramsResolution = resolveCliBuildParams(config.buildParams, {
|
|
33
38
|
cli: parseParamFlags(args.param),
|
|
34
39
|
paramsFile: args.paramsFile,
|
|
@@ -882,6 +882,37 @@ describe("runOp dispatcher: --components routes to runOpComponents", () => {
|
|
|
882
882
|
expect(discoverOpsMock).not.toHaveBeenCalled();
|
|
883
883
|
vi.restoreAllMocks();
|
|
884
884
|
});
|
|
885
|
+
|
|
886
|
+
// chant #1116 — --report is Op/Temporal-only (reads a past workflow run);
|
|
887
|
+
// the component driver never checked it, so it was silently ignored and the
|
|
888
|
+
// command fell through to a real dispatch. Hard-error instead, before
|
|
889
|
+
// runComponents is ever reached.
|
|
890
|
+
test("--report combined with --components → exit 1 before any dispatch, no fall-through (#1116)", async () => {
|
|
891
|
+
discoverOpsMock.mockReset();
|
|
892
|
+
const stderr = makeStderrSpy();
|
|
893
|
+
|
|
894
|
+
const exit = await runOp({ args: makeArgs({ path: "svc", components: true, report: true, temporal: false }), plugins: [], serializers: [] });
|
|
895
|
+
|
|
896
|
+
expect(exit).toBe(1);
|
|
897
|
+
expect(stderr.join("\n")).toContain("not supported with --components");
|
|
898
|
+
expect(stderr.join("\n")).toContain("#1116");
|
|
899
|
+
expect(discoverOpsMock).not.toHaveBeenCalled();
|
|
900
|
+
expect(runComponentsMock).not.toHaveBeenCalled();
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
// Plain --components (no --report) must be unaffected: it still reaches a
|
|
904
|
+
// real dispatch through runComponents — mocked here, never a real cloud call.
|
|
905
|
+
test("plain --components (no --report) still dispatches to runComponents (#1116 regression guard)", async () => {
|
|
906
|
+
discoverOpsMock.mockReset();
|
|
907
|
+
runComponentsMock.mockResolvedValue({ success: true, selected: ["svc"], run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true } });
|
|
908
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
909
|
+
|
|
910
|
+
const exit = await runOp({ args: makeArgs({ path: "svc", components: true, report: false, temporal: false }), plugins: [], serializers: [] });
|
|
911
|
+
|
|
912
|
+
expect(exit).toBe(0);
|
|
913
|
+
expect(runComponentsMock).toHaveBeenCalled();
|
|
914
|
+
vi.restoreAllMocks();
|
|
915
|
+
});
|
|
885
916
|
});
|
|
886
917
|
|
|
887
918
|
describe("runOpComponents", () => {
|
package/src/cli/handlers/run.ts
CHANGED
|
@@ -548,8 +548,23 @@ function renderProgress(opName: string, history: WorkflowHistoryRaw): void {
|
|
|
548
548
|
* (`../../components/driver.ts`) rather than a `*.op.ts` Op. Checked first,
|
|
549
549
|
* mirroring `runGraph`'s `if (ctx.args.components) return
|
|
550
550
|
* runComponentGraph(ctx)` branch (../handlers/graph.ts).
|
|
551
|
+
*
|
|
552
|
+
* chant #1116 — `--report` with `--components` is checked and hard-errored
|
|
553
|
+
* before that dispatch. There is no preview/dry-run mode for the component
|
|
554
|
+
* driver: unlike the Op path (where `--report` reads a past Temporal run),
|
|
555
|
+
* `runOpComponents` has never read `ctx.args.report` at all, so the flag was
|
|
556
|
+
* silently ignored and the command fell through to a real dispatch — observed
|
|
557
|
+
* live reaching an actual cloud shell-out. Erroring here is the safe minimum
|
|
558
|
+
* called out on the issue; a real preview is future work.
|
|
551
559
|
*/
|
|
552
560
|
export async function runOp(ctx: CommandContext): Promise<number> {
|
|
561
|
+
if (ctx.args.components && ctx.args.report) {
|
|
562
|
+
console.error(formatError({
|
|
563
|
+
message: "--report is not supported with --components",
|
|
564
|
+
hint: "No preview/dry-run mode exists yet for the component driver (see chant#1116). Omit --report.",
|
|
565
|
+
}));
|
|
566
|
+
return 1;
|
|
567
|
+
}
|
|
553
568
|
if (ctx.args.components) return runOpComponents(ctx);
|
|
554
569
|
if (ctx.args.local && ctx.args.temporal) {
|
|
555
570
|
console.error(formatError({
|
package/src/cli/main.test.ts
CHANGED
|
@@ -270,6 +270,29 @@ describe("parseArgs", () => {
|
|
|
270
270
|
const result = parseArgs(["build", "src", "--params-file", "./params.json"]);
|
|
271
271
|
expect(result.paramsFile).toBe("./params.json");
|
|
272
272
|
});
|
|
273
|
+
|
|
274
|
+
// ── --param=name=value hard error (chant #1118) ──────────────────────────
|
|
275
|
+
// The joined `--flag=value` form is not supported anywhere in this parser
|
|
276
|
+
// (see "ignores unknown flags" above) — a dropped --param can silently
|
|
277
|
+
// change what a build measures/deploys, so this form is rejected loudly
|
|
278
|
+
// instead of silently accepted as a no-op.
|
|
279
|
+
|
|
280
|
+
test("--param=name=value throws instead of silently dropping", () => {
|
|
281
|
+
expect(() => parseArgs(["build", "src", "--param=tier=production"])).toThrow(/--param=tier=production/);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("--param=name=value error names the working form", () => {
|
|
285
|
+
expect(() => parseArgs(["build", "src", "--param=tier=production"])).toThrow(/--param name=value/);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("--param= (empty value) also throws", () => {
|
|
289
|
+
expect(() => parseArgs(["build", "src", "--param="])).toThrow(/--param=/);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("plain --param name=value is unaffected", () => {
|
|
293
|
+
const result = parseArgs(["build", "src", "--param", "tier=production"]);
|
|
294
|
+
expect(result.param).toEqual(["tier=production"]);
|
|
295
|
+
});
|
|
273
296
|
});
|
|
274
297
|
|
|
275
298
|
// ── resolveCommand tests ──────────────────────────────────────────
|
package/src/cli/main.ts
CHANGED
|
@@ -5,7 +5,8 @@ import { isEntryPoint } from "./is-entry-point";
|
|
|
5
5
|
import { formatSuccess, formatError } from "./format";
|
|
6
6
|
import { loadPlugins, resolveProjectLexicons } from "./plugins";
|
|
7
7
|
import { resolveCommand, type CommandDef, type ParsedArgs } from "./registry";
|
|
8
|
-
import {
|
|
8
|
+
import { loadChantConfigUpward } from "../config";
|
|
9
|
+
import { armSandboxConfigEvaluation } from "../config-sandbox";
|
|
9
10
|
import { ENV_VAR, unknownEnvError } from "../env";
|
|
10
11
|
import { initRuntime } from "../runtime-adapter";
|
|
11
12
|
import { runBuild } from "./handlers/build";
|
|
@@ -224,6 +225,16 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
224
225
|
result.sandbox = true;
|
|
225
226
|
} else if (arg === "--param") {
|
|
226
227
|
(result.param ??= []).push(args[++i]);
|
|
228
|
+
} else if (arg.startsWith("--param=")) {
|
|
229
|
+
// chant #1118 — this parser never supports an `--flag=value` joined
|
|
230
|
+
// form for any value-taking flag (every branch above is an exact `===`
|
|
231
|
+
// match, so a joined token falls through unrecognized and is silently
|
|
232
|
+
// dropped — see the "ignores unknown flags" case below). `--param
|
|
233
|
+
// name=value` (space-separated) is the only accepted form. Rather than
|
|
234
|
+
// teach the parser joined forms generally, `--param=name=value` is
|
|
235
|
+
// called out as a hard error instead of a silent no-op: a dropped
|
|
236
|
+
// `--param` can silently change what a build measures/deploys.
|
|
237
|
+
throw new Error(`${arg} is not supported. Use --param name=value (space-separated) instead.`);
|
|
227
238
|
} else if (arg === "--params-file") {
|
|
228
239
|
result.paramsFile = args[++i];
|
|
229
240
|
} else if (!arg.startsWith("-")) {
|
|
@@ -588,11 +599,24 @@ async function main(): Promise<void> {
|
|
|
588
599
|
// chant.config itself may branch on the env. (#505)
|
|
589
600
|
if (args.env) process.env[ENV_VAR] = args.env;
|
|
590
601
|
|
|
591
|
-
//
|
|
602
|
+
// chant #1113 — `--sandbox` is a property of the whole invocation, and it
|
|
603
|
+
// has to be known BEFORE the first config load, because `chant.config.ts` is
|
|
604
|
+
// itself project-authored code. Arming here, straight off the parsed flag,
|
|
605
|
+
// is the only ordering that works: the project's own `build.sandbox: true`
|
|
606
|
+
// cannot cover its own evaluation (reading it means running it), so the
|
|
607
|
+
// command-line flag is what puts the config inside the boundary. See
|
|
608
|
+
// `../config-sandbox.ts`.
|
|
609
|
+
if (args.sandbox) armSandboxConfigEvaluation();
|
|
610
|
+
|
|
611
|
+
// Initialize runtime adapter early — before plugins or commands run.
|
|
612
|
+
// chant #1117 — walks up from `args.path` to the project root: for a
|
|
613
|
+
// subdirectory build/command (`chant build src/<stack> --env prod`) the
|
|
614
|
+
// declared `environments` almost always live in the root `chant.config.ts`,
|
|
615
|
+
// not `args.path` itself.
|
|
592
616
|
const projectPath0 = resolve(args.path === "." ? "." : args.path);
|
|
593
617
|
let loadedConfig;
|
|
594
618
|
try {
|
|
595
|
-
loadedConfig = await
|
|
619
|
+
loadedConfig = await loadChantConfigUpward(projectPath0);
|
|
596
620
|
initRuntime();
|
|
597
621
|
} catch {
|
|
598
622
|
// Config may not exist yet (e.g. `chant init`)
|
package/src/cli/plugins.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isLexiconPlugin, type LexiconPlugin } from "../lexicon";
|
|
2
|
-
import {
|
|
2
|
+
import { loadChantConfigUpward } from "../config";
|
|
3
3
|
import { findInfraFiles, detectLexicons } from "../index";
|
|
4
4
|
import { checkConflicts } from "./conflict-check";
|
|
5
5
|
|
|
@@ -96,9 +96,17 @@ export async function loadPlugins(lexiconNames: string[]): Promise<LexiconPlugin
|
|
|
96
96
|
* that never needed live module exports to begin with. This is strictly
|
|
97
97
|
* better than routing the detection through the sandbox: it removes the
|
|
98
98
|
* execution entirely rather than containing it, at no bundling/spawn cost.
|
|
99
|
+
*
|
|
100
|
+
* chant #1117 — walks up from `projectPath` to the project root
|
|
101
|
+
* (`loadChantConfigUpward`) rather than reading `projectPath` alone: `chant
|
|
102
|
+
* build src/<stack>` calls this with the stack's own subdirectory, and a
|
|
103
|
+
* project that declares `lexicons` only in its root `chant.config.ts` (never
|
|
104
|
+
* detectable by `detectLexicons()`'s import scan alone, e.g. a lexicon that's
|
|
105
|
+
* loaded but never imported by name in that particular stack's files) would
|
|
106
|
+
* otherwise silently miss it.
|
|
99
107
|
*/
|
|
100
108
|
export async function resolveProjectLexicons(projectPath: string): Promise<string[]> {
|
|
101
|
-
const { config } = await
|
|
109
|
+
const { config } = await loadChantConfigUpward(projectPath);
|
|
102
110
|
|
|
103
111
|
if (config.lexicons && config.lexicons.length > 0) {
|
|
104
112
|
return config.lexicons;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The single place chant evaluates a project's `chant.config.ts` **in the CLI's
|
|
6
|
+
* own process**.
|
|
7
|
+
*
|
|
8
|
+
* This is the config-side analogue of `./discovery/import.ts`'s `importModule`
|
|
9
|
+
* — one narrow module whose only job is "execute project-authored code here",
|
|
10
|
+
* so that the question "did any project code run in this process?" has one
|
|
11
|
+
* place to look and one place to instrument (see
|
|
12
|
+
* `examples/sandbox-execution-boundary.test.ts`, which spies on both).
|
|
13
|
+
*
|
|
14
|
+
* Callers must not `import()` a config file directly. `./config-sandbox.ts`
|
|
15
|
+
* decides whether a given load is allowed to come through here at all: under
|
|
16
|
+
* `chant build --sandbox` it routes the evaluation into the sandboxed child
|
|
17
|
+
* instead (chant #1113), and these functions are never reached.
|
|
18
|
+
*
|
|
19
|
+
* `chant.config.json` is pure data and is parsed, not executed — it never goes
|
|
20
|
+
* through this module.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** The shape a config module evaluates to, before `default`/`config`/namespace selection. */
|
|
24
|
+
export type ConfigModuleNamespace = Record<string, unknown>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Import a `chant.config.ts` into THIS process and return its module namespace.
|
|
28
|
+
* Node's ESM registry caches it, so repeated loads within one CLI invocation
|
|
29
|
+
* evaluate the file once — the behavior `loadChantConfig` has always had.
|
|
30
|
+
*/
|
|
31
|
+
export async function importConfigModule(configPath: string): Promise<ConfigModuleNamespace> {
|
|
32
|
+
return (await import(configPath)) as ConfigModuleNamespace;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* `require()` a `chant.config.ts` into THIS process from `dir`'s resolution
|
|
37
|
+
* context — the synchronous path `./lint/config.ts`'s `loadConfig` has used
|
|
38
|
+
* since before the async loader existed (`chant lint` is a sync pipeline).
|
|
39
|
+
*/
|
|
40
|
+
export function requireConfigModule(configPath: string, dir: string): ConfigModuleNamespace {
|
|
41
|
+
const req = createRequire(join(dir, "package.json"));
|
|
42
|
+
return req(configPath) as ConfigModuleNamespace;
|
|
43
|
+
}
|