@intentius/chant 0.19.1 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-params.d.ts +108 -0
- package/dist/build-params.d.ts.map +1 -0
- package/dist/build.d.ts +79 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +31 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +41 -0
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts +71 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon.d.ts +1 -1
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts +27 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/run-client.d.ts +1 -1
- package/dist/cli/handlers/run-client.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +16 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +10 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/codegen/docs-types.d.ts +2 -0
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +12 -5
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts +62 -7
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/sandbox/driver.d.ts +12 -0
- package/dist/components/sandbox/driver.d.ts.map +1 -0
- package/dist/components/sandbox/run.d.ts +42 -0
- package/dist/components/sandbox/run.d.ts.map +1 -0
- package/dist/composite.d.ts +5 -0
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +71 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/declarable.d.ts +16 -0
- package/dist/declarable.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/entity-wire-codec.d.ts +166 -0
- package/dist/discovery/entity-wire-codec.d.ts.map +1 -0
- package/dist/discovery/entity-wire.d.ts +50 -0
- package/dist/discovery/entity-wire.d.ts.map +1 -0
- package/dist/discovery/fold-import.d.ts +239 -0
- package/dist/discovery/fold-import.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +74 -1
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.d.ts +18 -0
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -0
- package/dist/discovery/sandbox/child-errors.d.ts +15 -0
- package/dist/discovery/sandbox/child-errors.d.ts.map +1 -0
- package/dist/discovery/sandbox/driver.d.ts +13 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -0
- package/dist/discovery/sandbox/run.d.ts +69 -0
- package/dist/discovery/sandbox/run.d.ts.map +1 -0
- package/dist/errors.d.ts +9 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +299 -0
- package/dist/fold/fold.d.ts.map +1 -0
- package/dist/fold/foldable-helpers.d.ts +121 -0
- package/dist/fold/foldable-helpers.d.ts.map +1 -0
- package/dist/fold/subset.d.ts +134 -0
- package/dist/fold/subset.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon-output.d.ts +7 -2
- package/dist/lexicon-output.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +4 -2
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +131 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts +11 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lint/component-checks.d.ts +7 -1
- package/dist/lint/component-checks.d.ts.map +1 -1
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/evl003-dynamic-property-access.d.ts.map +1 -1
- package/dist/params.d.ts +60 -0
- package/dist/params.d.ts.map +1 -0
- package/dist/provenance.d.ts +21 -0
- package/dist/provenance.d.ts.map +1 -1
- package/dist/runtime.d.ts +10 -1
- package/dist/runtime.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/build-params.test.ts +144 -0
- package/src/build-params.ts +207 -0
- package/src/build.test.ts +38 -0
- package/src/build.ts +144 -7
- package/src/cli/commands/build.test.ts +220 -2
- package/src/cli/commands/build.ts +111 -3
- package/src/cli/commands/check-lexicon-examples.test.ts +92 -0
- package/src/cli/commands/check-lexicon-examples.ts +103 -0
- package/src/cli/commands/check-lexicon-intrinsics.test.ts +179 -0
- package/src/cli/commands/check-lexicon-intrinsics.ts +348 -0
- package/src/cli/commands/check-lexicon.test.ts +34 -0
- package/src/cli/commands/check-lexicon.ts +137 -1
- package/src/cli/commands/lint.ts +31 -3
- package/src/cli/commands/onboard.ts +1 -1
- package/src/cli/component-security-boundary.test.ts +170 -0
- package/src/cli/handlers/build.ts +24 -3
- package/src/cli/handlers/components.ts +9 -2
- package/src/cli/handlers/dev.ts +1 -1
- package/src/cli/handlers/graph.ts +7 -5
- package/src/cli/handlers/lifecycle.ts +2 -2
- package/src/cli/handlers/lint.ts +2 -0
- package/src/cli/handlers/misc.ts +2 -2
- package/src/cli/handlers/run-client.ts +1 -1
- package/src/cli/handlers/run.ts +20 -5
- package/src/cli/main.test.ts +22 -0
- package/src/cli/main.ts +39 -0
- package/src/cli/plugins.ts +20 -4
- package/src/cli/registry.ts +10 -0
- package/src/cli/security-boundary.test.ts +135 -0
- package/src/codegen/docs-sections.test.ts +67 -0
- package/src/codegen/docs-sections.ts +7 -3
- package/src/codegen/docs-types.ts +2 -0
- package/src/components/cli-support.ts +22 -10
- package/src/components/discover.ts +127 -25
- package/src/components/sandbox/driver.ts +114 -0
- package/src/components/sandbox/run.test.ts +185 -0
- package/src/components/sandbox/run.ts +177 -0
- package/src/composite.test.ts +21 -0
- package/src/composite.ts +20 -1
- package/src/config.ts +81 -0
- package/src/declarable.ts +20 -0
- package/src/discovery/collect.ts +17 -3
- package/src/discovery/entity-wire-codec.ts +487 -0
- package/src/discovery/entity-wire.test.ts +240 -0
- package/src/discovery/entity-wire.ts +67 -0
- package/src/discovery/fold-import.test.ts +1598 -0
- package/src/discovery/fold-import.ts +1998 -0
- package/src/discovery/index.test.ts +191 -1
- package/src/discovery/index.ts +242 -1
- package/src/discovery/sandbox/bundle.ts +218 -0
- package/src/discovery/sandbox/child-errors.ts +65 -0
- package/src/discovery/sandbox/driver.ts +147 -0
- package/src/discovery/sandbox/run.test.ts +179 -0
- package/src/discovery/sandbox/run.ts +196 -0
- package/src/errors.ts +9 -1
- package/src/fold/fold.test.ts +812 -0
- package/src/fold/fold.ts +805 -0
- package/src/fold/foldable-helpers.ts +171 -0
- package/src/fold/subset-doc-parity.test.ts +210 -0
- package/src/fold/subset.test.ts +352 -0
- package/src/fold/subset.ts +383 -0
- package/src/index.ts +2 -0
- package/src/lexicon-output.ts +7 -2
- package/src/lexicon-schema.test.ts +57 -0
- package/src/lexicon-schema.ts +8 -1
- package/src/lexicon.ts +132 -1
- package/src/lifecycle/git.test.ts +10 -5
- package/src/lifecycle/release-ledger.test.ts +28 -0
- package/src/lifecycle/release-ledger.ts +11 -0
- package/src/lint/component-checks.ts +8 -1
- package/src/lint/rules/evl001-non-literal-expression.ts +26 -110
- package/src/lint/rules/evl003-dynamic-property-access.ts +11 -2
- package/src/params.test.ts +22 -0
- package/src/params.ts +66 -0
- package/src/provenance.ts +22 -0
- package/src/runtime.ts +11 -2
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { BuildParamProvenance } from "./provenance";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build-time parameter declaration + resolution (chant #1064, follow-up to
|
|
5
|
+
* epic #1019's fold work — see issue #1064's "DECISION: option 1" comment).
|
|
6
|
+
*
|
|
7
|
+
* A build-time parameter is declared in `chant.config.ts`'s `buildParams`
|
|
8
|
+
* (name, type, optional `default`/`enum`/`env` mapping), supplied to `chant
|
|
9
|
+
* build` (a `--param name=value` flag, a `--params-file` JSON file, or a
|
|
10
|
+
* declared `env` var), and referenced from source as `params.<name>` (see
|
|
11
|
+
* ./params.ts) — never as an ambient `process.env` read. This module owns
|
|
12
|
+
* declaration + precedence + validation; ./params.ts is the plain runtime
|
|
13
|
+
* object source references; ../discovery/fold-import.ts is what makes a
|
|
14
|
+
* `params.<name>` reference fold to a literal instead of a symbolic node.
|
|
15
|
+
*
|
|
16
|
+
* This is NOT the deploy-time `Parameter` class (`lexicons/aws/src/parameter.ts`)
|
|
17
|
+
* — that resolves when a CloudFormation stack deploys; this resolves before
|
|
18
|
+
* the template is even synthesized, so its value can change which resources
|
|
19
|
+
* are produced at all. See ./params.ts's module doc for the full distinction.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** A build-time parameter's resolved (and declared-default/enum) value — always a scalar. */
|
|
23
|
+
export type BuildParamValue = string | number | boolean;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* One declared build-time parameter (`chant.config.ts`'s `buildParams.<name>`).
|
|
27
|
+
*/
|
|
28
|
+
export interface BuildParamDef {
|
|
29
|
+
/** The value's declared type — supplied strings (CLI flags, env vars, JSON-file strings) are coerced to it. */
|
|
30
|
+
type: "string" | "number" | "boolean";
|
|
31
|
+
/** Value used when no `--param`/`--params-file`/declared `env` var supplies one. Omit to require an explicit value every build. */
|
|
32
|
+
default?: BuildParamValue;
|
|
33
|
+
/**
|
|
34
|
+
* Allowed values — a resolved value outside this list is a build error
|
|
35
|
+
* naming the parameter (never a thrown error from user source). Replaces
|
|
36
|
+
* the hand-written `if (!VALID.includes(raw)) throw ...` pattern loomster's
|
|
37
|
+
* `params.ts` files used before migrating to this mechanism.
|
|
38
|
+
*/
|
|
39
|
+
enum?: readonly BuildParamValue[];
|
|
40
|
+
/**
|
|
41
|
+
* Opt-in, EXPLICITLY declared environment-variable fallback — e.g. `env:
|
|
42
|
+
* "LOOM_TIER"`. This is the only place an env var may feed a build-time
|
|
43
|
+
* parameter: reading `process.env` directly from project source is never
|
|
44
|
+
* supported (see ./params.ts's module doc and ../fold/fold.ts's pointed
|
|
45
|
+
* error for a bare `process` reference). Consulted only when no
|
|
46
|
+
* `--param`/`--params-file` value was supplied for this parameter.
|
|
47
|
+
*/
|
|
48
|
+
env?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Set `false` to make an unresolved value NOT a build error: `params.<name>`
|
|
51
|
+
* is simply omitted (reads as plain JS `undefined` — a normal, un-erroring
|
|
52
|
+
* property access on an object missing that key), instead of the default
|
|
53
|
+
* behavior of requiring every declared parameter to resolve to something.
|
|
54
|
+
* For a value that is genuinely optional with no meaningful default (an ARN
|
|
55
|
+
* that references an existing resource only on some deploys, a CIDR
|
|
56
|
+
* override, a JSON blob) — the same "unset means the composite decides"
|
|
57
|
+
* shape a hand-written `process.env.X || undefined` used to express.
|
|
58
|
+
* Default `true` (a declared parameter must resolve to a value).
|
|
59
|
+
*/
|
|
60
|
+
required?: boolean;
|
|
61
|
+
/** Human-readable description, surfaced in error messages and docs generation. */
|
|
62
|
+
description?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A project's full set of declared build-time parameters, keyed by name. */
|
|
66
|
+
export type BuildParamsConfig = Record<string, BuildParamDef>;
|
|
67
|
+
|
|
68
|
+
/** Raw, not-yet-validated inputs {@link resolveBuildParams} resolves against a project's declared {@link BuildParamsConfig}. */
|
|
69
|
+
export interface BuildParamsInput {
|
|
70
|
+
/** `--param name=value` flags, repeated — highest precedence. */
|
|
71
|
+
cli?: Record<string, string>;
|
|
72
|
+
/** Parsed contents of a `--params-file` JSON file — second precedence. */
|
|
73
|
+
fromFile?: Record<string, unknown>;
|
|
74
|
+
/** The process environment, consulted only for a parameter that declares an `env` mapping, and only once `cli`/`fromFile` have no value for it. */
|
|
75
|
+
env?: Record<string, string | undefined>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Result of resolving a project's declared parameters against one build invocation's inputs. */
|
|
79
|
+
export interface BuildParamsResolution {
|
|
80
|
+
/** Every successfully resolved parameter — see {@link BuildParamProvenance}. Empty when the project declares none. */
|
|
81
|
+
provenance: BuildParamProvenance[];
|
|
82
|
+
/**
|
|
83
|
+
* Validation failures, each naming the offending parameter — an unknown
|
|
84
|
+
* `--param`/`--params-file` key, a missing required value, a type mismatch,
|
|
85
|
+
* or a value outside a declared `enum`. Reported by the CLI as a build
|
|
86
|
+
* error (chant #1064's acceptance criterion: "not a thrown error inside
|
|
87
|
+
* user source"); never thrown from here.
|
|
88
|
+
*/
|
|
89
|
+
errors: string[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function formatValue(value: unknown): string {
|
|
93
|
+
return typeof value === "string" ? `"${value}"` : String(value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Coerce `raw` to `def.type`, appending a located error and returning `undefined` on failure. */
|
|
97
|
+
function coerce(name: string, raw: BuildParamValue, def: BuildParamDef, errors: string[]): BuildParamValue | undefined {
|
|
98
|
+
if (def.type === "string") return String(raw);
|
|
99
|
+
|
|
100
|
+
if (def.type === "number") {
|
|
101
|
+
const n = typeof raw === "number" ? raw : Number(raw);
|
|
102
|
+
if (Number.isNaN(n)) {
|
|
103
|
+
errors.push(`build parameter "${name}" must be a number, got ${formatValue(raw)}`);
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return n;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// def.type === "boolean"
|
|
110
|
+
if (typeof raw === "boolean") return raw;
|
|
111
|
+
if (raw === "true") return true;
|
|
112
|
+
if (raw === "false") return false;
|
|
113
|
+
errors.push(`build parameter "${name}" must be a boolean ("true" or "false"), got ${formatValue(raw)}`);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolve a project's declared build-time parameters against one build's
|
|
119
|
+
* supplied inputs. Precedence per parameter, most to least specific:
|
|
120
|
+
* `cli` (`--param name=value`) > `fromFile` (`--params-file`) > the
|
|
121
|
+
* parameter's own declared `env` mapping (only if set) > `def.default`.
|
|
122
|
+
*
|
|
123
|
+
* A parameter with no declared `default` and no value from any source is a
|
|
124
|
+
* build error, not a silently-`undefined` value — a build-time parameter
|
|
125
|
+
* exists specifically so a project never has an invisible dependency on
|
|
126
|
+
* ambient state; leaving one unresolved would just reintroduce that under a
|
|
127
|
+
* different name. Every failure is collected (not thrown), each naming the
|
|
128
|
+
* offending parameter, so a single invocation reports every problem at once.
|
|
129
|
+
*
|
|
130
|
+
* The one opt-out is `def.required: false` — for a parameter that is
|
|
131
|
+
* genuinely optional with no meaningful default (an ARN that only applies to
|
|
132
|
+
* a reference-existing deploy, a CIDR override), an unresolved value is
|
|
133
|
+
* simply omitted from `provenance`/`params` rather than an error; source
|
|
134
|
+
* reads it as plain `undefined`, same as before migrating off
|
|
135
|
+
* `process.env.X || undefined`.
|
|
136
|
+
*/
|
|
137
|
+
export function resolveBuildParams(defs: BuildParamsConfig | undefined, input: BuildParamsInput): BuildParamsResolution {
|
|
138
|
+
const declared = defs ?? {};
|
|
139
|
+
const cli = input.cli ?? {};
|
|
140
|
+
const fromFile = input.fromFile ?? {};
|
|
141
|
+
const env = input.env ?? {};
|
|
142
|
+
const errors: string[] = [];
|
|
143
|
+
const provenance: BuildParamProvenance[] = [];
|
|
144
|
+
|
|
145
|
+
for (const key of Object.keys(cli)) {
|
|
146
|
+
if (!(key in declared)) {
|
|
147
|
+
errors.push(`unknown build parameter "${key}" (from --param) — not declared in chant.config.ts's buildParams`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const key of Object.keys(fromFile)) {
|
|
151
|
+
if (!(key in declared)) {
|
|
152
|
+
errors.push(`unknown build parameter "${key}" (from --params-file) — not declared in chant.config.ts's buildParams`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const [name, def] of Object.entries(declared)) {
|
|
157
|
+
let raw: BuildParamValue | undefined;
|
|
158
|
+
let source: BuildParamProvenance["source"] | undefined;
|
|
159
|
+
|
|
160
|
+
if (name in cli) {
|
|
161
|
+
raw = cli[name];
|
|
162
|
+
source = "cli";
|
|
163
|
+
} else if (name in fromFile) {
|
|
164
|
+
const fileValue = fromFile[name];
|
|
165
|
+
if (typeof fileValue !== "string" && typeof fileValue !== "number" && typeof fileValue !== "boolean") {
|
|
166
|
+
errors.push(`build parameter "${name}" (from --params-file) must be a string, number, or boolean`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
raw = fileValue;
|
|
170
|
+
source = "params-file";
|
|
171
|
+
} else if (def.env && env[def.env] !== undefined) {
|
|
172
|
+
raw = env[def.env];
|
|
173
|
+
source = "env";
|
|
174
|
+
} else if (def.default !== undefined) {
|
|
175
|
+
raw = def.default;
|
|
176
|
+
source = "default";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (raw === undefined || source === undefined) {
|
|
180
|
+
if (def.required === false) continue; // omitted entirely — params.<name> reads as undefined, not an error
|
|
181
|
+
const envHint = def.env ? `, set ${def.env}` : "";
|
|
182
|
+
errors.push(
|
|
183
|
+
`build parameter "${name}" has no value — pass --param ${name}=<value>, use --params-file${envHint}, or add a default in chant.config.ts's buildParams`,
|
|
184
|
+
);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const value = coerce(name, raw, def, errors);
|
|
189
|
+
if (value === undefined) continue;
|
|
190
|
+
|
|
191
|
+
if (def.enum && !def.enum.includes(value)) {
|
|
192
|
+
errors.push(
|
|
193
|
+
`build parameter "${name}" must be one of ${def.enum.map(formatValue).join(", ")}, got ${formatValue(value)}`,
|
|
194
|
+
);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
provenance.push({ name, value, source });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { provenance, errors };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Project the resolved provenance records down to a plain `{ name: value }` map — what actually gets bound to `params.<name>` (see ./params.ts). */
|
|
205
|
+
export function buildParamValues(provenance: readonly BuildParamProvenance[]): Record<string, BuildParamValue> {
|
|
206
|
+
return Object.fromEntries(provenance.map((p) => [p.name, p.value]));
|
|
207
|
+
}
|
package/src/build.test.ts
CHANGED
|
@@ -64,6 +64,44 @@ export const testEntity = {
|
|
|
64
64
|
expect(result.entities.size).toBe(1);
|
|
65
65
|
expect(result.entities.has("testEntity")).toBe(true);
|
|
66
66
|
expect(result.errors.length).toBe(0);
|
|
67
|
+
expect(result.foldDecisions).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("#1022 — --fold folds a leaf-only module and surfaces foldDecisions", async () => {
|
|
71
|
+
const { dirname, resolve: resolvePath } = await import("node:path");
|
|
72
|
+
const { fileURLToPath } = await import("node:url");
|
|
73
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
74
|
+
const runtimePath = resolvePath(thisDir, "runtime");
|
|
75
|
+
|
|
76
|
+
await writeFile(
|
|
77
|
+
join(testDir, "resources.ts"),
|
|
78
|
+
`
|
|
79
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
80
|
+
export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
81
|
+
`
|
|
82
|
+
);
|
|
83
|
+
await writeFile(
|
|
84
|
+
join(testDir, "main.ts"),
|
|
85
|
+
`
|
|
86
|
+
import { Bucket } from "./resources";
|
|
87
|
+
throw new Error("must never execute — sentinel for #1022 fold verification");
|
|
88
|
+
export const bucket = new Bucket({ name: "my-bucket" });
|
|
89
|
+
`
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const mockSerializer: Serializer = {
|
|
93
|
+
name: "aws",
|
|
94
|
+
rulePrefix: "TEST",
|
|
95
|
+
serialize: (entities) => JSON.stringify([...entities.keys()]),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const result = await build(testDir, [mockSerializer], undefined, { fold: true });
|
|
99
|
+
|
|
100
|
+
expect(result.errors).toEqual([]);
|
|
101
|
+
expect(result.entities.size).toBe(1);
|
|
102
|
+
expect(result.entities.has("bucket")).toBe(true);
|
|
103
|
+
const mainDecision = result.foldDecisions.find((d) => d.file.endsWith("main.ts"));
|
|
104
|
+
expect(mainDecision?.mode).toBe("fold");
|
|
67
105
|
});
|
|
68
106
|
|
|
69
107
|
test("handles discovery errors", async () => {
|
package/src/build.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { Declarable } from "./declarable";
|
|
2
2
|
import type { Serializer, SerializerResult } from "./serializer";
|
|
3
3
|
import type { OwnershipMarker } from "./ownership";
|
|
4
|
-
import type {
|
|
5
|
-
import {
|
|
4
|
+
import type { BuildError, DiscoveryErrorType } from "./errors";
|
|
5
|
+
import type { IntrinsicDef } from "./lexicon";
|
|
6
|
+
import type { BuildParamProvenance } from "./provenance";
|
|
7
|
+
import { DiscoveryError, BuildError as BuildErrorClass } from "./errors";
|
|
6
8
|
import { LexiconOutput, isLexiconOutput } from "./lexicon-output";
|
|
7
9
|
import { AttrRef } from "./attrref";
|
|
8
10
|
import { isChildProject, type ChildProjectInstance } from "./child-project";
|
|
9
|
-
import { discover } from "./discovery/index";
|
|
11
|
+
import { discover, type DiscoveryResult, type FoldDecision } from "./discovery/index";
|
|
12
|
+
import { decodeEntitySet, type DiscoveredEntitiesJson } from "./discovery/entity-wire";
|
|
13
|
+
import { buildDependencyGraph } from "./discovery/graph";
|
|
10
14
|
import { topologicalSort } from "./sort";
|
|
11
15
|
import { resolve } from "node:path";
|
|
12
16
|
|
|
@@ -149,6 +153,45 @@ export interface BuildOptions {
|
|
|
149
153
|
* {@link SerializeContext} so a dialect can read its lexicon-scoped settings.
|
|
150
154
|
*/
|
|
151
155
|
config?: Record<string, unknown>;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* chant #1022 (epic #1019) — opt-in: fold source modules statically
|
|
159
|
+
* instead of importing/running them, falling back to run per-file for
|
|
160
|
+
* anything the folder can't represent. Default `false` (unchanged
|
|
161
|
+
* behavior). See {@link DiscoveryOptions.fold} in `./discovery/index`.
|
|
162
|
+
*/
|
|
163
|
+
fold?: boolean;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* chant #1039 — lexicon-registered intrinsic tags (e.g. AWS's `Sub`) to
|
|
167
|
+
* recognize while folding. Passed straight through to
|
|
168
|
+
* {@link DiscoveryOptions.intrinsics}; ignored unless {@link fold} is set.
|
|
169
|
+
* The CLI populates this from `options.plugins.flatMap(p => p.intrinsics?.() ?? [])`.
|
|
170
|
+
*/
|
|
171
|
+
intrinsics?: IntrinsicDef[];
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* chant #1045 Phase 2 — opt-in: run-fallback files (or, when {@link fold}
|
|
175
|
+
* isn't set, every file) execute together, isolated, in one sandboxed
|
|
176
|
+
* child process instead of in-process. Passed straight through to
|
|
177
|
+
* {@link DiscoveryOptions.sandbox} in `./discovery/index`. Default `false`
|
|
178
|
+
* (unchanged behavior/performance).
|
|
179
|
+
*/
|
|
180
|
+
sandbox?: boolean;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* chant #1064 — this build's resolved build-time parameter values (see
|
|
184
|
+
* ./build-params.ts's `resolveBuildParams`, driven by the CLI's
|
|
185
|
+
* `--param`/`--params-file`/declared `env` mapping/`chant.config.ts`
|
|
186
|
+
* `buildParams` defaults). Threaded through to `discover()`, which
|
|
187
|
+
* populates `./params.ts`'s shared `params` object before any project file
|
|
188
|
+
* is imported or folded, and into the fold session so a `params.<name>`
|
|
189
|
+
* reference resolves to a literal. Passed through verbatim onto
|
|
190
|
+
* {@link BuildResult.buildParams} — `build()` itself does no
|
|
191
|
+
* declaration/validation (that's the CLI/config layer's job); it only
|
|
192
|
+
* carries the already-resolved records for provenance.
|
|
193
|
+
*/
|
|
194
|
+
buildParams?: BuildParamProvenance[];
|
|
152
195
|
}
|
|
153
196
|
|
|
154
197
|
export interface BuildResult {
|
|
@@ -166,6 +209,20 @@ export interface BuildResult {
|
|
|
166
209
|
manifest: BuildManifest;
|
|
167
210
|
/** Number of source files processed */
|
|
168
211
|
sourceFileCount: number;
|
|
212
|
+
/**
|
|
213
|
+
* Per-file fold-vs-run decisions (#1022). Empty unless
|
|
214
|
+
* {@link BuildOptions.fold} was set.
|
|
215
|
+
*/
|
|
216
|
+
foldDecisions: FoldDecision[];
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* This build's resolved build-time parameters (#1064) — the build
|
|
220
|
+
* provenance record for `params.<name>` values, alongside the existing
|
|
221
|
+
* entity-level provenance (./provenance.ts). Passed through verbatim from
|
|
222
|
+
* {@link BuildOptions.buildParams}; empty when the project declares/
|
|
223
|
+
* supplies none.
|
|
224
|
+
*/
|
|
225
|
+
buildParams: BuildParamProvenance[];
|
|
169
226
|
}
|
|
170
227
|
|
|
171
228
|
/**
|
|
@@ -451,13 +508,43 @@ export async function build(
|
|
|
451
508
|
serializers: Serializer[],
|
|
452
509
|
parentBuildStack?: Set<string>,
|
|
453
510
|
options?: BuildOptions,
|
|
511
|
+
): Promise<BuildResult> {
|
|
512
|
+
// Step 1: Discover entities and dependencies
|
|
513
|
+
const discoveryResult = await discover(path, {
|
|
514
|
+
fold: options?.fold,
|
|
515
|
+
intrinsics: options?.intrinsics,
|
|
516
|
+
sandbox: options?.sandbox,
|
|
517
|
+
buildParams: options?.buildParams,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
return buildFromDiscoveryResult(discoveryResult, path, serializers, parentBuildStack, options);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* chant #1045 (Phase 1) — build from a discovery result produced OUTSIDE the
|
|
525
|
+
* normal `discover(path)` call, i.e. decoded from {@link DiscoveredEntitiesJson}
|
|
526
|
+
* (see {@link buildFromEntitiesJson}). Everything from here on (topological
|
|
527
|
+
* sort, recursive child-project builds, partitioning, output detection,
|
|
528
|
+
* serialization, manifest) is exactly what `build()` already does after its
|
|
529
|
+
* own `discover()` call — extracted so the JSON path reuses it verbatim
|
|
530
|
+
* instead of forking it.
|
|
531
|
+
*
|
|
532
|
+
* @param resolvedPathForChildStack - Used only to seed the circular-nested-
|
|
533
|
+
* stack detection (`buildStack`); the JSON path has no single directory a
|
|
534
|
+
* decoded entity set came from, so callers without one may pass any stable
|
|
535
|
+
* label (child projects aren't supported by the JSON boundary yet — see
|
|
536
|
+
* `discovery/entity-wire.ts` — so this is inert for that path today).
|
|
537
|
+
*/
|
|
538
|
+
async function buildFromDiscoveryResult(
|
|
539
|
+
discoveryResult: DiscoveryResult,
|
|
540
|
+
resolvedPathForChildStack: string,
|
|
541
|
+
serializers: Serializer[],
|
|
542
|
+
parentBuildStack?: Set<string>,
|
|
543
|
+
options?: BuildOptions,
|
|
454
544
|
): Promise<BuildResult> {
|
|
455
545
|
const warnings: string[] = [];
|
|
456
546
|
const errors: Array<DiscoveryError | BuildError> = [];
|
|
457
547
|
|
|
458
|
-
// Step 1: Discover entities and dependencies
|
|
459
|
-
const discoveryResult = await discover(path);
|
|
460
|
-
|
|
461
548
|
// Collect discovery errors
|
|
462
549
|
errors.push(...discoveryResult.errors);
|
|
463
550
|
|
|
@@ -486,7 +573,7 @@ export async function build(
|
|
|
486
573
|
}
|
|
487
574
|
|
|
488
575
|
// Step 4: Recursively build child projects
|
|
489
|
-
const resolvedPath = resolve(
|
|
576
|
+
const resolvedPath = resolve(resolvedPathForChildStack);
|
|
490
577
|
const buildStack = parentBuildStack
|
|
491
578
|
? new Set(parentBuildStack)
|
|
492
579
|
: new Set<string>();
|
|
@@ -596,5 +683,55 @@ export async function build(
|
|
|
596
683
|
errors,
|
|
597
684
|
manifest,
|
|
598
685
|
sourceFileCount: discoveryResult.sourceFiles.length,
|
|
686
|
+
foldDecisions: discoveryResult.foldDecisions,
|
|
687
|
+
buildParams: options?.buildParams ?? [],
|
|
599
688
|
};
|
|
600
689
|
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* chant #1045 (Phase 1) — build directly from a JSON-encoded discovery
|
|
693
|
+
* result (see {@link discoverEntitySetJson} in `./discovery/entity-wire.ts`)
|
|
694
|
+
* instead of pointing `build()` at a directory.
|
|
695
|
+
*
|
|
696
|
+
* Decodes the wire entity set back into a live entities map — see
|
|
697
|
+
* `decodeEntitySet`'s doc for why the decoded entities are functionally
|
|
698
|
+
* indistinguishable from what `discover()` produces in-process (real
|
|
699
|
+
* `AttrRef` instances, whole-entity identity preserved by reference, not by
|
|
700
|
+
* clone) — then runs the exact same post-discovery pipeline `build()` uses
|
|
701
|
+
* ({@link buildFromDiscoveryResult}), so partitioning, output detection,
|
|
702
|
+
* serialization, and the manifest are the SAME code path, not a fork of it.
|
|
703
|
+
*
|
|
704
|
+
* Dependencies aren't part of the wire format: unlike entities, a dependency
|
|
705
|
+
* graph is plain name-to-name data with no identity problem, so it's cheaper
|
|
706
|
+
* and more honest to recompute it from the decoded entities via the same
|
|
707
|
+
* `buildDependencyGraph()` `discover()` itself uses than to carry a second,
|
|
708
|
+
* redundant wire shape across the boundary.
|
|
709
|
+
*
|
|
710
|
+
* @param label - Used only to seed circular-nested-stack detection; a JSON
|
|
711
|
+
* entity set has no single source directory the way a `build(path, …)`
|
|
712
|
+
* call does. Inert today — child projects (`nestedStack()`) aren't
|
|
713
|
+
* supported by the JSON boundary yet (see `discovery/entity-wire.ts`).
|
|
714
|
+
*/
|
|
715
|
+
export async function buildFromEntitiesJson(
|
|
716
|
+
json: DiscoveredEntitiesJson,
|
|
717
|
+
serializers: Serializer[],
|
|
718
|
+
label = "<json-entity-set>",
|
|
719
|
+
parentBuildStack?: Set<string>,
|
|
720
|
+
options?: BuildOptions,
|
|
721
|
+
): Promise<BuildResult> {
|
|
722
|
+
const entities = decodeEntitySet(json.entitySet);
|
|
723
|
+
const dependencies = buildDependencyGraph(entities);
|
|
724
|
+
const errors: DiscoveryError[] = json.errors.map(
|
|
725
|
+
(e) => new DiscoveryError(e.file, e.message, e.type as DiscoveryErrorType),
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
const discoveryResult: DiscoveryResult = {
|
|
729
|
+
entities,
|
|
730
|
+
dependencies,
|
|
731
|
+
sourceFiles: json.sourceFiles,
|
|
732
|
+
errors,
|
|
733
|
+
foldDecisions: json.foldDecisions,
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
return buildFromDiscoveryResult(discoveryResult, label, serializers, parentBuildStack, options);
|
|
737
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
2
|
import { buildCommand, resolveBuildFormat, type BuildOptions } from "./build";
|
|
3
3
|
import type { Serializer } from "../../serializer";
|
|
4
4
|
import { parseYAML } from "../../yaml";
|
|
5
5
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
-
import { join } from "node:path";
|
|
7
|
+
import { join, dirname, resolve as resolvePath } from "node:path";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
9
10
|
|
|
10
11
|
describe("buildCommand", () => {
|
|
11
12
|
let testDir: string;
|
|
@@ -133,6 +134,223 @@ export const testEntity = {
|
|
|
133
134
|
expect(result.fileCount).toBeDefined();
|
|
134
135
|
});
|
|
135
136
|
|
|
137
|
+
test("#1022 — --fold folds a leaf-only module and logs the fold decision", async () => {
|
|
138
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
139
|
+
const runtimePath = resolvePath(thisDir, "../../runtime");
|
|
140
|
+
|
|
141
|
+
await writeFile(
|
|
142
|
+
join(testDir, "resources.ts"),
|
|
143
|
+
`
|
|
144
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
145
|
+
export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
146
|
+
`
|
|
147
|
+
);
|
|
148
|
+
await writeFile(
|
|
149
|
+
join(testDir, "main.ts"),
|
|
150
|
+
`
|
|
151
|
+
import { Bucket } from "./resources";
|
|
152
|
+
throw new Error("must never execute — sentinel for #1022 fold verification");
|
|
153
|
+
export const bucket = new Bucket({ name: "my-bucket" });
|
|
154
|
+
`
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const awsSerializer: Serializer = {
|
|
158
|
+
name: "aws",
|
|
159
|
+
rulePrefix: "TEST",
|
|
160
|
+
serialize: (entities) => JSON.stringify([...entities.keys()]),
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
164
|
+
try {
|
|
165
|
+
const result = await buildCommand({
|
|
166
|
+
path: testDir,
|
|
167
|
+
format: "json",
|
|
168
|
+
serializers: [awsSerializer],
|
|
169
|
+
fold: true,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(result.success).toBe(true);
|
|
173
|
+
expect(result.resourceCount).toBe(1);
|
|
174
|
+
|
|
175
|
+
const loggedFoldLine = errorSpy.mock.calls
|
|
176
|
+
.map((call) => String(call[0]))
|
|
177
|
+
.some((line) => line.includes("[fold:fold]") && line.includes("main.ts"));
|
|
178
|
+
expect(loggedFoldLine).toBe(true);
|
|
179
|
+
} finally {
|
|
180
|
+
errorSpy.mockRestore();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("#1064 — a declared build-time parameter binds to params.<name> and folds to a literal", async () => {
|
|
185
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
186
|
+
const runtimePath = resolvePath(thisDir, "../../runtime");
|
|
187
|
+
const paramsPath = resolvePath(thisDir, "../../params");
|
|
188
|
+
|
|
189
|
+
await writeFile(
|
|
190
|
+
join(testDir, "chant.config.ts"),
|
|
191
|
+
`
|
|
192
|
+
export default {
|
|
193
|
+
buildParams: {
|
|
194
|
+
tier: { type: "string", enum: ["light", "production"], default: "light" },
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
`,
|
|
198
|
+
);
|
|
199
|
+
await writeFile(
|
|
200
|
+
join(testDir, "resources.ts"),
|
|
201
|
+
`
|
|
202
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
203
|
+
export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
204
|
+
`,
|
|
205
|
+
);
|
|
206
|
+
await writeFile(
|
|
207
|
+
join(testDir, "main.ts"),
|
|
208
|
+
`
|
|
209
|
+
import { Bucket } from "./resources";
|
|
210
|
+
import { params } from ${JSON.stringify(paramsPath)};
|
|
211
|
+
throw new Error("must never execute — sentinel for #1064 fold verification");
|
|
212
|
+
export const bucket = new Bucket({ name: params.tier });
|
|
213
|
+
`,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const awsSerializer: Serializer = {
|
|
217
|
+
name: "aws",
|
|
218
|
+
rulePrefix: "TEST",
|
|
219
|
+
serialize: (entities) => JSON.stringify([...entities.values()].map((e) => (e as unknown as { props: unknown }).props)),
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
223
|
+
try {
|
|
224
|
+
const result = await buildCommand({
|
|
225
|
+
path: testDir,
|
|
226
|
+
format: "json",
|
|
227
|
+
serializers: [awsSerializer],
|
|
228
|
+
fold: true,
|
|
229
|
+
params: { tier: "production" },
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(result.success).toBe(true);
|
|
233
|
+
expect(result.resourceCount).toBe(1);
|
|
234
|
+
expect(result.buildParams).toEqual([{ name: "tier", value: "production", source: "cli" }]);
|
|
235
|
+
|
|
236
|
+
const loggedParamLine = errorSpy.mock.calls
|
|
237
|
+
.map((call) => String(call[0]))
|
|
238
|
+
.some((line) => line.includes("[param] tier") && line.includes("production") && line.includes("cli"));
|
|
239
|
+
expect(loggedParamLine).toBe(true);
|
|
240
|
+
} finally {
|
|
241
|
+
errorSpy.mockRestore();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("#1064 — an unresolved required build-time parameter is a build error naming the parameter, not a thrown error", async () => {
|
|
246
|
+
await writeFile(
|
|
247
|
+
join(testDir, "chant.config.ts"),
|
|
248
|
+
`
|
|
249
|
+
export default {
|
|
250
|
+
buildParams: { tier: { type: "string" } },
|
|
251
|
+
};
|
|
252
|
+
`,
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const result = await buildCommand({
|
|
256
|
+
path: testDir,
|
|
257
|
+
format: "json",
|
|
258
|
+
serializers: [mockSerializer],
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
expect(result.success).toBe(false);
|
|
262
|
+
expect(result.errors.some((e) => e.includes('"tier"') && e.includes("--param"))).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("#1064 — an enum violation is a build error naming the parameter and the allowed values", async () => {
|
|
266
|
+
await writeFile(
|
|
267
|
+
join(testDir, "chant.config.ts"),
|
|
268
|
+
`
|
|
269
|
+
export default {
|
|
270
|
+
buildParams: { tier: { type: "string", enum: ["light", "production"] } },
|
|
271
|
+
};
|
|
272
|
+
`,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const result = await buildCommand({
|
|
276
|
+
path: testDir,
|
|
277
|
+
format: "json",
|
|
278
|
+
serializers: [mockSerializer],
|
|
279
|
+
params: { tier: "bogus" },
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
expect(result.success).toBe(false);
|
|
283
|
+
expect(result.errors.some((e) => e.includes('"tier"') && e.includes("bogus"))).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("#1064 — --params-file supplies values from a JSON file, second precedence after --param", async () => {
|
|
287
|
+
await writeFile(
|
|
288
|
+
join(testDir, "chant.config.ts"),
|
|
289
|
+
`
|
|
290
|
+
export default {
|
|
291
|
+
buildParams: {
|
|
292
|
+
tier: { type: "string", default: "light" },
|
|
293
|
+
env: { type: "string", default: "dev" },
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
`,
|
|
297
|
+
);
|
|
298
|
+
await writeFile(
|
|
299
|
+
join(testDir, "test.infra.ts"),
|
|
300
|
+
`
|
|
301
|
+
export const testEntity = {
|
|
302
|
+
lexicon: "test",
|
|
303
|
+
entityType: "TestEntity",
|
|
304
|
+
[Symbol.for("chant.declarable")]: true,
|
|
305
|
+
};
|
|
306
|
+
`,
|
|
307
|
+
);
|
|
308
|
+
const paramsFilePath = join(testDir, "params.json");
|
|
309
|
+
await writeFile(paramsFilePath, JSON.stringify({ tier: "production", env: "from-file" }));
|
|
310
|
+
|
|
311
|
+
const result = await buildCommand({
|
|
312
|
+
path: testDir,
|
|
313
|
+
format: "json",
|
|
314
|
+
serializers: [mockSerializer],
|
|
315
|
+
params: { tier: "from-cli" },
|
|
316
|
+
paramsFile: paramsFilePath,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
expect(result.success).toBe(true);
|
|
320
|
+
const byName = new Map((result.buildParams ?? []).map((p) => [p.name, p]));
|
|
321
|
+
expect(byName.get("tier")).toEqual({ name: "tier", value: "from-cli", source: "cli" });
|
|
322
|
+
expect(byName.get("env")).toEqual({ name: "env", value: "from-file", source: "params-file" });
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("--fold is opt-in: omitting it builds via the unchanged run path", async () => {
|
|
326
|
+
await writeFile(
|
|
327
|
+
join(testDir, "test.infra.ts"),
|
|
328
|
+
`
|
|
329
|
+
export const testEntity = {
|
|
330
|
+
lexicon: "test",
|
|
331
|
+
entityType: "TestEntity",
|
|
332
|
+
[Symbol.for("chant.declarable")]: true,
|
|
333
|
+
};
|
|
334
|
+
`
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
338
|
+
try {
|
|
339
|
+
const result = await buildCommand({
|
|
340
|
+
path: testDir,
|
|
341
|
+
format: "json",
|
|
342
|
+
serializers: [mockSerializer],
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
expect(result.success).toBe(true);
|
|
346
|
+
expect(result.resourceCount).toBe(1);
|
|
347
|
+
const anyFoldLine = errorSpy.mock.calls.map((call) => String(call[0])).some((line) => line.includes("[fold:"));
|
|
348
|
+
expect(anyFoldLine).toBe(false);
|
|
349
|
+
} finally {
|
|
350
|
+
errorSpy.mockRestore();
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
136
354
|
test("creates parent directories for the primary output path (#38)", async () => {
|
|
137
355
|
// Write into a nested temp path whose parent dirs don't yet exist —
|
|
138
356
|
// chant build should mkdir -p the parents rather than fail with ENOENT.
|