@intentius/chant 0.19.1 → 0.20.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 +54 -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/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/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 +210 -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 +226 -0
- package/dist/fold/fold.d.ts.map +1 -0
- package/dist/fold/subset.d.ts +102 -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 +2 -2
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +81 -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/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 +145 -0
- package/src/cli/commands/check-lexicon-intrinsics.ts +312 -0
- package/src/cli/commands/check-lexicon.test.ts +34 -0
- package/src/cli/commands/check-lexicon.ts +119 -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 +61 -0
- package/src/codegen/docs-sections.ts +7 -3
- 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/discovery/collect.ts +17 -3
- package/src/discovery/entity-wire-codec.ts +485 -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 +1026 -0
- package/src/discovery/fold-import.ts +1805 -0
- package/src/discovery/index.test.ts +191 -1
- package/src/discovery/index.ts +233 -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 +535 -0
- package/src/fold/fold.ts +648 -0
- package/src/fold/subset-doc-parity.test.ts +183 -0
- package/src/fold/subset.test.ts +241 -0
- package/src/fold/subset.ts +302 -0
- package/src/index.ts +2 -0
- package/src/lexicon-output.ts +7 -2
- package/src/lexicon-schema.test.ts +14 -0
- package/src/lexicon-schema.ts +3 -1
- package/src/lexicon.ts +82 -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/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.
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { build } from "../../build";
|
|
2
|
-
import { loadChantConfig, resolveOwnershipMarker } from "../../config";
|
|
2
|
+
import { loadChantConfig, resolveOwnershipMarker, resolveFoldEnabled, resolveSandboxEnabled } from "../../config";
|
|
3
|
+
import { resolveBuildParams } from "../../build-params";
|
|
3
4
|
import type { Serializer, SerializerResult } from "../../serializer";
|
|
4
5
|
import type { LexiconPlugin } from "../../lexicon";
|
|
5
6
|
import { runPostSynthChecks } from "../../lint/post-synth";
|
|
6
7
|
import { loadPolicyChecks } from "../../lint/policy";
|
|
7
8
|
import { sortedJsonReplacer } from "../../utils";
|
|
8
9
|
import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
|
|
9
|
-
import { writeFileSync, mkdirSync } from "fs";
|
|
10
|
-
import { resolve, dirname, join } from "path";
|
|
10
|
+
import { writeFileSync, mkdirSync, readFileSync } from "fs";
|
|
11
|
+
import { resolve, dirname, join, relative } from "path";
|
|
11
12
|
import { watchDirectory, formatTimestamp, formatChangedFiles } from "../watch";
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -32,6 +33,38 @@ export interface BuildOptions {
|
|
|
32
33
|
* policy can branch on environment.
|
|
33
34
|
*/
|
|
34
35
|
env?: string;
|
|
36
|
+
/**
|
|
37
|
+
* chant #1022 (epic #1019) — opt-in: fold source modules statically
|
|
38
|
+
* instead of importing/running them (`chant build --fold`). Falls back to
|
|
39
|
+
* run per-file for anything the folder can't represent. Merged with the
|
|
40
|
+
* project's `chant.config.ts` `build.fold` via {@link resolveFoldEnabled}
|
|
41
|
+
* — this flag, when true, always wins for the invocation.
|
|
42
|
+
*/
|
|
43
|
+
fold?: boolean;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* chant #1045 Phase 2 — opt-in: run-fallback source files (or, without
|
|
47
|
+
* `fold`, every file) execute together, isolated, in one sandboxed child
|
|
48
|
+
* process (`chant build --sandbox`). Merged with the project's
|
|
49
|
+
* `chant.config.ts` `build.sandbox` via {@link resolveSandboxEnabled} —
|
|
50
|
+
* this flag, when true, always wins for the invocation.
|
|
51
|
+
*/
|
|
52
|
+
sandbox?: boolean;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* chant #1064 — `--param name=value` flags (repeatable), parsed to a flat
|
|
56
|
+
* `{ name: value }` record of raw (unvalidated) strings. Highest
|
|
57
|
+
* precedence in {@link resolveBuildParams}'s resolution against the
|
|
58
|
+
* project's declared `chant.config.ts` `buildParams`.
|
|
59
|
+
*/
|
|
60
|
+
params?: Record<string, string>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* chant #1064 — `--params-file <path>`: a JSON file of `{ "name": value }`
|
|
64
|
+
* build-time parameter values, read and parsed here. Second precedence,
|
|
65
|
+
* after {@link params}.
|
|
66
|
+
*/
|
|
67
|
+
paramsFile?: string;
|
|
35
68
|
}
|
|
36
69
|
|
|
37
70
|
/**
|
|
@@ -81,6 +114,8 @@ export interface BuildResult {
|
|
|
81
114
|
errors: string[];
|
|
82
115
|
/** Warning messages */
|
|
83
116
|
warnings: string[];
|
|
117
|
+
/** This build's resolved build-time parameters (#1064) — see `BuildResult.buildParams` (../../build.ts). Empty when the project declares/supplies none. */
|
|
118
|
+
buildParams?: import("../../provenance").BuildParamProvenance[];
|
|
84
119
|
}
|
|
85
120
|
|
|
86
121
|
/**
|
|
@@ -110,12 +145,84 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
110
145
|
? await loadPolicyChecks(config.lint.policies, configDir)
|
|
111
146
|
: [];
|
|
112
147
|
|
|
148
|
+
// #1022 — opt-in fold path: the CLI flag wins over `chant.config.ts`'s
|
|
149
|
+
// `build.fold`, which wins over the (unchanged) default of running every
|
|
150
|
+
// module.
|
|
151
|
+
const fold = resolveFoldEnabled(config, options.fold);
|
|
152
|
+
|
|
153
|
+
// #1045 Phase 2 — opt-in sandboxed execution of run-fallback files (or,
|
|
154
|
+
// without --fold, every file). Same CLI-flag-wins-over-config precedence
|
|
155
|
+
// as fold, resolved independently.
|
|
156
|
+
const sandbox = resolveSandboxEnabled(config, options.sandbox);
|
|
157
|
+
|
|
158
|
+
// #1064 — resolve declared build-time parameters (chant.config.ts's
|
|
159
|
+
// buildParams) against this invocation's --param/--params-file/declared
|
|
160
|
+
// env mapping, BEFORE calling build() — a resolution failure (an unknown
|
|
161
|
+
// name, a missing required value, a type/enum mismatch) is reported as a
|
|
162
|
+
// chant build error naming the parameter, never a thrown error from inside
|
|
163
|
+
// user source (which is what loomster's hand-rolled `tierFromEnv()`-style
|
|
164
|
+
// validators did before migrating to this mechanism).
|
|
165
|
+
let paramsFileContent: Record<string, unknown> | undefined;
|
|
166
|
+
if (options.paramsFile) {
|
|
167
|
+
try {
|
|
168
|
+
paramsFileContent = JSON.parse(readFileSync(resolve(options.paramsFile), "utf-8"));
|
|
169
|
+
} catch (err) {
|
|
170
|
+
errors.push(
|
|
171
|
+
formatError({
|
|
172
|
+
message: `Failed to read/parse --params-file "${options.paramsFile}": ${err instanceof Error ? err.message : String(err)}`,
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const paramsResolution = resolveBuildParams(config.buildParams, {
|
|
178
|
+
cli: options.params,
|
|
179
|
+
fromFile: paramsFileContent,
|
|
180
|
+
env: process.env,
|
|
181
|
+
});
|
|
182
|
+
for (const message of paramsResolution.errors) {
|
|
183
|
+
errors.push(formatError({ message }));
|
|
184
|
+
}
|
|
185
|
+
if (errors.length > 0) {
|
|
186
|
+
return { success: false, resourceCount: 0, fileCount: 0, errors, warnings };
|
|
187
|
+
}
|
|
188
|
+
// #1064 — build-provenance visibility: report every resolved build-time
|
|
189
|
+
// parameter (name, value, and which source won it) the same
|
|
190
|
+
// unconditional-log-not-gated-on---verbose way #1022's fold decisions are
|
|
191
|
+
// reported just below, so a build's environment-varying inputs are as
|
|
192
|
+
// visible as its fold-vs-run choices.
|
|
193
|
+
for (const p of paramsResolution.provenance) {
|
|
194
|
+
console.error(formatInfo(`[param] ${p.name} = ${JSON.stringify(p.value)} (${p.source})`));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// #1039 — thread each loaded plugin's registered intrinsics (e.g. AWS's
|
|
198
|
+
// `Sub`) through to the fold path, so a file using a registered intrinsic
|
|
199
|
+
// tagged template folds instead of unconditionally falling back to run.
|
|
200
|
+
// `intrinsics` is an optional plugin extension (not every lexicon defines
|
|
201
|
+
// any), hence the guard.
|
|
202
|
+
const intrinsics = options.plugins?.flatMap((plugin) => plugin.intrinsics?.() ?? []) ?? [];
|
|
203
|
+
|
|
113
204
|
// Run the build
|
|
114
205
|
const result = await build(infraPath, options.serializers, undefined, {
|
|
115
206
|
ownership,
|
|
116
207
|
config: config as unknown as Record<string, unknown>,
|
|
208
|
+
fold,
|
|
209
|
+
sandbox,
|
|
210
|
+
intrinsics,
|
|
211
|
+
buildParams: paramsResolution.provenance,
|
|
117
212
|
});
|
|
118
213
|
|
|
214
|
+
// #1022 — report per-file fold vs run so it's visible what still runs.
|
|
215
|
+
if (fold) {
|
|
216
|
+
for (const decision of result.foldDecisions) {
|
|
217
|
+
const rel = relative(infraPath, decision.file) || decision.file;
|
|
218
|
+
const detail =
|
|
219
|
+
decision.mode === "fold"
|
|
220
|
+
? `${decision.resourceCount ?? 0} resource(s), no module execution`
|
|
221
|
+
: (decision.reason ?? "fell back to run");
|
|
222
|
+
console.error(formatInfo(`[fold:${decision.mode}] ${rel} — ${detail}`));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
119
226
|
// Format errors
|
|
120
227
|
for (const error of result.errors) {
|
|
121
228
|
const formatted = formatError({
|
|
@@ -359,6 +466,7 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
359
466
|
fileCount,
|
|
360
467
|
errors,
|
|
361
468
|
warnings,
|
|
469
|
+
buildParams: paramsResolution.provenance,
|
|
362
470
|
};
|
|
363
471
|
}
|
|
364
472
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { checkExamplesBuild } from "./check-lexicon-examples";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* chant #1067 — proof that "every shipped example builds" actually builds
|
|
9
|
+
* one, rather than counting example directories the way every prior
|
|
10
|
+
* check-lexicon.ts check did. The fixture reproduces the exact defect
|
|
11
|
+
* found in `lexicons/aws/examples/core-concepts`: two files in the same
|
|
12
|
+
* example independently exporting a top-level binding of the same name,
|
|
13
|
+
* which is fine on its own but a "Duplicate export name" discovery error
|
|
14
|
+
* the moment the whole `src/` directory is built as one project — which is
|
|
15
|
+
* exactly what each example's own `npm run build` does.
|
|
16
|
+
*
|
|
17
|
+
* These fixtures import the real, already-installed `@intentius/chant-lexicon-aws`
|
|
18
|
+
* package (workspace-linked), so no lexicon plugin needs to be faked.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function writeLexiconDirWithExample(
|
|
22
|
+
lexiconDir: string,
|
|
23
|
+
exampleName: string,
|
|
24
|
+
files: Record<string, string>,
|
|
25
|
+
): void {
|
|
26
|
+
const srcDir = join(lexiconDir, "examples", exampleName, "src");
|
|
27
|
+
mkdirSync(srcDir, { recursive: true });
|
|
28
|
+
for (const [name, content] of Object.entries(files)) {
|
|
29
|
+
writeFileSync(join(srcDir, name), content);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("checkExamplesBuild", () => {
|
|
34
|
+
let dir: string;
|
|
35
|
+
|
|
36
|
+
beforeAll(() => {
|
|
37
|
+
dir = mkdtempSync(join(tmpdir(), "chant-check-lexicon-examples-"));
|
|
38
|
+
});
|
|
39
|
+
afterAll(() => rmSync(dir, { recursive: true, force: true }));
|
|
40
|
+
|
|
41
|
+
test("a clean example builds", async () => {
|
|
42
|
+
writeLexiconDirWithExample(dir, "clean", {
|
|
43
|
+
"bucket.ts": `import { Bucket } from "@intentius/chant-lexicon-aws";
|
|
44
|
+
|
|
45
|
+
export const appBucket = new Bucket({
|
|
46
|
+
BucketName: "my-app-bucket",
|
|
47
|
+
});
|
|
48
|
+
`,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const results = await checkExamplesBuild(dir);
|
|
52
|
+
const clean = results.find((r) => r.example === "clean");
|
|
53
|
+
expect(clean).toMatchObject({ ok: true });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("two files independently exporting the same top-level name fails to build (the core-concepts defect)", async () => {
|
|
57
|
+
writeLexiconDirWithExample(dir, "broken", {
|
|
58
|
+
"a.ts": `import { Bucket } from "@intentius/chant-lexicon-aws";
|
|
59
|
+
|
|
60
|
+
export const dataBucket = new Bucket({
|
|
61
|
+
BucketName: "a-bucket",
|
|
62
|
+
});
|
|
63
|
+
`,
|
|
64
|
+
"b.ts": `import { Bucket } from "@intentius/chant-lexicon-aws";
|
|
65
|
+
|
|
66
|
+
export const dataBucket = new Bucket({
|
|
67
|
+
BucketName: "b-bucket",
|
|
68
|
+
});
|
|
69
|
+
`,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const results = await checkExamplesBuild(dir);
|
|
73
|
+
const broken = results.find((r) => r.example === "broken");
|
|
74
|
+
expect(broken?.ok).toBe(false);
|
|
75
|
+
expect(broken?.detail).toMatch(/Duplicate export name "dataBucket" found/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("an empty src/ directory is skipped, not reported as a failure", async () => {
|
|
79
|
+
writeLexiconDirWithExample(dir, "empty", {});
|
|
80
|
+
const results = await checkExamplesBuild(dir);
|
|
81
|
+
expect(results.find((r) => r.example === "empty")).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("returns [] when there is no examples/ directory at all", async () => {
|
|
85
|
+
const noExamplesDir = mkdtempSync(join(tmpdir(), "chant-check-lexicon-examples-none-"));
|
|
86
|
+
try {
|
|
87
|
+
expect(await checkExamplesBuild(noExamplesDir)).toEqual([]);
|
|
88
|
+
} finally {
|
|
89
|
+
rmSync(noExamplesDir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|