@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
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Every shipped example builds" (chant #1067).
|
|
3
|
+
*
|
|
4
|
+
* `check-lexicon.ts`'s existing example checks only count directories
|
|
5
|
+
* ("At least 1 example", "At least 3 examples", "At least 5 examples with
|
|
6
|
+
* tests") — nothing ever tries to build one. `lexicons/aws/examples/core-concepts`
|
|
7
|
+
* shipped with a discovery-time error (two files independently exporting a
|
|
8
|
+
* top-level `dataBucket`) and `chant dev check-lexicon lexicons/aws` still
|
|
9
|
+
* reported "All tier-1 checks passed," because none of its 29 checks touch
|
|
10
|
+
* build output. This module closes that gap directly: for every non-empty
|
|
11
|
+
* `examples/<name>/src/` directory, run the same discover-and-serialize
|
|
12
|
+
* pipeline `chant build` runs and report whether it produced output with no
|
|
13
|
+
* structural error.
|
|
14
|
+
*
|
|
15
|
+
* Scope, deliberately: "builds" here means discovery + serialization
|
|
16
|
+
* succeed (no `DiscoveryError`/`BuildError`, real output produced) — the
|
|
17
|
+
* same thing a `Duplicate export name` failure blocks. It does not
|
|
18
|
+
* additionally require the output to pass every post-synth/lint check
|
|
19
|
+
* (WAW0xx and friends). Those are a separate, already-gated contract
|
|
20
|
+
* (`chant lint`, the post-synth pipeline) with their own severity model and
|
|
21
|
+
* their own CI step; several existing AWS examples (docs-snippets,
|
|
22
|
+
* lambda-api, lambda-s3, shared-alb) currently fail one or more post-synth
|
|
23
|
+
* checks for reasons unrelated to this issue (e.g. WAW042, a TLS-only
|
|
24
|
+
* bucket policy check added after those examples were written). Folding
|
|
25
|
+
* that axis into "does it build" would fail this new check for all of them
|
|
26
|
+
* on its very first run, for defects this issue never set out to fix.
|
|
27
|
+
* Tracked separately; not silently absorbed here.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { existsSync, readdirSync } from "fs";
|
|
31
|
+
import { join } from "path";
|
|
32
|
+
import { build } from "../../build";
|
|
33
|
+
import { findInfraFiles } from "../../discovery/files";
|
|
34
|
+
import { detectLexicons } from "../../detectLexicon";
|
|
35
|
+
import { loadPlugins } from "../plugins";
|
|
36
|
+
|
|
37
|
+
export interface ExampleBuildResult {
|
|
38
|
+
example: string;
|
|
39
|
+
ok: boolean;
|
|
40
|
+
detail?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build every non-empty example under `<lexiconDir>/examples/*\/src` and
|
|
45
|
+
* report per-example pass/fail. Returns `[]` when the lexicon has no
|
|
46
|
+
* `examples/` directory at all (a separate tier-1 check already covers
|
|
47
|
+
* that).
|
|
48
|
+
*/
|
|
49
|
+
export async function checkExamplesBuild(lexiconDir: string): Promise<ExampleBuildResult[]> {
|
|
50
|
+
const examplesDir = join(lexiconDir, "examples");
|
|
51
|
+
if (!existsSync(examplesDir)) return [];
|
|
52
|
+
|
|
53
|
+
const results: ExampleBuildResult[] = [];
|
|
54
|
+
const entries = readdirSync(examplesDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
55
|
+
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const srcDir = join(examplesDir, entry.name, "src");
|
|
58
|
+
if (!existsSync(srcDir)) continue;
|
|
59
|
+
|
|
60
|
+
const contents = readdirSync(srcDir);
|
|
61
|
+
if (contents.length === 0 || (contents.length === 1 && contents[0] === ".gitkeep")) continue;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const files = await findInfraFiles(srcDir);
|
|
65
|
+
if (files.length === 0) continue;
|
|
66
|
+
|
|
67
|
+
const lexiconNames = await detectLexicons(files);
|
|
68
|
+
if (lexiconNames.length === 0) {
|
|
69
|
+
results.push({
|
|
70
|
+
example: entry.name,
|
|
71
|
+
ok: false,
|
|
72
|
+
detail: "no lexicon imports detected in src/ — cannot determine which plugin(s) to build with",
|
|
73
|
+
});
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const plugins = await loadPlugins(lexiconNames);
|
|
78
|
+
const result = await build(srcDir, plugins.map((p) => p.serializer));
|
|
79
|
+
|
|
80
|
+
const structuralErrors = result.errors.map((e) => e.message);
|
|
81
|
+
const producedOutput = result.outputs.size > 0;
|
|
82
|
+
const ok = structuralErrors.length === 0 && producedOutput;
|
|
83
|
+
|
|
84
|
+
results.push({
|
|
85
|
+
example: entry.name,
|
|
86
|
+
ok,
|
|
87
|
+
detail: ok
|
|
88
|
+
? undefined
|
|
89
|
+
: structuralErrors.length > 0
|
|
90
|
+
? structuralErrors.join("; ")
|
|
91
|
+
: "discovered source but produced no output",
|
|
92
|
+
});
|
|
93
|
+
} catch (err) {
|
|
94
|
+
results.push({
|
|
95
|
+
example: entry.name,
|
|
96
|
+
ok: false,
|
|
97
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return results;
|
|
103
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
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 { auditIntrinsics } from "./check-lexicon-intrinsics";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* chant #1067 — proof that the intrinsic-foldability audit actually
|
|
9
|
+
* validates, rather than just counting. Each case here is a small,
|
|
10
|
+
* synthetic lexicon (plugin.ts + index.ts + intrinsics.ts) so the
|
|
11
|
+
* fixtures encode the exact failure shapes #1039 shipped: a genuine
|
|
12
|
+
* tagged template registered with the wrong `isTag`, and a plain call
|
|
13
|
+
* registered with the wrong `isTag`, in both directions — plus a
|
|
14
|
+
* registration naming something the package never exports.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
function writeFixture(dir: string): void {
|
|
18
|
+
mkdirSync(join(dir, "src"), { recursive: true });
|
|
19
|
+
|
|
20
|
+
writeFileSync(
|
|
21
|
+
join(dir, "src/plugin.ts"),
|
|
22
|
+
`export const testPlugin = {
|
|
23
|
+
name: "test",
|
|
24
|
+
intrinsics() {
|
|
25
|
+
return [
|
|
26
|
+
{ name: "Tag1", description: "a genuine tagged template", isTag: true },
|
|
27
|
+
{ name: "Plain1", description: "a genuine plain call", isTag: false },
|
|
28
|
+
{ name: "Mismatch1", description: "claims tag but is a plain call", isTag: true },
|
|
29
|
+
{ name: "Mismatch2", description: "claims plain but is a tagged template", isTag: false },
|
|
30
|
+
{ name: "Missing1", description: "registered but never exported", isTag: false },
|
|
31
|
+
{ name: "Plain2", description: "a plain call opted into call-form folding", isTag: false, foldsAsCall: true },
|
|
32
|
+
{ name: "Tag2", description: "a tagged template wrongly opted into call-form folding", isTag: true, foldsAsCall: true },
|
|
33
|
+
];
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
`,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
writeFileSync(
|
|
40
|
+
join(dir, "src/index.ts"),
|
|
41
|
+
`export { Tag1, Plain1, Mismatch1, Mismatch2, Plain2, Tag2 } from "./intrinsics";
|
|
42
|
+
`,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
writeFileSync(
|
|
46
|
+
join(dir, "src/intrinsics.ts"),
|
|
47
|
+
`export function Tag1(strings: TemplateStringsArray, ...values: unknown[]): string {
|
|
48
|
+
return strings.join("");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function Plain1(x: string): string {
|
|
52
|
+
return x;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Registered as isTag: true above — this is the #1039 "aws Sub" shape,
|
|
56
|
+
// inverted: a plain call wrongly claimed as a tag.
|
|
57
|
+
export function Mismatch1(x: string): string {
|
|
58
|
+
return x;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Registered as isTag: false above — this is the #1039 "gitlab reference()"
|
|
62
|
+
// shape: a genuine tagged template wrongly claimed as a plain call.
|
|
63
|
+
export function Mismatch2(strings: TemplateStringsArray, ...values: unknown[]): string {
|
|
64
|
+
return strings.join("");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function Plain2(x: string): string {
|
|
68
|
+
return x;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// chant #1044 — registered with foldsAsCall: true above, but authored as a
|
|
72
|
+
// tagged template, so there is no plain-call form to opt in.
|
|
73
|
+
export function Tag2(strings: TemplateStringsArray, ...values: unknown[]): string {
|
|
74
|
+
return strings.join("");
|
|
75
|
+
}
|
|
76
|
+
`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe("auditIntrinsics", () => {
|
|
81
|
+
let dir: string;
|
|
82
|
+
|
|
83
|
+
beforeAll(() => {
|
|
84
|
+
dir = mkdtempSync(join(tmpdir(), "chant-check-lexicon-intrinsics-"));
|
|
85
|
+
writeFixture(dir);
|
|
86
|
+
});
|
|
87
|
+
afterAll(() => rmSync(dir, { recursive: true, force: true }));
|
|
88
|
+
|
|
89
|
+
test("a genuine tagged template registered as isTag: true passes", () => {
|
|
90
|
+
const items = auditIntrinsics(dir);
|
|
91
|
+
const item = items.find((i) => i.name === "Tag1");
|
|
92
|
+
expect(item).toMatchObject({ exported: true, actualIsTag: true, ok: true });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("a genuine plain call registered as isTag: false passes", () => {
|
|
96
|
+
const items = auditIntrinsics(dir);
|
|
97
|
+
const item = items.find((i) => i.name === "Plain1");
|
|
98
|
+
expect(item).toMatchObject({ exported: true, actualIsTag: false, ok: true });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("a plain call wrongly registered as isTag: true fails (aws Sub shape, inverted)", () => {
|
|
102
|
+
const items = auditIntrinsics(dir);
|
|
103
|
+
const item = items.find((i) => i.name === "Mismatch1");
|
|
104
|
+
expect(item?.ok).toBe(false);
|
|
105
|
+
expect(item?.exported).toBe(true);
|
|
106
|
+
expect(item?.actualIsTag).toBe(false);
|
|
107
|
+
expect(item?.detail).toMatch(/plain call.*but registered with isTag: true/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("a tagged template wrongly registered as isTag: false fails (gitlab reference() shape)", () => {
|
|
111
|
+
const items = auditIntrinsics(dir);
|
|
112
|
+
const item = items.find((i) => i.name === "Mismatch2");
|
|
113
|
+
expect(item?.ok).toBe(false);
|
|
114
|
+
expect(item?.exported).toBe(true);
|
|
115
|
+
expect(item?.actualIsTag).toBe(true);
|
|
116
|
+
expect(item?.detail).toMatch(/authored as a tagged template.*isTag: false/);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("a registered intrinsic that isn't exported fails, distinctly from a signature mismatch", () => {
|
|
120
|
+
const items = auditIntrinsics(dir);
|
|
121
|
+
const item = items.find((i) => i.name === "Missing1");
|
|
122
|
+
expect(item?.exported).toBe(false);
|
|
123
|
+
expect(item?.ok).toBe(false);
|
|
124
|
+
expect(item?.detail).toMatch(/not exported from src\/index\.ts/);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("a plain call opted into call-form folding passes (chant #1044)", () => {
|
|
128
|
+
const items = auditIntrinsics(dir);
|
|
129
|
+
const item = items.find((i) => i.name === "Plain2");
|
|
130
|
+
expect(item).toMatchObject({ exported: true, actualIsTag: false, ok: true, declaredFoldsAsCall: true, callFormOk: true });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("a tagged template registered with foldsAsCall: true fails — the opt-in is for plain calls only (chant #1044)", () => {
|
|
134
|
+
const items = auditIntrinsics(dir);
|
|
135
|
+
const item = items.find((i) => i.name === "Tag2");
|
|
136
|
+
// The isTag half is fine (it really is a tag); the call-form half is not.
|
|
137
|
+
expect(item?.ok).toBe(true);
|
|
138
|
+
expect(item?.callFormOk).toBe(false);
|
|
139
|
+
expect(item?.callFormDetail).toMatch(/authored as a tagged template.*foldsAsCall: true/);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("an intrinsic with no foldsAsCall at all is simply not opted in — absent, not false (chant #1044)", () => {
|
|
143
|
+
const items = auditIntrinsics(dir);
|
|
144
|
+
const item = items.find((i) => i.name === "Plain1");
|
|
145
|
+
expect(item?.declaredFoldsAsCall).toBeUndefined();
|
|
146
|
+
expect(item?.callFormOk).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("returns [] for a lexicon with no intrinsics() method", () => {
|
|
150
|
+
const emptyDir = mkdtempSync(join(tmpdir(), "chant-check-lexicon-intrinsics-empty-"));
|
|
151
|
+
try {
|
|
152
|
+
mkdirSync(join(emptyDir, "src"), { recursive: true });
|
|
153
|
+
writeFileSync(join(emptyDir, "src/plugin.ts"), `export const testPlugin = { name: "test" };\n`);
|
|
154
|
+
expect(auditIntrinsics(emptyDir)).toEqual([]);
|
|
155
|
+
} finally {
|
|
156
|
+
rmSync(emptyDir, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("returns [] for a lexicon whose intrinsics() legitimately registers none (gcp/k8s shape)", () => {
|
|
161
|
+
const emptyArrayDir = mkdtempSync(join(tmpdir(), "chant-check-lexicon-intrinsics-emptyarray-"));
|
|
162
|
+
try {
|
|
163
|
+
mkdirSync(join(emptyArrayDir, "src"), { recursive: true });
|
|
164
|
+
writeFileSync(
|
|
165
|
+
join(emptyArrayDir, "src/plugin.ts"),
|
|
166
|
+
`export const testPlugin = {
|
|
167
|
+
name: "test",
|
|
168
|
+
intrinsics() {
|
|
169
|
+
return [];
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
`,
|
|
173
|
+
);
|
|
174
|
+
expect(auditIntrinsics(emptyArrayDir)).toEqual([]);
|
|
175
|
+
} finally {
|
|
176
|
+
rmSync(emptyArrayDir, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
});
|