@intentius/chant 0.26.0 → 0.28.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/cli/commands/build.d.ts +6 -5
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/config.d.ts +13 -10
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/sandbox/config-run.d.ts.map +1 -1
- package/dist/discovery/sandbox/fork.d.ts +25 -0
- package/dist/discovery/sandbox/fork.d.ts.map +1 -1
- package/dist/discovery/sandbox/policy-run.d.ts.map +1 -1
- package/dist/discovery/sandbox/run.d.ts.map +1 -1
- package/dist/kubectl-context.d.ts +73 -0
- package/dist/kubectl-context.d.ts.map +1 -0
- package/dist/lint/config.d.ts +80 -0
- package/dist/lint/config.d.ts.map +1 -1
- package/dist/lint/policy.d.ts +8 -2
- package/dist/lint/policy.d.ts.map +1 -1
- package/dist/lint/post-synth.d.ts +18 -1
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/stack-output.d.ts +9 -4
- package/dist/stack-output.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/build.test.ts +206 -2
- package/src/cli/commands/build.ts +43 -10
- package/src/cli/commands/lint.ts +17 -25
- package/src/cli/main.test.ts +6 -0
- package/src/cli/main.ts +10 -2
- package/src/config.test.ts +28 -1
- package/src/config.ts +16 -12
- package/src/discovery/sandbox/config-boundary.test.ts +55 -1
- package/src/discovery/sandbox/config-run.ts +3 -0
- package/src/discovery/sandbox/fork.ts +75 -1
- package/src/discovery/sandbox/policy-boundary.test.ts +56 -1
- package/src/discovery/sandbox/policy-run.ts +15 -1
- package/src/discovery/sandbox/run.test.ts +85 -1
- package/src/discovery/sandbox/run.ts +3 -0
- package/src/kubectl-context.test.ts +94 -0
- package/src/kubectl-context.ts +126 -0
- package/src/lint/config.test.ts +93 -1
- package/src/lint/config.ts +108 -0
- package/src/lint/policy.test.ts +90 -0
- package/src/lint/policy.ts +17 -5
- package/src/lint/post-synth.test.ts +4 -0
- package/src/lint/post-synth.ts +30 -1
- package/src/stack-output.test.ts +21 -3
- package/src/stack-output.ts +21 -9
package/src/lint/config.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join, dirname, resolve } from "path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { evaluateProjectConfigSync } from "../config-sandbox";
|
|
5
5
|
import type { Severity, RuleConfig } from "./rule";
|
|
6
|
+
import type { PostSynthDiagnostic } from "./post-synth";
|
|
6
7
|
import { moduleDir, getRuntime } from "../runtime-adapter";
|
|
7
8
|
import strictPreset from "./presets/strict.json";
|
|
8
9
|
|
|
@@ -202,6 +203,40 @@ export function parseRuleConfig(value: RuleConfig): ParsedRuleConfig {
|
|
|
202
203
|
return { severity, options };
|
|
203
204
|
}
|
|
204
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Resolve one check/rule id's effective severity (and options) against an
|
|
208
|
+
* already-resolved `lint.rules` map — the ONE place `"off"`/severity-override
|
|
209
|
+
* resolution happens, so a rule id behaves identically regardless of which
|
|
210
|
+
* phase produced it.
|
|
211
|
+
*
|
|
212
|
+
* chant #1138 — before this, the same `lint.rules: { ID: "off" }` config was
|
|
213
|
+
* resolved by two independent call sites that had grown their own copy of
|
|
214
|
+
* this logic (`../cli/commands/lint.ts`'s `getDefaultRules` for AST COR/EVL
|
|
215
|
+
* rules, and its `runComponentCheckDiagnostics` for whole-component COMP*
|
|
216
|
+
* checks) — identical in effect, but a rule id's suppression having two
|
|
217
|
+
* places to (potentially, eventually) diverge is itself the bug class #1138
|
|
218
|
+
* is about. Both were converted to call this instead, and post-synth checks/
|
|
219
|
+
* policies (`./post-synth.ts`'s `applyConfiguredSeverity`) now go through it
|
|
220
|
+
* too, closing the gap the issue reports: a post-synth check id honors
|
|
221
|
+
* `lint.rules` exactly like an AST rule id does.
|
|
222
|
+
*
|
|
223
|
+
* `rules` takes the already-resolved map (`config.rules`, or
|
|
224
|
+
* `resolveRulesForFile`'s per-file merge) rather than a whole `LintConfig` —
|
|
225
|
+
* callers that need per-file `overrides` resolve that first; post-synth
|
|
226
|
+
* checks have no per-file scope to begin with (see {@link
|
|
227
|
+
* ./post-synth.ts!PostSynthDiagnostic}'s doc for why), so they always pass
|
|
228
|
+
* `config.rules` directly.
|
|
229
|
+
*/
|
|
230
|
+
export function resolveConfiguredSeverity(
|
|
231
|
+
rules: Record<string, RuleConfig> | undefined,
|
|
232
|
+
id: string,
|
|
233
|
+
defaultSeverity: Severity,
|
|
234
|
+
): ParsedRuleConfig {
|
|
235
|
+
const configValue = rules?.[id];
|
|
236
|
+
if (configValue === undefined) return { severity: defaultSeverity };
|
|
237
|
+
return parseRuleConfig(configValue);
|
|
238
|
+
}
|
|
239
|
+
|
|
205
240
|
/**
|
|
206
241
|
* Default configuration with all rules enabled at strict preset severities
|
|
207
242
|
*/
|
|
@@ -411,3 +446,76 @@ export function resolveRulesForFile(config: LintConfig, filePath: string): Recor
|
|
|
411
446
|
|
|
412
447
|
return rules;
|
|
413
448
|
}
|
|
449
|
+
|
|
450
|
+
/** Result of applying `lint.rules` to a set of post-synth diagnostics. */
|
|
451
|
+
export interface PostSynthSeverityResult {
|
|
452
|
+
/** Diagnostics after config resolution — `"off"`-suppressed ones removed, everything else at its resolved severity. */
|
|
453
|
+
diagnostics: PostSynthDiagnostic[];
|
|
454
|
+
/**
|
|
455
|
+
* Diagnostics `lint.rules` turned `"off"`, unaltered — present so a caller
|
|
456
|
+
* can report a count (chant #1138) rather than the finding simply
|
|
457
|
+
* vanishing, mirroring `../lint/engine.ts`'s `LintRunResult.suppressed` for
|
|
458
|
+
* AST rules.
|
|
459
|
+
*/
|
|
460
|
+
suppressed: PostSynthDiagnostic[];
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Apply `lint.rules` severity overrides to already-produced post-synth
|
|
465
|
+
* diagnostics — one lexicon-shipped check's findings, or one project's
|
|
466
|
+
* `lint.policies` findings, it doesn't matter which: both are plain
|
|
467
|
+
* `PostSynthDiagnostic[]` by the time they reach this function, so both go
|
|
468
|
+
* through the identical resolution an AST rule id or a COMP* check id gets
|
|
469
|
+
* (`resolveConfiguredSeverity`, above), keyed by `diag.checkId` instead of
|
|
470
|
+
* `LintRule.id`/`ComponentCheck.id`. `"off"` suppresses (moved to
|
|
471
|
+
* `suppressed`); any other configured severity replaces `diag.severity`,
|
|
472
|
+
* exactly as a config override changes an AST diagnostic's reported level.
|
|
473
|
+
*
|
|
474
|
+
* Lives here rather than in `./post-synth.ts` (where `PostSynthDiagnostic` is
|
|
475
|
+
* declared) on purpose: `post-synth.ts` is a leaf every lexicon's checks
|
|
476
|
+
* import as a real runtime module, and this file resolves built-in preset
|
|
477
|
+
* paths via the runtime adapter at module scope — pulling that into every
|
|
478
|
+
* lexicon's check barrel merely to share one filter function would be the
|
|
479
|
+
* wrong trade. Only this file's TYPE (`PostSynthDiagnostic`) crosses back,
|
|
480
|
+
* which costs nothing at runtime.
|
|
481
|
+
*
|
|
482
|
+
* chant #1138 — deliberately does NOT also honor `chant-disable` source
|
|
483
|
+
* comments. `PostSynthDiagnostic` has no source anchor to disable AT — see
|
|
484
|
+
* its doc comment (`./post-synth.ts`) for why `entity` doesn't supply one —
|
|
485
|
+
* so there is no coherent site to check for a directive. Even in the one case
|
|
486
|
+
* a real anchor sometimes exists (a live, in-process `ctx.entities` value
|
|
487
|
+
* stamped with build provenance, `../provenance.ts`'s `getProvenance`), it
|
|
488
|
+
* would not generalize: not every check sets `entity`, `entity` isn't
|
|
489
|
+
* guaranteed to be an entities-map key (it's a name in the synthesized
|
|
490
|
+
* OUTPUT — a CFN logical id, a k8s `metadata.name` — which a serializer is
|
|
491
|
+
* free to have derived, prefixed, or renamed from the source-level entity
|
|
492
|
+
* name), and that provenance never crosses the `--sandbox` policy child's
|
|
493
|
+
* JSON wire (`../discovery/entity-wire-codec.ts` doesn't carry it, and
|
|
494
|
+
* re-deriving it on the far side would mean sending source file paths into a
|
|
495
|
+
* channel that's supposed to carry only the resolved build). Building
|
|
496
|
+
* directive suppression on a sometimes-present, sometimes-not anchor would
|
|
497
|
+
* make `chant build` and `chant build --sandbox` disagree about the
|
|
498
|
+
* identical finding depending on which path happened to still have the
|
|
499
|
+
* entity object around — precisely the kind of inconsistency #1138 exists to
|
|
500
|
+
* remove. Config severity (`"off"`) is the one suppression surface this can
|
|
501
|
+
* offer uniformly; a check that wants a per-instance escape hatch can read
|
|
502
|
+
* `ctx.env`/its own options to decide not to emit a diagnostic at all.
|
|
503
|
+
*/
|
|
504
|
+
export function applyConfiguredSeverity(
|
|
505
|
+
diagnostics: readonly PostSynthDiagnostic[],
|
|
506
|
+
rules: Record<string, RuleConfig> | undefined,
|
|
507
|
+
): PostSynthSeverityResult {
|
|
508
|
+
const kept: PostSynthDiagnostic[] = [];
|
|
509
|
+
const suppressed: PostSynthDiagnostic[] = [];
|
|
510
|
+
|
|
511
|
+
for (const diag of diagnostics) {
|
|
512
|
+
const resolved = resolveConfiguredSeverity(rules, diag.checkId, diag.severity);
|
|
513
|
+
if (resolved.severity === "off") {
|
|
514
|
+
suppressed.push(diag);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
kept.push(resolved.severity === diag.severity ? diag : { ...diag, severity: resolved.severity });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return { diagnostics: kept, suppressed };
|
|
521
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chant #1138 — `evaluateProjectPolicies` (the `policyGate` Op step's entry
|
|
3
|
+
* point, ../../lexicons/temporal/src/op/activities/policy.ts) applies
|
|
4
|
+
* `lint.rules` severity overrides to policy diagnostics the same way `chant
|
|
5
|
+
* build` does (`../cli/commands/build.ts`), so a check `lint.rules` turns
|
|
6
|
+
* "off" no longer gates an apply either — before this fix, only `chant
|
|
7
|
+
* build`'s own error list was affected (and, before #1138, not even that).
|
|
8
|
+
*
|
|
9
|
+
* No lexicon is declared: the fixture project has no source files, so
|
|
10
|
+
* `resolveProjectLexicons` detects none and `build()` succeeds trivially with
|
|
11
|
+
* zero entities — the policy pack is the only thing under test.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
14
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { evaluateProjectPolicies } from "./policy";
|
|
18
|
+
|
|
19
|
+
describe("evaluateProjectPolicies honors lint.rules (chant #1138)", () => {
|
|
20
|
+
let testDir: string;
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
testDir = join(tmpdir(), `chant-policy-eval-test-${Date.now()}-${Math.random()}`);
|
|
24
|
+
await mkdir(join(testDir, "policies"), { recursive: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
await rm(testDir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
async function writePolicyProject(checkId: string, severity: "error" | "warning", rules: Record<string, string>): Promise<void> {
|
|
32
|
+
await writeFile(
|
|
33
|
+
join(testDir, "policies", "org.ts"),
|
|
34
|
+
`export const check = {\n` +
|
|
35
|
+
` id: ${JSON.stringify(checkId)},\n` +
|
|
36
|
+
` description: "test policy",\n` +
|
|
37
|
+
` check: () => [{ checkId: ${JSON.stringify(checkId)}, severity: ${JSON.stringify(severity)}, message: ${JSON.stringify(`${checkId} triggered`)} }],\n` +
|
|
38
|
+
`};\n`,
|
|
39
|
+
);
|
|
40
|
+
await writeFile(
|
|
41
|
+
join(testDir, "chant.config.ts"),
|
|
42
|
+
// `lexicons: ["k8s"]` avoids `resolveProjectLexicons`'s source-import
|
|
43
|
+
// auto-detection, which throws on a project with no lexicon-importing
|
|
44
|
+
// source file at all (this fixture has none — the policy pack is the
|
|
45
|
+
// only thing under test).
|
|
46
|
+
`export default { lexicons: ["k8s"], lint: { policies: ["policies/org.ts"], rules: ${JSON.stringify(rules)} } };\n`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test('lint.rules "off" removes a check from violations and reports it as suppressed', async () => {
|
|
51
|
+
await writePolicyProject("ORG-OFF", "error", { "ORG-OFF": "off" });
|
|
52
|
+
|
|
53
|
+
const evaluation = await evaluateProjectPolicies({ path: testDir });
|
|
54
|
+
|
|
55
|
+
expect(evaluation.violations).toEqual([]);
|
|
56
|
+
expect(evaluation.diagnostics).toEqual([]);
|
|
57
|
+
expect(evaluation.suppressed).toHaveLength(1);
|
|
58
|
+
expect(evaluation.suppressed[0].checkId).toBe("ORG-OFF");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('lint.rules "warning" downgrades an error-severity check out of violations', async () => {
|
|
62
|
+
await writePolicyProject("ORG-DOWN", "error", { "ORG-DOWN": "warning" });
|
|
63
|
+
|
|
64
|
+
const evaluation = await evaluateProjectPolicies({ path: testDir });
|
|
65
|
+
|
|
66
|
+
expect(evaluation.violations).toEqual([]);
|
|
67
|
+
expect(evaluation.diagnostics).toHaveLength(1);
|
|
68
|
+
expect(evaluation.diagnostics[0].severity).toBe("warning");
|
|
69
|
+
expect(evaluation.suppressed).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('lint.rules "error" upgrades a warning-severity check INTO violations', async () => {
|
|
73
|
+
await writePolicyProject("ORG-UP", "warning", { "ORG-UP": "error" });
|
|
74
|
+
|
|
75
|
+
const evaluation = await evaluateProjectPolicies({ path: testDir });
|
|
76
|
+
|
|
77
|
+
expect(evaluation.violations).toHaveLength(1);
|
|
78
|
+
expect(evaluation.violations[0].checkId).toBe("ORG-UP");
|
|
79
|
+
expect(evaluation.violations[0].severity).toBe("error");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("an unconfigured check id is unaffected — no drift for the common case", async () => {
|
|
83
|
+
await writePolicyProject("ORG-PLAIN", "error", {});
|
|
84
|
+
|
|
85
|
+
const evaluation = await evaluateProjectPolicies({ path: testDir });
|
|
86
|
+
|
|
87
|
+
expect(evaluation.violations).toHaveLength(1);
|
|
88
|
+
expect(evaluation.suppressed).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
});
|
package/src/lint/policy.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { resolveProjectLexicons, loadPlugins } from "../cli/plugins";
|
|
|
9
9
|
import { build } from "../build";
|
|
10
10
|
import { runPostSynthChecks, isPostSynthCheck } from "./post-synth";
|
|
11
11
|
import type { PostSynthCheck, PostSynthDiagnostic } from "./post-synth";
|
|
12
|
+
import { applyConfiguredSeverity } from "./config";
|
|
12
13
|
import { importPolicyModule, isSandboxPolicyExecutionArmed } from "./policy-import";
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -49,12 +50,18 @@ export async function loadPolicyChecks(paths: string[], configDir: string): Prom
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export interface PolicyEvaluation {
|
|
52
|
-
/** All policy diagnostics (errors + warnings). */
|
|
53
|
+
/** All policy diagnostics (errors + warnings), after `lint.rules` severity overrides. */
|
|
53
54
|
diagnostics: PostSynthDiagnostic[];
|
|
54
|
-
/** The error-severity subset — these are policy *violations* that gate. */
|
|
55
|
+
/** The error-severity subset (post-override) — these are policy *violations* that gate. */
|
|
55
56
|
violations: PostSynthDiagnostic[];
|
|
56
57
|
/** The environment policies were evaluated against (if any). */
|
|
57
58
|
env?: string;
|
|
59
|
+
/**
|
|
60
|
+
* chant #1138 — diagnostics `lint.rules` turned `"off"`, present here rather
|
|
61
|
+
* than dropped so a caller (`policyGate`, below) can report a count instead
|
|
62
|
+
* of the finding just vanishing.
|
|
63
|
+
*/
|
|
64
|
+
suppressed: PostSynthDiagnostic[];
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
/**
|
|
@@ -90,10 +97,15 @@ export async function evaluateProjectPolicies(opts: {
|
|
|
90
97
|
? await loadPolicyChecks(config.lint.policies, configDir)
|
|
91
98
|
: [];
|
|
92
99
|
if (checks.length === 0) {
|
|
93
|
-
return { diagnostics: [], violations: [], env };
|
|
100
|
+
return { diagnostics: [], violations: [], env, suppressed: [] };
|
|
94
101
|
}
|
|
95
102
|
|
|
96
|
-
const
|
|
103
|
+
const raw = runPostSynthChecks(checks, result, env);
|
|
104
|
+
// chant #1138 — same `lint.rules` resolution `chant build` applies
|
|
105
|
+
// (`../cli/commands/build.ts`), so a check `lint.rules` turned "off"/
|
|
106
|
+
// "warning" doesn't gate an apply here even though `chant build` no longer
|
|
107
|
+
// fails on it either, and vice versa for a check turned UP to "error".
|
|
108
|
+
const { diagnostics, suppressed } = applyConfiguredSeverity(raw, config.lint?.rules);
|
|
97
109
|
const violations = diagnostics.filter((d) => d.severity === "error");
|
|
98
|
-
return { diagnostics, violations, env };
|
|
110
|
+
return { diagnostics, violations, env, suppressed };
|
|
99
111
|
}
|
|
@@ -141,3 +141,7 @@ describe("isPostSynthCheck", () => {
|
|
|
141
141
|
expect(isPostSynthCheck("nope")).toBe(false);
|
|
142
142
|
});
|
|
143
143
|
});
|
|
144
|
+
|
|
145
|
+
// chant #1138 — `applyConfiguredSeverity` (the `lint.rules` severity-override
|
|
146
|
+
// pass over `PostSynthDiagnostic`s) is tested in `./config.test.ts`, where the
|
|
147
|
+
// function itself now lives — see `./config.ts`'s doc comment for why.
|
package/src/lint/post-synth.ts
CHANGED
|
@@ -35,6 +35,19 @@ export function getPrimaryOutput(output: string | SerializerResult): string {
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* A diagnostic from a post-synthesis check.
|
|
38
|
+
*
|
|
39
|
+
* chant #1138 — deliberately carries no `file`/`line` the way `LintDiagnostic`
|
|
40
|
+
* (`./rule.ts`) does. A post-synth check runs over `ctx.outputs` — the
|
|
41
|
+
* SYNTHESIZED output text (a CloudFormation template, a Kubernetes manifest) —
|
|
42
|
+
* not a `ts.SourceFile`, so there is no AST position to report in the first
|
|
43
|
+
* place. `entity` (below) is the closest thing to a locator and is NOT a
|
|
44
|
+
* substitute: it names a resource in that synthesized output (a CFN logical
|
|
45
|
+
* id, a k8s `metadata.name`), which several checks in this repo never even
|
|
46
|
+
* set (a cross-cutting check with no single implicated resource), and which
|
|
47
|
+
* is not guaranteed to match a `ctx.entities` map key. This is why source-
|
|
48
|
+
* comment (`chant-disable`) suppression is out of scope for post-synth
|
|
49
|
+
* findings — see `./config.ts`'s `applyConfiguredSeverity` doc for the full
|
|
50
|
+
* reasoning and what suppression surface post-synth findings get instead.
|
|
38
51
|
*/
|
|
39
52
|
export interface PostSynthDiagnostic {
|
|
40
53
|
/** ID of the check that produced this diagnostic */
|
|
@@ -43,7 +56,11 @@ export interface PostSynthDiagnostic {
|
|
|
43
56
|
severity: Severity;
|
|
44
57
|
/** Human-readable message */
|
|
45
58
|
message: string;
|
|
46
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Optional resource name related to this diagnostic — a name from the
|
|
61
|
+
* SYNTHESIZED OUTPUT (a CFN logical id, a k8s `metadata.name`), not a
|
|
62
|
+
* source file/line. See this interface's doc comment.
|
|
63
|
+
*/
|
|
47
64
|
entity?: string;
|
|
48
65
|
/** Optional lexicon related to this diagnostic */
|
|
49
66
|
lexicon?: string;
|
|
@@ -96,3 +113,15 @@ export function runPostSynthChecks(
|
|
|
96
113
|
}
|
|
97
114
|
return diagnostics;
|
|
98
115
|
}
|
|
116
|
+
|
|
117
|
+
// chant #1138 — `applyConfiguredSeverity` (the `lint.rules` severity-override
|
|
118
|
+
// pass over a set of `PostSynthDiagnostic`s) lives in `./config.ts`, not here,
|
|
119
|
+
// even though it operates on this module's own type. This file is a leaf:
|
|
120
|
+
// every lexicon's post-synth checks import it as a real runtime module (not
|
|
121
|
+
// just for types — `getPrimaryOutput` above is a plain function several
|
|
122
|
+
// checks call directly), so it has to stay cheap to load. `./config.ts` is
|
|
123
|
+
// not cheap — it resolves built-in preset paths via the runtime adapter at
|
|
124
|
+
// module scope — and pulling that into every lexicon's check barrel merely to
|
|
125
|
+
// share one filter function is the wrong trade. `applyConfiguredSeverity`
|
|
126
|
+
// only needs this module's TYPE (`PostSynthDiagnostic`), which costs nothing
|
|
127
|
+
// at runtime, so the dependency runs the other way instead.
|
package/src/stack-output.test.ts
CHANGED
|
@@ -41,9 +41,9 @@ describe("stackOutput", () => {
|
|
|
41
41
|
expect(out.description).toBe("Primary VPC id");
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
test("throws for a value that is neither AttrRef-like nor
|
|
45
|
-
expect(() => stackOutput(
|
|
46
|
-
"stackOutput(ref): ref must be an attribute reference
|
|
44
|
+
test("throws for a value that is neither AttrRef-like, Intrinsic, nor a string literal", () => {
|
|
45
|
+
expect(() => stackOutput(42 as unknown as AttrRef)).toThrow(
|
|
46
|
+
"stackOutput(ref): ref must be an attribute reference, an intrinsic wrapping one, or a literal string",
|
|
47
47
|
);
|
|
48
48
|
});
|
|
49
49
|
|
|
@@ -116,3 +116,21 @@ describe("isStackOutput", () => {
|
|
|
116
116
|
expect(isStackOutput(42)).toBe(false);
|
|
117
117
|
});
|
|
118
118
|
});
|
|
119
|
+
|
|
120
|
+
describe("stackOutput export names and literals", () => {
|
|
121
|
+
test("carries exportName through to the declaration", () => {
|
|
122
|
+
const ref = new AttrRef(vpc, "VpcId");
|
|
123
|
+
const out = stackOutput(ref, { exportName: "my-stack-VpcId" });
|
|
124
|
+
expect(out.exportName).toBe("my-stack-VpcId");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("accepts a literal string with an explicit lexicon", () => {
|
|
128
|
+
const out = stackOutput("22", { lexicon: "aws", exportName: "my-stack-OpenSSHPort" });
|
|
129
|
+
expect(out.sourceRef).toBe("22");
|
|
130
|
+
expect(out.lexicon).toBe("aws");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("rejects a literal without a lexicon", () => {
|
|
134
|
+
expect(() => stackOutput("22")).toThrow(/options\.lexicon/);
|
|
135
|
+
});
|
|
136
|
+
});
|
package/src/stack-output.ts
CHANGED
|
@@ -26,10 +26,13 @@ export interface StackOutput extends Declarable {
|
|
|
26
26
|
readonly lexicon: string;
|
|
27
27
|
readonly entityType: string;
|
|
28
28
|
readonly kind: "output";
|
|
29
|
-
/** The exported value: a bare attribute reference,
|
|
30
|
-
* one (e.g. `Join(",", zone.NameServers)`). */
|
|
31
|
-
readonly sourceRef: AttrRef | Intrinsic;
|
|
29
|
+
/** The exported value: a bare attribute reference, an intrinsic wrapping
|
|
30
|
+
* one (e.g. `Join(",", zone.NameServers)`), or a literal string. */
|
|
31
|
+
readonly sourceRef: AttrRef | Intrinsic | string;
|
|
32
32
|
readonly description?: string;
|
|
33
|
+
/** When set, the serializer emits a cross-stack export under this name
|
|
34
|
+
* (CloudFormation `Output.Export.Name`) in addition to the plain output. */
|
|
35
|
+
readonly exportName?: string;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/** Find the first AttrRef anywhere inside a value (walking intrinsics/objects/
|
|
@@ -82,8 +85,8 @@ export function isStackOutput(value: unknown): value is StackOutput {
|
|
|
82
85
|
* ```
|
|
83
86
|
*/
|
|
84
87
|
export function stackOutput(
|
|
85
|
-
ref: AttrRef | Intrinsic,
|
|
86
|
-
options?: { description?: string },
|
|
88
|
+
ref: AttrRef | Intrinsic | string,
|
|
89
|
+
options?: { description?: string; exportName?: string; lexicon?: string },
|
|
87
90
|
): StackOutput {
|
|
88
91
|
// Duck-type, not `instanceof` (chant #1137): AttrRef also implements
|
|
89
92
|
// Intrinsic (a global-symbol marker), so `isIntrinsic(ref)` alone already
|
|
@@ -92,19 +95,27 @@ export function stackOutput(
|
|
|
92
95
|
// so the guard's own logic states the invariant explicitly ("ref must be
|
|
93
96
|
// AttrRef-like or Intrinsic") rather than relying on that coincidence,
|
|
94
97
|
// matching the anchor selection right below it, which does misbehave.
|
|
95
|
-
if (!isAttrRefLike(ref) && !isIntrinsic(ref)) {
|
|
96
|
-
throw new Error(
|
|
98
|
+
if (typeof ref !== "string" && !isAttrRefLike(ref) && !isIntrinsic(ref)) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
"stackOutput(ref): ref must be an attribute reference, an intrinsic wrapping one, or a literal string",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (typeof ref === "string" && !options?.lexicon) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
"stackOutput(literal): a literal output has no entity to derive its lexicon from — pass options.lexicon",
|
|
106
|
+
);
|
|
97
107
|
}
|
|
98
108
|
// Derive lexicon from the referenced entity — for a bare AttrRef, its parent;
|
|
99
109
|
// for an intrinsic (Join etc.), the first AttrRef nested inside it. A
|
|
100
110
|
// foreign-copy AttrRef failing raw `instanceof` here would fall to
|
|
101
111
|
// `firstAttrRef`, which (before its own #1137 fix) would also miss it —
|
|
102
112
|
// silently anchoring on nothing and recording `lexicon: "unknown"`.
|
|
103
|
-
const anchor = isAttrRefLike(ref) ? ref : firstAttrRef(ref);
|
|
113
|
+
const anchor = typeof ref === "string" ? undefined : isAttrRefLike(ref) ? ref : firstAttrRef(ref);
|
|
104
114
|
const parent = anchor?.parent.deref();
|
|
105
|
-
const
|
|
115
|
+
const derived = parent && typeof (parent as Record<string, unknown>).lexicon === "string"
|
|
106
116
|
? (parent as Record<string, unknown>).lexicon as string
|
|
107
117
|
: "unknown";
|
|
118
|
+
const lexicon = options?.lexicon ?? derived;
|
|
108
119
|
|
|
109
120
|
const output: StackOutput = {
|
|
110
121
|
[STACK_OUTPUT_MARKER]: true,
|
|
@@ -114,6 +125,7 @@ export function stackOutput(
|
|
|
114
125
|
kind: "output",
|
|
115
126
|
sourceRef: ref,
|
|
116
127
|
description: options?.description,
|
|
128
|
+
exportName: options?.exportName,
|
|
117
129
|
};
|
|
118
130
|
|
|
119
131
|
return output;
|