@intentius/chant 0.22.0 → 0.24.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/build-params-cli.d.ts +55 -0
- package/dist/cli/build-params-cli.d.ts.map +1 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts +22 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/lsp/server.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +8 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +33 -2
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts +28 -0
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/config.d.ts +18 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +38 -8
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/index.d.ts +15 -4
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +16 -2
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/lint/config.d.ts +1 -12
- package/dist/lint/config.d.ts.map +1 -1
- package/dist/lint/engine.d.ts +11 -1
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lint/rule.d.ts +14 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/project-root.d.ts +51 -0
- package/dist/project-root.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/cli/build-params-cli.test.ts +139 -0
- package/src/cli/build-params-cli.ts +107 -0
- package/src/cli/commands/build.ts +44 -41
- package/src/cli/commands/lint.test.ts +74 -0
- package/src/cli/commands/lint.ts +33 -9
- package/src/cli/handlers/build.test.ts +149 -0
- package/src/cli/handlers/build.ts +28 -8
- package/src/cli/handlers/run.test.ts +191 -5
- package/src/cli/handlers/run.ts +61 -8
- package/src/cli/lsp/server.ts +7 -2
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +17 -3
- package/src/cli/plugins.ts +10 -2
- package/src/components/cli-support.test.ts +221 -3
- package/src/components/cli-support.ts +37 -6
- package/src/components/discover.test.ts +63 -1
- package/src/components/discover.ts +42 -0
- package/src/config.ts +23 -0
- package/src/discovery/fold-import.ts +202 -20
- package/src/discovery/index.test.ts +131 -0
- package/src/discovery/index.ts +38 -8
- package/src/discovery/sandbox/fold-boundary.test.ts +254 -0
- package/src/fold/subset.test.ts +28 -14
- package/src/fold/subset.ts +16 -2
- package/src/lint/config.test.ts +9 -4
- package/src/lint/config.ts +7 -23
- package/src/lint/engine.ts +12 -0
- package/src/lint/policy.ts +5 -5
- package/src/lint/rule.ts +14 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +39 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +1 -1
- package/src/project-root.test.ts +105 -0
- package/src/project-root.ts +78 -0
package/dist/lint/rule.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type * as ts from "typescript";
|
|
2
|
+
import type { IntrinsicDef } from "../lexicon.js";
|
|
2
3
|
/**
|
|
3
4
|
* Severity level for lint diagnostics
|
|
4
5
|
*/
|
|
@@ -55,6 +56,19 @@ export interface LintContext {
|
|
|
55
56
|
filePath: string;
|
|
56
57
|
/** Optional lexicon context (undefined for core rules) */
|
|
57
58
|
lexicon?: string;
|
|
59
|
+
/**
|
|
60
|
+
* chant #1106 — the active lexicons' registered intrinsics (`Ref`,
|
|
61
|
+
* `GetAtt`, ...), threaded down from `runLint` (../lint/engine.ts) so a
|
|
62
|
+
* rule built on the shared `../fold/subset.ts` predicate
|
|
63
|
+
* (`findSubsetViolation`/`checkObjectMember`, used by EVL001) gets the
|
|
64
|
+
* SAME answer `fold()` does for a registered, opted-in call. Mirrors how
|
|
65
|
+
* `discover()` has threaded `IntrinsicDef[]` into the fold path since
|
|
66
|
+
* #1039/#1105. Undefined when the caller hasn't resolved a project's
|
|
67
|
+
* lexicons (a bare unit test constructing a `LintContext` directly, for
|
|
68
|
+
* instance) — subset.ts then falls back to its pre-#1044 answer: every
|
|
69
|
+
* call is a violation.
|
|
70
|
+
*/
|
|
71
|
+
intrinsics?: readonly IntrinsicDef[];
|
|
58
72
|
}
|
|
59
73
|
/**
|
|
60
74
|
* Configuration value for a rule: either a severity string or a [severity, options] tuple
|
package/dist/lint/rule.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rule.d.ts","sourceRoot":"","sources":["../../src/lint/rule.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"rule.d.ts","sourceRoot":"","sources":["../../src/lint/rule.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEpD;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,aAAa,GAAG,OAAO,GAAG,aAAa,GAAG,UAAU,CAAC;AAE5E;;GAEG;AACH,MAAM,WAAW,OAAO;IACtB,gDAAgD;IAChD,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxB,qCAAqC;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,IAAI,CAAC,EAAE,SAAS,GAAG,eAAe,GAAG,cAAc,GAAG,QAAQ,GAAG,YAAY,CAAC;IAC9E,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,EAAE,QAAQ,CAAC;IACnB,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,oCAAoC;IACpC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC;IAC1B,sCAAsC;IACtC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;CACtC;AAED;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,KAAK,GAAG,CAAC,QAAQ,GAAG,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAExF;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,sCAAsC;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,oDAAoD;IACpD,QAAQ,EAAE,QAAQ,CAAC;IACnB,4BAA4B;IAC5B,QAAQ,EAAE,QAAQ,CAAC;IACnB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,KAAK,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,cAAc,EAAE,CAAC;IACjF,gDAAgD;IAChD,GAAG,CAAC,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,EAAE,CAAC;CACvC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared upward config-discovery walk (chant #1117).
|
|
3
|
+
*
|
|
4
|
+
* Before this, `chant build <subdir>` and `lint.policies`
|
|
5
|
+
* (`./lint/policy.ts`'s `evaluateProjectPolicies`) each searched only the
|
|
6
|
+
* build directory and its immediate parent for `chant.config.ts`/`.json`,
|
|
7
|
+
* while `chant lint`/`chant graph` (`./lint/config.ts`'s old, file-local
|
|
8
|
+
* `findProjectRoot`) already walked all the way up. A project with a deeper
|
|
9
|
+
* `src/<stack>` layout — `chant build src/<stack>` two or more levels below
|
|
10
|
+
* the project root — silently never found the root config: `buildParams`'
|
|
11
|
+
* declared `env:` mappings went inert, `ownership`/`lint.policies`/etc quietly
|
|
12
|
+
* fell back to defaults, and nothing warned (loomster#162: `LOOM_TIER`/
|
|
13
|
+
* `LOOM_ENV` inert under every `npm run synth:*` for two releases).
|
|
14
|
+
*
|
|
15
|
+
* `findProjectConfig` is the one walk every config-discovery call site now
|
|
16
|
+
* shares. It stops at the first of:
|
|
17
|
+
*
|
|
18
|
+
* 1. A directory holding `chant.config.ts` or `chant.config.json` — found.
|
|
19
|
+
* 2. A directory holding `.git` or `package.json` with no chant config of its
|
|
20
|
+
* own — the project boundary. Discovery must never wander past the actual
|
|
21
|
+
* project into an unrelated ancestor directory just because this project
|
|
22
|
+
* happens not to declare a config (a stray `chant.config.ts` two levels
|
|
23
|
+
* above an unrelated git repo must never be picked up).
|
|
24
|
+
* 3. `startDir` itself, unchanged, if the walk reaches the filesystem root
|
|
25
|
+
* without ever finding a config OR a boundary marker. This is not just a
|
|
26
|
+
* "give up gracefully" nicety — several callers (`resolveProjectLexicons`
|
|
27
|
+
* -> `findInfraFiles`) scope a real directory walk off this function's
|
|
28
|
+
* result; if a rootless/marker-less start dir (a bare tmpdir, as chant's
|
|
29
|
+
* own test suites use) resolved all the way to `/`, that downstream walk
|
|
30
|
+
* would scan the entire filesystem instead of failing fast. Falling back
|
|
31
|
+
* to `startDir` keeps every caller's blast radius local no matter how far
|
|
32
|
+
* up the walk had to look.
|
|
33
|
+
*/
|
|
34
|
+
export interface ProjectConfigSearch {
|
|
35
|
+
/** The resolved project root: the config's directory, the boundary directory, or the (resolved) `startDir` when neither was found. */
|
|
36
|
+
dir: string;
|
|
37
|
+
/** Absolute path to the discovered `chant.config.ts`/`.json`, if any. */
|
|
38
|
+
configPath?: string;
|
|
39
|
+
}
|
|
40
|
+
/** Walk up from `startDir` (inclusive) to the nearest chant config or project boundary. See {@link ProjectConfigSearch}. */
|
|
41
|
+
export declare function findProjectConfig(startDir: string): ProjectConfigSearch;
|
|
42
|
+
/**
|
|
43
|
+
* Walk up from `startDir` to the nearest ancestor holding a chant project
|
|
44
|
+
* config (`chant.config.ts` or `chant.config.json`), the `.git`/`package.json`
|
|
45
|
+
* project boundary, or `startDir` itself when neither is found. Thin wrapper
|
|
46
|
+
* over {@link findProjectConfig} for callers that only need the directory
|
|
47
|
+
* (e.g. `chant lint`'s rule/plugin resolution, which just needs *a* stable
|
|
48
|
+
* root to resolve relative paths against).
|
|
49
|
+
*/
|
|
50
|
+
export declare function findProjectRoot(startDir: string): string;
|
|
51
|
+
//# sourceMappingURL=project-root.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-root.d.ts","sourceRoot":"","sources":["../src/project-root.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,WAAW,mBAAmB;IAClC,sIAAsI;IACtI,GAAG,EAAE,MAAM,CAAC;IACZ,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,4HAA4H;AAC5H,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAqBvE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAExD"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the CLI-layer build-time-parameter helpers factored out by chant
|
|
3
|
+
* #1108 (`parseParamFlags`/`resolveCliBuildParams`) — the exact sequence
|
|
4
|
+
* `chant build` (`./commands/build.ts`'s `buildCommand`) runs, now shared with
|
|
5
|
+
* the component deploy driver (`./handlers/run.ts`, `./handlers/build.ts`'s
|
|
6
|
+
* generate mode) so both resolve `--param`/`--params-file`/a declared `env`
|
|
7
|
+
* mapping/`chant.config.ts`'s `buildParams` defaults identically. Precedence
|
|
8
|
+
* itself is `../build-params.ts`'s `resolveBuildParams`'s responsibility
|
|
9
|
+
* (see `../build-params.test.ts`) — these tests cover the CLI-specific glue:
|
|
10
|
+
* flag parsing, `--params-file` reading, error formatting, and logging.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, test, expect, vi } from "vitest";
|
|
13
|
+
import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { parseParamFlags, resolveCliBuildParams } from "./build-params-cli";
|
|
17
|
+
import type { BuildParamsConfig } from "../build-params";
|
|
18
|
+
|
|
19
|
+
describe("parseParamFlags", () => {
|
|
20
|
+
test("undefined/empty input yields undefined (matches resolveBuildParams's 'no cli input' shape)", () => {
|
|
21
|
+
expect(parseParamFlags(undefined)).toBeUndefined();
|
|
22
|
+
expect(parseParamFlags([])).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("parses repeated name=value flags into a flat record", () => {
|
|
26
|
+
expect(parseParamFlags(["tier=production", "replicas=3"])).toEqual({
|
|
27
|
+
tier: "production",
|
|
28
|
+
replicas: "3",
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("a value containing '=' keeps everything after the first '=' (only the first splits)", () => {
|
|
33
|
+
expect(parseParamFlags(["url=https://example.com?a=b"])).toEqual({
|
|
34
|
+
url: "https://example.com?a=b",
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("a flag with no '=' becomes an empty-string value", () => {
|
|
39
|
+
expect(parseParamFlags(["flag-only"])).toEqual({ "flag-only": "" });
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("resolveCliBuildParams", () => {
|
|
44
|
+
test("cli beats params-file beats a declared env mapping beats the default — the exact chant build precedence", () => {
|
|
45
|
+
const defs: BuildParamsConfig = { tier: { type: "string", default: "light", env: "TIER_ENV" } };
|
|
46
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
47
|
+
try {
|
|
48
|
+
const result = resolveCliBuildParams(defs, { cli: { tier: "production" } });
|
|
49
|
+
expect(result).toEqual({
|
|
50
|
+
success: true,
|
|
51
|
+
provenance: [{ name: "tier", value: "production", source: "cli" }],
|
|
52
|
+
errors: [],
|
|
53
|
+
});
|
|
54
|
+
} finally {
|
|
55
|
+
errorSpy.mockRestore();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("logs every resolved parameter as '[param] name = value (source)'", () => {
|
|
60
|
+
const defs: BuildParamsConfig = { tier: { type: "string", default: "light" } };
|
|
61
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
62
|
+
try {
|
|
63
|
+
resolveCliBuildParams(defs, {});
|
|
64
|
+
const logged = errorSpy.mock.calls.map((call) => String(call[0]));
|
|
65
|
+
expect(logged.some((line) => line.includes("[param] tier") && line.includes("light") && line.includes("default"))).toBe(true);
|
|
66
|
+
} finally {
|
|
67
|
+
errorSpy.mockRestore();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("no declared params → success with empty provenance and no logging", () => {
|
|
72
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
73
|
+
try {
|
|
74
|
+
const result = resolveCliBuildParams(undefined, {});
|
|
75
|
+
expect(result).toEqual({ success: true, provenance: [], errors: [] });
|
|
76
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
77
|
+
} finally {
|
|
78
|
+
errorSpy.mockRestore();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("an unresolved required parameter is a formatted error, not a thrown exception", () => {
|
|
83
|
+
const defs: BuildParamsConfig = { tier: { type: "string" } };
|
|
84
|
+
const result = resolveCliBuildParams(defs, {});
|
|
85
|
+
expect(result.success).toBe(false);
|
|
86
|
+
expect(result.provenance).toEqual([]);
|
|
87
|
+
expect(result.errors.some((e) => e.includes('"tier"') && e.includes("--param"))).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("an enum violation is a formatted error naming the parameter and the offending value", () => {
|
|
91
|
+
const defs: BuildParamsConfig = { tier: { type: "string", enum: ["light", "production"] } };
|
|
92
|
+
const result = resolveCliBuildParams(defs, { cli: { tier: "bogus" } });
|
|
93
|
+
expect(result.success).toBe(false);
|
|
94
|
+
expect(result.errors.some((e) => e.includes('"tier"') && e.includes("bogus"))).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("an unknown --param name is a formatted error", () => {
|
|
98
|
+
const defs: BuildParamsConfig = { tier: { type: "string", default: "light" } };
|
|
99
|
+
const result = resolveCliBuildParams(defs, { cli: { nonexistent: "x" } });
|
|
100
|
+
expect(result.success).toBe(false);
|
|
101
|
+
expect(result.errors.some((e) => e.includes("nonexistent") && e.includes("--param"))).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("--params-file", () => {
|
|
105
|
+
let dir: string;
|
|
106
|
+
|
|
107
|
+
test("reads and resolves values from a JSON file (second precedence, after --param)", () => {
|
|
108
|
+
dir = mkdtempSync(join(tmpdir(), "chant-build-params-cli-test-"));
|
|
109
|
+
try {
|
|
110
|
+
const file = join(dir, "params.json");
|
|
111
|
+
writeFileSync(file, JSON.stringify({ tier: "from-file", env: "from-file-env" }));
|
|
112
|
+
const defs: BuildParamsConfig = {
|
|
113
|
+
tier: { type: "string", default: "light" },
|
|
114
|
+
env: { type: "string", default: "dev" },
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
118
|
+
try {
|
|
119
|
+
const result = resolveCliBuildParams(defs, { cli: { tier: "from-cli" }, paramsFile: file });
|
|
120
|
+
expect(result.success).toBe(true);
|
|
121
|
+
const byName = new Map(result.provenance.map((p) => [p.name, p]));
|
|
122
|
+
expect(byName.get("tier")).toEqual({ name: "tier", value: "from-cli", source: "cli" });
|
|
123
|
+
expect(byName.get("env")).toEqual({ name: "env", value: "from-file-env", source: "params-file" });
|
|
124
|
+
} finally {
|
|
125
|
+
errorSpy.mockRestore();
|
|
126
|
+
}
|
|
127
|
+
} finally {
|
|
128
|
+
rmSync(dir, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("an unreadable/unparsable --params-file is a formatted error naming the path, not a thrown exception", () => {
|
|
133
|
+
const defs: BuildParamsConfig = { tier: { type: "string", default: "light" } };
|
|
134
|
+
const result = resolveCliBuildParams(defs, { paramsFile: "/nonexistent/path/params.json" });
|
|
135
|
+
expect(result.success).toBe(false);
|
|
136
|
+
expect(result.errors.some((e) => e.includes("--params-file") && e.includes("/nonexistent/path/params.json"))).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { resolveBuildParams, type BuildParamsConfig } from "../build-params";
|
|
4
|
+
import type { BuildParamProvenance } from "../provenance";
|
|
5
|
+
import { formatError, formatInfo } from "./format";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Shared CLI-layer wiring for chant #1064's build-time parameters, factored
|
|
9
|
+
* out (chant #1108) so every command that discovers project source before
|
|
10
|
+
* running it resolves `chant.config.ts`'s declared `buildParams` the exact
|
|
11
|
+
* same way: `chant build` (`../commands/build.ts`'s `buildCommand`), the
|
|
12
|
+
* component deploy driver (`./handlers/run.ts`'s `chant run --components`
|
|
13
|
+
* local + `--temporal` paths), and generate mode (`./handlers/build.ts`'s
|
|
14
|
+
* `chant build --components --generate <lexicon>`).
|
|
15
|
+
*
|
|
16
|
+
* Before #1108, only `buildCommand` ran this sequence — `chant run
|
|
17
|
+
* --components` never resolved `--param`/`--params-file`/a declared `env`
|
|
18
|
+
* mapping at all, so a `*.component.ts` file reading `params.<name>`
|
|
19
|
+
* (`@intentius/chant/params`) always saw `{}`, no matter what a CI job
|
|
20
|
+
* exported into the environment. See ../build-params.ts's module doc for the
|
|
21
|
+
* full precedence rules this wraps.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Parse repeated `--param name=value` flags into a flat `{ name: value }` record — the raw (unvalidated) strings {@link resolveCliBuildParams} resolves against a project's declared `buildParams`. `undefined` when no `--param` flag was given, matching `resolveBuildParams`'s "no cli input" shape. */
|
|
25
|
+
export function parseParamFlags(entries?: string[]): Record<string, string> | undefined {
|
|
26
|
+
if (!entries?.length) return undefined;
|
|
27
|
+
return Object.fromEntries(
|
|
28
|
+
entries.map((entry) => {
|
|
29
|
+
const eq = entry.indexOf("=");
|
|
30
|
+
return eq === -1 ? [entry, ""] : [entry.slice(0, eq), entry.slice(eq + 1)];
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Inputs {@link resolveCliBuildParams} needs from a parsed CLI invocation — the `--param`/`--params-file` half of {@link resolveBuildParams}'s `BuildParamsInput` (the `env` half is always `process.env`, resolved internally). */
|
|
36
|
+
export interface CliBuildParamsArgs {
|
|
37
|
+
/** Already-parsed `--param name=value` flags (see {@link parseParamFlags}), or the CLI's already-typed `Record<string,string>` (`chant build`'s own flag parsing does this itself — see `./handlers/build.ts`). */
|
|
38
|
+
cli?: Record<string, string>;
|
|
39
|
+
/** `--params-file <path>` — a JSON file of `{ "name": value }` values, read and parsed here. */
|
|
40
|
+
paramsFile?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CliBuildParamsResolution {
|
|
44
|
+
success: boolean;
|
|
45
|
+
/** Every successfully resolved parameter. Empty when the project declares none, or when `success` is `false`. */
|
|
46
|
+
provenance: BuildParamProvenance[];
|
|
47
|
+
/** `formatError`-wrapped messages, ready to print as-is. Empty when `success` is `true`. */
|
|
48
|
+
errors: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve this invocation's declared build-time parameters (`chant.config.ts`'s
|
|
53
|
+
* `buildParams`) against `args`/the process environment, and log each
|
|
54
|
+
* resolved value — the identical resolution + logging sequence `chant build`
|
|
55
|
+
* runs (`../commands/build.ts`'s `buildCommand`), factored out so every other
|
|
56
|
+
* command that discovers project source runs it too (chant #1108).
|
|
57
|
+
*
|
|
58
|
+
* A resolution failure (an unknown `--param`/`--params-file` name, a missing
|
|
59
|
+
* required value, a type/enum mismatch, or an unreadable `--params-file`) is
|
|
60
|
+
* returned as `{ success: false, errors }` — never thrown — so the caller can
|
|
61
|
+
* print each message and exit non-zero exactly like a build error, matching
|
|
62
|
+
* chant #1064's acceptance criterion that this never surfaces as a thrown
|
|
63
|
+
* error from inside user source.
|
|
64
|
+
*
|
|
65
|
+
* On success, every resolved parameter is logged via `console.error` as
|
|
66
|
+
* `[param] <name> = <value> (<source>)` — unconditional, not gated on
|
|
67
|
+
* `--verbose`, so a build's environment-varying inputs are always visible,
|
|
68
|
+
* the same way #1022's fold decisions are.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveCliBuildParams(
|
|
71
|
+
buildParamsConfig: BuildParamsConfig | undefined,
|
|
72
|
+
args: CliBuildParamsArgs,
|
|
73
|
+
): CliBuildParamsResolution {
|
|
74
|
+
const errors: string[] = [];
|
|
75
|
+
|
|
76
|
+
let fromFile: Record<string, unknown> | undefined;
|
|
77
|
+
if (args.paramsFile) {
|
|
78
|
+
try {
|
|
79
|
+
fromFile = JSON.parse(readFileSync(resolve(args.paramsFile), "utf-8"));
|
|
80
|
+
} catch (err) {
|
|
81
|
+
errors.push(
|
|
82
|
+
formatError({
|
|
83
|
+
message: `Failed to read/parse --params-file "${args.paramsFile}": ${err instanceof Error ? err.message : String(err)}`,
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const resolution = resolveBuildParams(buildParamsConfig, {
|
|
90
|
+
cli: args.cli,
|
|
91
|
+
fromFile,
|
|
92
|
+
env: process.env,
|
|
93
|
+
});
|
|
94
|
+
for (const message of resolution.errors) {
|
|
95
|
+
errors.push(formatError({ message }));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (errors.length > 0) {
|
|
99
|
+
return { success: false, provenance: [], errors };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const p of resolution.provenance) {
|
|
103
|
+
console.error(formatInfo(`[param] ${p.name} = ${JSON.stringify(p.value)} (${p.source})`));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { success: true, provenance: resolution.provenance, errors: [] };
|
|
107
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { build } from "../../build";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { loadChantConfigUpward, resolveOwnershipMarker, resolveFoldEnabled, resolveSandboxEnabled } from "../../config";
|
|
3
|
+
import { resolveCliBuildParams } from "../build-params-cli";
|
|
4
4
|
import type { Serializer, SerializerResult } from "../../serializer";
|
|
5
5
|
import type { LexiconPlugin } from "../../lexicon";
|
|
6
6
|
import { runPostSynthChecks } from "../../lint/post-synth";
|
|
7
7
|
import { loadPolicyChecks } from "../../lint/policy";
|
|
8
8
|
import { sortedJsonReplacer } from "../../utils";
|
|
9
9
|
import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
|
|
10
|
-
import { writeFileSync, mkdirSync
|
|
10
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
11
11
|
import { resolve, dirname, join, relative } from "path";
|
|
12
12
|
import { watchDirectory, formatTimestamp, formatChangedFiles } from "../watch";
|
|
13
13
|
|
|
@@ -128,11 +128,14 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
128
128
|
// Resolve the path
|
|
129
129
|
const infraPath = resolve(options.path);
|
|
130
130
|
|
|
131
|
-
// Resolve opt-in ownership marking from project config
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
// Resolve opt-in ownership marking from project config. chant #1117 — walks
|
|
132
|
+
// up from the infra dir to the project root (`loadChantConfigUpward`), not
|
|
133
|
+
// just the infra dir's immediate parent: a project whose stacks live two or
|
|
134
|
+
// more levels below `chant.config.ts` (loomster's `src/<stack>` layout)
|
|
135
|
+
// otherwise never finds the root config at all, and every declared
|
|
136
|
+
// `buildParams`/`ownership`/`lint.policies` setting silently falls back to
|
|
137
|
+
// its default.
|
|
138
|
+
const loaded = await loadChantConfigUpward(infraPath);
|
|
136
139
|
const config = loaded.config;
|
|
137
140
|
const ownership = resolveOwnershipMarker(config);
|
|
138
141
|
|
|
@@ -155,43 +158,43 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
155
158
|
// as fold, resolved independently.
|
|
156
159
|
const sandbox = resolveSandboxEnabled(config, options.sandbox);
|
|
157
160
|
|
|
158
|
-
// #1064
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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, {
|
|
161
|
+
// #1064 (factored into ../build-params-cli.ts's resolveCliBuildParams by
|
|
162
|
+
// #1108, so the component deploy driver runs the identical sequence) —
|
|
163
|
+
// resolve declared build-time parameters (chant.config.ts's buildParams)
|
|
164
|
+
// against this invocation's --param/--params-file/declared env mapping,
|
|
165
|
+
// BEFORE calling build() — a resolution failure (an unknown name, a
|
|
166
|
+
// missing required value, a type/enum mismatch) is reported as a chant
|
|
167
|
+
// build error naming the parameter, never a thrown error from inside user
|
|
168
|
+
// source (which is what loomster's hand-rolled `tierFromEnv()`-style
|
|
169
|
+
// validators did before migrating to this mechanism). Also logs every
|
|
170
|
+
// resolved value (`[param] name = value (source)`) on success.
|
|
171
|
+
const paramsResolution = resolveCliBuildParams(config.buildParams, {
|
|
178
172
|
cli: options.params,
|
|
179
|
-
|
|
180
|
-
env: process.env,
|
|
173
|
+
paramsFile: options.paramsFile,
|
|
181
174
|
});
|
|
182
|
-
|
|
183
|
-
errors.push(
|
|
184
|
-
}
|
|
185
|
-
if (errors.length > 0) {
|
|
175
|
+
if (!paramsResolution.success) {
|
|
176
|
+
errors.push(...paramsResolution.errors);
|
|
186
177
|
return { success: false, resourceCount: 0, fileCount: 0, errors, warnings };
|
|
187
178
|
}
|
|
188
|
-
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
179
|
+
|
|
180
|
+
// chant #1117 — a project that declares buildParams but resolves NONE of
|
|
181
|
+
// them for this build is the exact shape that let loomster#162 live for two
|
|
182
|
+
// releases: the discovered config wasn't the one the project author
|
|
183
|
+
// expected (a stale --path, a workspace boundary that stopped the walk
|
|
184
|
+
// short), or every declared parameter's `env:` var went unset, and either
|
|
185
|
+
// way every `params.<name>` read silently falls back to `undefined` — with
|
|
186
|
+
// no error (an all-`required: false` declaration resolves successfully to
|
|
187
|
+
// an empty set). Warn, naming the config path this build actually
|
|
188
|
+
// discovered, so a mismatch is visible instead of silent.
|
|
189
|
+
if (
|
|
190
|
+
Object.keys(config.buildParams ?? {}).length > 0 &&
|
|
191
|
+
paramsResolution.provenance.length === 0
|
|
192
|
+
) {
|
|
193
|
+
warnings.push(
|
|
194
|
+
formatWarning({
|
|
195
|
+
message: `chant.config.ts declares buildParams${loaded.configPath ? ` (${loaded.configPath})` : ""}, but none resolved for this build — every params.<name> read will be undefined. Pass --param/--params-file, or check that this is the config you expect.`,
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
195
198
|
}
|
|
196
199
|
|
|
197
200
|
// #1039 — thread each loaded plugin's registered intrinsics (e.g. AWS's
|
|
@@ -332,6 +332,80 @@ describe("plugin integration", () => {
|
|
|
332
332
|
});
|
|
333
333
|
});
|
|
334
334
|
|
|
335
|
+
/**
|
|
336
|
+
* chant #1106 — `runLint`'s EVL rules now receive the active lexicons'
|
|
337
|
+
* registered intrinsics (threaded from `loadAllPluginRules`'s
|
|
338
|
+
* `plugin.intrinsics?.()`, mirroring how `../commands/build.ts` gathers the
|
|
339
|
+
* same set for the fold path). Before this, `chant lint` flagged
|
|
340
|
+
* `Ref(...)` — a registered, opted-in call-form intrinsic (aws's
|
|
341
|
+
* `lexicons/aws/src/plugin.ts`) that `chant build --fold` folds cleanly —
|
|
342
|
+
* as EVL001, purely because EVL001's shared `../../fold/subset.ts`
|
|
343
|
+
* predicate never saw the registry. These use the real aws lexicon plugin
|
|
344
|
+
* (already a workspace dependency) rather than a mock, so the registration
|
|
345
|
+
* this asserts against is the one that actually ships.
|
|
346
|
+
*/
|
|
347
|
+
describe("lintCommand — EVL/intrinsic registry convergence (#1106)", () => {
|
|
348
|
+
let testDir: string;
|
|
349
|
+
|
|
350
|
+
beforeEach(async () => {
|
|
351
|
+
testDir = join(tmpdir(), `chant-lint-intrinsics-test-${Date.now()}-${Math.random()}`);
|
|
352
|
+
await mkdir(testDir, { recursive: true });
|
|
353
|
+
process.env.NO_COLOR = "1";
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
afterEach(async () => {
|
|
357
|
+
await rm(testDir, { recursive: true, force: true });
|
|
358
|
+
delete process.env.NO_COLOR;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("a registered, opted-in intrinsic call (aws's Ref) is not flagged as EVL001", async () => {
|
|
362
|
+
await writeFile(join(testDir, "chant.config.json"), JSON.stringify({ lexicons: ["aws"] }));
|
|
363
|
+
await writeFile(
|
|
364
|
+
join(testDir, "index.ts"),
|
|
365
|
+
`
|
|
366
|
+
import { Ref } from "@intentius/chant-lexicon-aws";
|
|
367
|
+
|
|
368
|
+
class Queue {
|
|
369
|
+
constructor(_props: Record<string, unknown>) {}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export const environment = "prod";
|
|
373
|
+
export const queue = new Queue({ name: Ref(environment) });
|
|
374
|
+
`,
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
const result = await lintCommand({ path: testDir, format: "stylish" });
|
|
378
|
+
|
|
379
|
+
expect(result.diagnostics.filter((d) => d.ruleId === "EVL001")).toHaveLength(0);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("an unregistered call is still flagged as EVL001 in the same project", async () => {
|
|
383
|
+
await writeFile(join(testDir, "chant.config.json"), JSON.stringify({ lexicons: ["aws"] }));
|
|
384
|
+
await writeFile(
|
|
385
|
+
join(testDir, "index.ts"),
|
|
386
|
+
`
|
|
387
|
+
import { Ref } from "@intentius/chant-lexicon-aws";
|
|
388
|
+
|
|
389
|
+
class Queue {
|
|
390
|
+
constructor(_props: Record<string, unknown>) {}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function makeName(): string {
|
|
394
|
+
return "generated";
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export const queue = new Queue({ name: makeName() });
|
|
398
|
+
`,
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const result = await lintCommand({ path: testDir, format: "stylish" });
|
|
402
|
+
|
|
403
|
+
const evl001 = result.diagnostics.filter((d) => d.ruleId === "EVL001");
|
|
404
|
+
expect(evl001).toHaveLength(1);
|
|
405
|
+
expect(evl001[0].message).toContain("statically evaluable");
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
335
409
|
describe("lintCommand — project-root config resolution (scoped lint)", () => {
|
|
336
410
|
let testDir: string;
|
|
337
411
|
|