@intentius/chant 0.44.13 → 0.45.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/audit/discover.d.ts +26 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +10 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/cli/build-params-cli.d.ts +16 -7
- package/dist/cli/build-params-cli.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +20 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +9 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-docs.d.ts +13 -0
- package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +24 -14
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts +5 -0
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts +28 -0
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts +27 -0
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/codegen/docs-pages.d.ts +39 -0
- package/dist/codegen/docs-pages.d.ts.map +1 -0
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/codegen/docs-sidebar.d.ts +13 -2
- package/dist/codegen/docs-sidebar.d.ts.map +1 -1
- package/dist/codegen/docs-types.d.ts +30 -10
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts +14 -2
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/surface-snapshot.d.ts +27 -3
- package/dist/codegen/surface-snapshot.d.ts.map +1 -1
- package/dist/codegen/validate.d.ts +11 -3
- package/dist/codegen/validate.d.ts.map +1 -1
- package/dist/components/capability-plugin.d.ts +13 -2
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/driver.d.ts +38 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/config.d.ts +57 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +100 -1
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +12 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ops.d.ts +17 -0
- package/dist/graph-ops.d.ts.map +1 -0
- package/dist/lifecycle/change-set.d.ts +23 -2
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +8 -0
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/replay.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/types.d.ts +7 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lint/policy.d.ts.map +1 -1
- package/dist/managed-fields.d.ts +33 -21
- package/dist/managed-fields.d.ts.map +1 -1
- package/dist/observation.d.ts +23 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/yaml.d.ts +0 -8
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__snapshots__/okf.test.ts.snap +20 -0
- package/src/audit/discover.test.ts +55 -1
- package/src/audit/discover.ts +70 -2
- package/src/audit/edge-init-safety.test.ts +149 -18
- package/src/audit/report-model.ts +12 -1
- package/src/audit/rules-doc.ts +1 -0
- package/src/build-params.test.ts +52 -0
- package/src/cli/build-params-cli.test.ts +25 -8
- package/src/cli/build-params-cli.ts +37 -23
- package/src/cli/commands/__fixtures__/audit-coverage/.github/workflows/ci.yml +5 -0
- package/src/cli/commands/__fixtures__/audit-coverage/Dockerfile +2 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/main.tf +3 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/stack.json +6 -0
- package/src/cli/commands/__fixtures__/audit-coverage/k8s/deploy.yaml +12 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/pages/getting-started.mdx +16 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +10 -2
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +3 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +2 -1
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +2 -1
- package/src/cli/commands/audit.test.ts +83 -1
- package/src/cli/commands/audit.ts +121 -28
- package/src/cli/commands/build.test.ts +144 -5
- package/src/cli/commands/build.ts +66 -14
- package/src/cli/commands/check-lexicon-docs.ts +30 -0
- package/src/cli/commands/check-lexicon-examples.test.ts +73 -7
- package/src/cli/commands/check-lexicon-examples.ts +66 -17
- package/src/cli/commands/check-lexicon.test.ts +1 -1
- package/src/cli/commands/check-lexicon.ts +19 -4
- package/src/cli/commands/init-lexicon/templates/codegen.ts +5 -2
- package/src/cli/commands/init-lexicon/templates/docs.ts +35 -2
- package/src/cli/commands/init-lexicon.test.ts +42 -1
- package/src/cli/commands/init-lexicon.ts +3 -1
- package/src/cli/commands/lexicon-rolling-upgrade.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.test.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.ts +1 -1
- package/src/cli/commands/onboard.test.ts +319 -225
- package/src/cli/commands/onboard.ts +144 -58
- package/src/cli/handlers/build.test.ts +3 -3
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.ts +2 -2
- package/src/cli/handlers/graph.test.ts +44 -0
- package/src/cli/handlers/graph.ts +22 -2
- package/src/cli/handlers/lifecycle.test.ts +117 -0
- package/src/cli/handlers/lifecycle.ts +16 -0
- package/src/cli/handlers/run.test.ts +5 -5
- package/src/cli/handlers/run.ts +1 -0
- package/src/cli/handlers/search-live.test.ts +217 -0
- package/src/cli/handlers/search.test.ts +41 -1
- package/src/cli/handlers/search.ts +55 -6
- package/src/cli/main.ts +5 -3
- package/src/cli/security-boundary.test.ts +2 -1
- package/src/codegen/docs-pages.test.ts +194 -0
- package/src/codegen/docs-pages.ts +138 -0
- package/src/codegen/docs-sections.ts +0 -8
- package/src/codegen/docs-sidebar.ts +50 -37
- package/src/codegen/docs-types.ts +32 -4
- package/src/codegen/docs.ts +78 -40
- package/src/codegen/lexicon-regen.ts +3 -1
- package/src/codegen/rolling-upgrade.test.ts +1 -1
- package/src/codegen/rolling-upgrade.ts +1 -1
- package/src/codegen/surface-snapshot.test.ts +139 -0
- package/src/codegen/surface-snapshot.ts +297 -122
- package/src/codegen/validate.test.ts +16 -3
- package/src/codegen/validate.ts +17 -7
- package/src/components/auto-release.ts +1 -1
- package/src/components/capability-plugin.ts +23 -2
- package/src/components/driver.test.ts +45 -0
- package/src/components/driver.ts +64 -27
- package/src/components/index.ts +2 -0
- package/src/components/starter-plugin.ts +5 -2
- package/src/config.test.ts +56 -1
- package/src/config.ts +152 -8
- package/src/discovery/fold-import.test.ts +427 -2
- package/src/discovery/fold-import.ts +163 -7
- package/src/discovery/sandbox/driver.test.ts +11 -0
- package/src/fold/fold.test.ts +41 -0
- package/src/fold/fold.ts +372 -2
- package/src/graph-ir-live.test.ts +42 -0
- package/src/graph-ir.ts +42 -2
- package/src/graph-ops.test.ts +69 -0
- package/src/graph-ops.ts +42 -0
- package/src/lifecycle/change-set.test.ts +41 -0
- package/src/lifecycle/change-set.ts +30 -2
- package/src/lifecycle/deep-observe.ts +4 -0
- package/src/lifecycle/observe.ts +29 -2
- package/src/lifecycle/replay.ts +3 -0
- package/src/lifecycle/snapshot.ts +3 -0
- package/src/lifecycle/types.ts +7 -0
- package/src/lint/policy.ts +14 -3
- package/src/managed-fields.test.ts +15 -42
- package/src/managed-fields.ts +43 -26
- package/src/observation.test.ts +24 -8
- package/src/observation.ts +48 -4
- package/src/op/builders-exports.test.ts +60 -0
- package/src/op/index.ts +1 -1
- package/src/ownership.test.ts +63 -1
- package/src/yaml.test.ts +15 -0
- package/src/yaml.ts +19 -2
package/src/audit/discover.ts
CHANGED
|
@@ -243,6 +243,63 @@ export function isCandidatePath(path: string): boolean {
|
|
|
243
243
|
return /\.(ya?ml|json|template)$/i.test(name);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
/**
|
|
247
|
+
* What a candidate file looks like it wants, judged cheaply by core alone
|
|
248
|
+
* (path, filename, a few content markers) with no lexicon plugin involved.
|
|
249
|
+
* Names the lexicon that would have claimed a file when that lexicon isn't
|
|
250
|
+
* installed (#1623). Deliberately coarse: precision comes from the plugin's
|
|
251
|
+
* `detectTemplate`, which is exactly what's missing in that case.
|
|
252
|
+
* `terraform` is not an audit lexicon; `*.tf` is surfaced so the user learns
|
|
253
|
+
* the audit never reads it (see `chant carve`).
|
|
254
|
+
*/
|
|
255
|
+
export type LexiconHint = AuditLexicon | "terraform";
|
|
256
|
+
|
|
257
|
+
export function hintLexiconForFile(path: string, content: string): LexiconHint | undefined {
|
|
258
|
+
const name = basename(path);
|
|
259
|
+
const ci = ciLexiconForPath(path);
|
|
260
|
+
if (ci) return ci;
|
|
261
|
+
if (isDockerfileName(name)) return "docker";
|
|
262
|
+
if (name === "Chart.yaml") return "helm";
|
|
263
|
+
if (/\.tf$/i.test(name)) return "terraform";
|
|
264
|
+
const head = content.slice(0, 64 * 1024);
|
|
265
|
+
if (/cnrm\.cloud\.google\.com/.test(head)) return "gcp";
|
|
266
|
+
if (/AWSTemplateFormatVersion|["']?Type["']?\s*:\s*["']AWS::/.test(head)) return "aws";
|
|
267
|
+
if (/deploymentTemplate\.json/.test(head)) return "azure";
|
|
268
|
+
if (isYaml(name)) {
|
|
269
|
+
if (/^apiVersion:/m.test(head) && /^kind:/m.test(head)) return "k8s";
|
|
270
|
+
if (/^services:/m.test(head)) return "docker";
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** A candidate file that looked like it belonged to a lexicon the audit did not have. */
|
|
276
|
+
export interface UnclaimedFile {
|
|
277
|
+
path: string;
|
|
278
|
+
lexicon: LexiconHint;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Files no loaded lexicon claimed, paired with the lexicon core guesses they
|
|
283
|
+
* wanted. Only files whose guessed lexicon is absent are reported; a file an
|
|
284
|
+
* installed lexicon declined is that plugin's call, not a coverage gap.
|
|
285
|
+
*/
|
|
286
|
+
export function unclaimedFiles(files: RepoFile[], inputs: AuditInput[], plugins: DetectPlugin[]): UnclaimedFile[] {
|
|
287
|
+
const loaded = new Set<string>(plugins.map((p) => p.name));
|
|
288
|
+
const claimed = new Set<string>();
|
|
289
|
+
for (const i of inputs) {
|
|
290
|
+
if (i.files) for (const rel of Object.keys(i.files)) claimed.add(i.path === "." ? rel : `${i.path}/${rel}`);
|
|
291
|
+
else claimed.add(i.path);
|
|
292
|
+
}
|
|
293
|
+
const out: UnclaimedFile[] = [];
|
|
294
|
+
for (const f of files) {
|
|
295
|
+
if (claimed.has(f.path)) continue;
|
|
296
|
+
const lexicon = hintLexiconForFile(f.path, f.content);
|
|
297
|
+
if (!lexicon || loaded.has(lexicon)) continue;
|
|
298
|
+
out.push({ path: f.path, lexicon });
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
|
|
246
303
|
/** Collect Helm charts as bundles from an in-memory file set; returns inputs + chart path-prefixes. */
|
|
247
304
|
function classifyHelm(files: RepoFile[], plugin: DetectPlugin | undefined): { inputs: AuditInput[]; prefixes: string[] } {
|
|
248
305
|
const inputs: AuditInput[] = [];
|
|
@@ -315,15 +372,26 @@ export function classifyFiles(files: RepoFile[], plugins: DetectPlugin[]): Audit
|
|
|
315
372
|
* then handed to the shared `classifyFiles`. Detection is skipped for any
|
|
316
373
|
* lexicon whose plugin isn't provided, so a caller can scope discovery.
|
|
317
374
|
*/
|
|
318
|
-
export function discoverByDetection(root: string, plugins:
|
|
375
|
+
export function discoverByDetection(root: string, plugins: DetectPlugin[]): AuditInput[] {
|
|
376
|
+
return classifyFiles(collectCandidates(root), plugins);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** The walk half of `discoverByDetection`: candidate files read into memory, paths relative to the root. */
|
|
380
|
+
export function collectCandidates(root: string): RepoFile[] {
|
|
319
381
|
const all: string[] = [];
|
|
320
382
|
walkFiles(root, all);
|
|
321
383
|
const files: RepoFile[] = [];
|
|
322
384
|
for (const full of all) {
|
|
323
385
|
const path = relative(root, full);
|
|
386
|
+
// Terraform is never audited; the path alone is enough to say so (#1623).
|
|
387
|
+
// Kept out of `isCandidatePath` so remote fetches never pull HCL.
|
|
388
|
+
if (/\.tf$/i.test(path)) {
|
|
389
|
+
files.push({ path, content: "" });
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
324
392
|
if (!isCandidatePath(path)) continue;
|
|
325
393
|
const content = readSafe(full);
|
|
326
394
|
if (content !== undefined) files.push({ path, content });
|
|
327
395
|
}
|
|
328
|
-
return
|
|
396
|
+
return files;
|
|
329
397
|
}
|
|
@@ -1,43 +1,174 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
|
-
import { readdirSync, readFileSync, existsSync } from "fs";
|
|
2
|
+
import { readdirSync, readFileSync, existsSync, statSync } from "fs";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
|
-
import { join } from "path";
|
|
4
|
+
import { join, dirname, resolve } from "path";
|
|
5
|
+
import ts from "typescript";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* #428: the lexicon modules the hosted service imports on the edge —
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* #428 / #1621: the lexicon modules the hosted service imports on the edge —
|
|
9
|
+
* the post-synth barrel and the `detect` module, plus everything they reach
|
|
10
|
+
* through static imports — must not build filesystem paths at module load.
|
|
11
|
+
* On workerd `import.meta.url` is undefined and `__dirname`, `require` and
|
|
12
|
+
* `process.cwd()` do not exist, so a module-scope
|
|
13
|
+
* `dirname(fileURLToPath(import.meta.url))` crashes the worker at startup.
|
|
14
|
+
* Anything that genuinely needs a path must compute it lazily, inside a
|
|
15
|
+
* function, behind a try.
|
|
16
|
+
*
|
|
17
|
+
* The old guard only grepped the post-synth files themselves for
|
|
18
|
+
* `createRequire`. azure's `deploy-scopes.ts` (reached from azr030) used
|
|
19
|
+
* `fileURLToPath` and slipped through. This version walks the transitive
|
|
20
|
+
* relative-import graph of every barrel with the TypeScript parser and flags
|
|
21
|
+
* the whole family of constructions at module scope.
|
|
12
22
|
*/
|
|
13
23
|
const lexiconsDir = fileURLToPath(new URL("../../../../lexicons", import.meta.url));
|
|
14
24
|
|
|
15
|
-
function
|
|
25
|
+
function barrels(): string[] {
|
|
16
26
|
const out: string[] = [];
|
|
17
27
|
for (const lex of readdirSync(lexiconsDir)) {
|
|
18
28
|
const detect = join(lexiconsDir, lex, "src", "detect.ts");
|
|
19
29
|
if (existsSync(detect)) out.push(detect);
|
|
20
30
|
const ps = join(lexiconsDir, lex, "src", "lint", "post-synth");
|
|
21
31
|
if (existsSync(ps)) {
|
|
32
|
+
const index = join(ps, "index.ts");
|
|
33
|
+
if (existsSync(index)) out.push(index);
|
|
34
|
+
// Checks not re-exported from the barrel are still edge-imported
|
|
35
|
+
// individually by the hosted service, so keep them as roots too.
|
|
22
36
|
for (const f of readdirSync(ps)) {
|
|
23
|
-
if (f.endsWith(".ts") && !f.endsWith(".test.ts")) out.push(join(ps, f));
|
|
37
|
+
if (f.endsWith(".ts") && !f.endsWith(".test.ts") && f !== "index.ts") out.push(join(ps, f));
|
|
24
38
|
}
|
|
25
39
|
}
|
|
26
40
|
}
|
|
27
41
|
return out;
|
|
28
42
|
}
|
|
29
43
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
function parse(file: string): ts.SourceFile {
|
|
45
|
+
return ts.createSourceFile(file, readFileSync(file, "utf-8"), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Resolve a relative specifier to a .ts file the way the lexicon tsconfigs do. */
|
|
49
|
+
function resolveRelative(from: string, spec: string): string | undefined {
|
|
50
|
+
const base = resolve(dirname(from), spec.replace(/\.js$/, ""));
|
|
51
|
+
for (const candidate of [`${base}.ts`, `${base}.tsx`, join(base, "index.ts")]) {
|
|
52
|
+
if (existsSync(candidate) && statSync(candidate).isFile()) return candidate;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Static (non-type-only) imports and re-exports of a file, resolved to paths. */
|
|
58
|
+
function staticImports(sf: ts.SourceFile): string[] {
|
|
59
|
+
const out: string[] = [];
|
|
60
|
+
for (const stmt of sf.statements) {
|
|
61
|
+
let spec: string | undefined;
|
|
62
|
+
if (ts.isImportDeclaration(stmt)) {
|
|
63
|
+
if (stmt.importClause?.isTypeOnly) continue;
|
|
64
|
+
spec = (stmt.moduleSpecifier as ts.StringLiteral).text;
|
|
65
|
+
} else if (ts.isExportDeclaration(stmt) && stmt.moduleSpecifier) {
|
|
66
|
+
if (stmt.isTypeOnly) continue;
|
|
67
|
+
spec = (stmt.moduleSpecifier as ts.StringLiteral).text;
|
|
68
|
+
}
|
|
69
|
+
if (!spec || !spec.startsWith(".")) continue;
|
|
70
|
+
const resolved = resolveRelative(sf.fileName, spec);
|
|
71
|
+
if (resolved) out.push(resolved);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Every file reachable from the roots through static relative imports. */
|
|
77
|
+
function transitiveClosure(roots: string[]): Map<string, ts.SourceFile> {
|
|
78
|
+
const seen = new Map<string, ts.SourceFile>();
|
|
79
|
+
const queue = [...roots];
|
|
80
|
+
while (queue.length) {
|
|
81
|
+
const file = queue.pop()!;
|
|
82
|
+
if (seen.has(file)) continue;
|
|
83
|
+
const sf = parse(file);
|
|
84
|
+
seen.set(file, sf);
|
|
85
|
+
for (const dep of staticImports(sf)) if (!seen.has(dep)) queue.push(dep);
|
|
86
|
+
}
|
|
87
|
+
return seen;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isImportMetaUrl(node: ts.Node): boolean {
|
|
91
|
+
return (
|
|
92
|
+
ts.isPropertyAccessExpression(node) &&
|
|
93
|
+
node.name.text === "url" &&
|
|
94
|
+
ts.isMetaProperty(node.expression) &&
|
|
95
|
+
node.expression.keywordToken === ts.SyntaxKind.ImportKeyword
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function containsImportMetaUrl(node: ts.Node): boolean {
|
|
100
|
+
if (isImportMetaUrl(node)) return true;
|
|
101
|
+
return ts.forEachChild(node, containsImportMetaUrl) ?? false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Describe a module-scope path construction, or undefined if `node` is benign. */
|
|
105
|
+
function offense(node: ts.Node): string | undefined {
|
|
106
|
+
if (ts.isIdentifier(node) && (node.text === "__dirname" || node.text === "__filename")) {
|
|
107
|
+
// `const __dirname_ = ...` is a declaration, not a use of the Node global.
|
|
108
|
+
if (ts.isVariableDeclaration(node.parent) && node.parent.name === node) return undefined;
|
|
109
|
+
return node.text;
|
|
110
|
+
}
|
|
111
|
+
if (ts.isCallExpression(node)) {
|
|
112
|
+
const callee = node.expression;
|
|
113
|
+
if (ts.isIdentifier(callee)) {
|
|
114
|
+
if (callee.text === "require") return "require(...)";
|
|
115
|
+
if ((callee.text === "fileURLToPath" || callee.text === "createRequire") && node.arguments.some(containsImportMetaUrl)) {
|
|
116
|
+
return `${callee.text}(import.meta.url)`;
|
|
39
117
|
}
|
|
40
118
|
}
|
|
119
|
+
if (
|
|
120
|
+
ts.isPropertyAccessExpression(callee) &&
|
|
121
|
+
ts.isIdentifier(callee.expression) &&
|
|
122
|
+
callee.expression.text === "process" &&
|
|
123
|
+
callee.name.text === "cwd"
|
|
124
|
+
) {
|
|
125
|
+
return "process.cwd()";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "URL" && node.arguments?.some(containsImportMetaUrl)) {
|
|
129
|
+
return "new URL(..., import.meta.url)";
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Bodies that run later than module evaluation. */
|
|
135
|
+
function defersEvaluation(node: ts.Node): boolean {
|
|
136
|
+
return (
|
|
137
|
+
ts.isFunctionDeclaration(node) ||
|
|
138
|
+
ts.isFunctionExpression(node) ||
|
|
139
|
+
ts.isArrowFunction(node) ||
|
|
140
|
+
ts.isMethodDeclaration(node) ||
|
|
141
|
+
ts.isConstructorDeclaration(node) ||
|
|
142
|
+
ts.isGetAccessorDeclaration(node) ||
|
|
143
|
+
ts.isSetAccessorDeclaration(node) ||
|
|
144
|
+
ts.isPropertyDeclaration(node)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function moduleScopeOffenses(sf: ts.SourceFile): string[] {
|
|
149
|
+
const hits: string[] = [];
|
|
150
|
+
const visit = (node: ts.Node): void => {
|
|
151
|
+
if (defersEvaluation(node)) return;
|
|
152
|
+
const what = offense(node);
|
|
153
|
+
if (what) {
|
|
154
|
+
const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
|
|
155
|
+
hits.push(`${sf.fileName.replace(lexiconsDir, "lexicons")}:${line + 1}: ${what}`);
|
|
156
|
+
}
|
|
157
|
+
ts.forEachChild(node, visit);
|
|
158
|
+
};
|
|
159
|
+
ts.forEachChild(sf, visit);
|
|
160
|
+
return hits;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
describe("edge-imported lexicon modules are init-safe", () => {
|
|
164
|
+
test("reaches helpers through the barrel import graph", () => {
|
|
165
|
+
const files = [...transitiveClosure(barrels()).keys()];
|
|
166
|
+
expect(files).toContain(join(lexiconsDir, "azure", "src", "deploy-scopes.ts"));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("no module-scope filesystem-path constructions in the transitive import graph", () => {
|
|
170
|
+
const offenders: string[] = [];
|
|
171
|
+
for (const sf of transitiveClosure(barrels()).values()) offenders.push(...moduleScopeOffenses(sf));
|
|
41
172
|
expect(offenders, "move these into a function (lazy) — they crash edge bundles").toEqual([]);
|
|
42
173
|
});
|
|
43
174
|
});
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { AuditFinding } from "./core";
|
|
9
|
+
import type { UnclaimedFile } from "./discover";
|
|
9
10
|
import { RULE_CATALOG, ruleDocUrl, type Authority, type Category, type FixKind, type RuleMeta, type Tier } from "./catalog";
|
|
10
11
|
import { proveFix, unifiedDiff, type ProveOptions } from "./proof";
|
|
11
12
|
import type { Severity } from "../lint/rule";
|
|
@@ -107,9 +108,17 @@ export interface SerializedFinding {
|
|
|
107
108
|
export interface AuditReportJson {
|
|
108
109
|
schemaVersion: string;
|
|
109
110
|
tool: { name: string; version: string };
|
|
111
|
+
/**
|
|
112
|
+
* Whether the audit had lexicons to look with (#1623). `"ok"` when at least
|
|
113
|
+
* one audit lexicon resolved; `"no-lexicons"` means nothing was inspected and
|
|
114
|
+
* the report carries no findings by construction.
|
|
115
|
+
*/
|
|
116
|
+
status: "ok" | "no-lexicons";
|
|
110
117
|
snapshot?: AuditSnapshot;
|
|
111
118
|
summary: ReportCounts;
|
|
112
119
|
findings: SerializedFinding[];
|
|
120
|
+
/** Candidate files that looked like they wanted a lexicon that is not installed. */
|
|
121
|
+
unclaimed?: UnclaimedFile[];
|
|
113
122
|
}
|
|
114
123
|
|
|
115
124
|
export function metaFor(id: string, catalog: Record<string, RuleMeta> = RULE_CATALOG): RuleMeta {
|
|
@@ -270,13 +279,14 @@ export function buildReportModel(findings: AuditFinding[], opts: BuildModelOptio
|
|
|
270
279
|
/** Build the versioned, machine-readable JSON report (stable contract). */
|
|
271
280
|
export function buildReportJson(
|
|
272
281
|
findings: AuditFinding[],
|
|
273
|
-
opts: { snapshot?: AuditSnapshot; toolVersion?: string; catalog?: Record<string, RuleMeta
|
|
282
|
+
opts: { snapshot?: AuditSnapshot; toolVersion?: string; catalog?: Record<string, RuleMeta>; unclaimed?: UnclaimedFile[] } = {},
|
|
274
283
|
): AuditReportJson {
|
|
275
284
|
const model = buildReportModel(findings, { catalog: opts.catalog });
|
|
276
285
|
const version = opts.toolVersion ?? opts.snapshot?.toolVersion ?? "0.0.0";
|
|
277
286
|
return {
|
|
278
287
|
schemaVersion: REPORT_SCHEMA_VERSION,
|
|
279
288
|
tool: { name: "chant-audit", version },
|
|
289
|
+
status: "ok",
|
|
280
290
|
snapshot: opts.snapshot,
|
|
281
291
|
summary: model.counts,
|
|
282
292
|
findings: model.findings.map((f) => ({
|
|
@@ -294,5 +304,6 @@ export function buildReportJson(
|
|
|
294
304
|
authority: f.meta.authority ?? [],
|
|
295
305
|
docUrl: ruleDocUrl(f.checkId),
|
|
296
306
|
})),
|
|
307
|
+
unclaimed: opts.unclaimed && opts.unclaimed.length > 0 ? opts.unclaimed : undefined,
|
|
297
308
|
};
|
|
298
309
|
}
|
package/src/audit/rules-doc.ts
CHANGED
|
@@ -49,6 +49,7 @@ export async function renderRulesReference(): Promise<string> {
|
|
|
49
49
|
return `---
|
|
50
50
|
title: Audit rules reference
|
|
51
51
|
description: Every rule chant audit can report, with its tier, fix kind, and remediation.
|
|
52
|
+
diataxis: reference
|
|
52
53
|
---
|
|
53
54
|
|
|
54
55
|
This is the reference for every rule [\`chant audit\`](/chant/cli/audit/) can report. Each finding in a report links to its rule here.
|
package/src/build-params.test.ts
CHANGED
|
@@ -129,6 +129,58 @@ describe("resolveBuildParams — validation", () => {
|
|
|
129
129
|
});
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
describe("resolveBuildParams — unset optional parameters (#1371)", () => {
|
|
133
|
+
// An unset optional parameter must read as `undefined` from `params.<name>`
|
|
134
|
+
// — never `null`. `null` is a value ("explicitly nothing"); leaving a
|
|
135
|
+
// parameter unset is not that statement, and a `null` that reaches a
|
|
136
|
+
// composite ships as `key: null` in the output.
|
|
137
|
+
test("unset optional with a declared default resolves to the default", () => {
|
|
138
|
+
const defs: BuildParamsConfig = {
|
|
139
|
+
baseImageArn: { type: "string", required: false, env: "KMV_BASE_IMAGE_ARN", default: "arn:base" },
|
|
140
|
+
};
|
|
141
|
+
const result = resolveBuildParams(defs, { env: {} });
|
|
142
|
+
expect(result.errors).toEqual([]);
|
|
143
|
+
expect(buildParamValues(result.provenance)).toEqual({ baseImageArn: "arn:base" });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("unset optional without a default is absent — reads as undefined, never null", () => {
|
|
147
|
+
const defs: BuildParamsConfig = { baseImageArn: { type: "string", required: false, env: "KMV_BASE_IMAGE_ARN" } };
|
|
148
|
+
const result = resolveBuildParams(defs, { env: {} });
|
|
149
|
+
expect(result.errors).toEqual([]);
|
|
150
|
+
const values = buildParamValues(result.provenance);
|
|
151
|
+
expect("baseImageArn" in values).toBe(false);
|
|
152
|
+
expect(values.baseImageArn).toBeUndefined();
|
|
153
|
+
expect(values.baseImageArn ?? "fallback").toBe("fallback");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("an env var declared but not exported is the same as unset", () => {
|
|
157
|
+
const defs: BuildParamsConfig = { baseImageArn: { type: "string", required: false, env: "KMV_BASE_IMAGE_ARN" } };
|
|
158
|
+
const result = resolveBuildParams(defs, { env: { KMV_BASE_IMAGE_ARN: undefined } });
|
|
159
|
+
expect(result.errors).toEqual([]);
|
|
160
|
+
expect(result.provenance).toEqual([]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("an explicit empty string is a supplied value, not unset", () => {
|
|
164
|
+
const defs: BuildParamsConfig = { baseImageArn: { type: "string", required: false, default: "arn:base" } };
|
|
165
|
+
const fromCli = resolveBuildParams(defs, { cli: { baseImageArn: "" } });
|
|
166
|
+
expect(fromCli.errors).toEqual([]);
|
|
167
|
+
expect(fromCli.provenance).toEqual([{ name: "baseImageArn", value: "", source: "cli" }]);
|
|
168
|
+
|
|
169
|
+
const fromEnv = resolveBuildParams({ baseImageArn: { type: "string", required: false, env: "X" } }, { env: { X: "" } });
|
|
170
|
+
expect(fromEnv.errors).toEqual([]);
|
|
171
|
+
expect(fromEnv.provenance).toEqual([{ name: "baseImageArn", value: "", source: "env" }]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("a null in --params-file is rejected, never passed through as a value", () => {
|
|
175
|
+
const defs: BuildParamsConfig = { baseImageArn: { type: "string", required: false } };
|
|
176
|
+
const result = resolveBuildParams(defs, { fromFile: { baseImageArn: null } });
|
|
177
|
+
expect(result.errors).toEqual([
|
|
178
|
+
'build parameter "baseImageArn" (from --params-file) must be a string, number, or boolean',
|
|
179
|
+
]);
|
|
180
|
+
expect(result.provenance).toEqual([]);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
132
184
|
describe("buildParamValues", () => {
|
|
133
185
|
test("projects provenance records down to a plain value map", () => {
|
|
134
186
|
const provenance = [
|
|
@@ -56,16 +56,16 @@ describe("resolveCliBuildParams", () => {
|
|
|
56
56
|
}
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
test("a build with no overrides collapses to a single
|
|
59
|
+
test("a build with no overrides collapses to a single count line (#1424)", () => {
|
|
60
60
|
// The provenance still records every value; the echo just stops naming
|
|
61
|
-
//
|
|
62
|
-
// reached us".
|
|
61
|
+
// them. One line, so silence still never means "no parameters reached us".
|
|
63
62
|
const defs: BuildParamsConfig = { tier: { type: "string", default: "light" } };
|
|
64
63
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
65
64
|
try {
|
|
66
65
|
const result = resolveCliBuildParams(defs, {});
|
|
67
66
|
const logged = errorSpy.mock.calls.map((call) => String(call[0]));
|
|
68
|
-
expect(logged
|
|
67
|
+
expect(logged).toHaveLength(1);
|
|
68
|
+
expect(logged[0]).toContain("1 build parameter resolved (1 default); --verbose to list");
|
|
69
69
|
expect(logged.some((line) => line.includes("[param] tier"))).toBe(false);
|
|
70
70
|
expect(result.provenance).toEqual([{ name: "tier", value: "light", source: "default" }]);
|
|
71
71
|
} finally {
|
|
@@ -73,7 +73,7 @@ describe("resolveCliBuildParams", () => {
|
|
|
73
73
|
}
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
test("
|
|
76
|
+
test("the count line breaks down by source, so an override's arrival is still visible", () => {
|
|
77
77
|
const defs: BuildParamsConfig = {
|
|
78
78
|
tier: { type: "string", default: "light" },
|
|
79
79
|
target: { type: "string", default: "k3d" },
|
|
@@ -83,9 +83,26 @@ describe("resolveCliBuildParams", () => {
|
|
|
83
83
|
try {
|
|
84
84
|
resolveCliBuildParams(defs, { cli: { tier: "ha" } });
|
|
85
85
|
const logged = errorSpy.mock.calls.map((call) => String(call[0]));
|
|
86
|
+
expect(logged).toHaveLength(1);
|
|
87
|
+
expect(logged[0]).toContain("3 build parameters resolved (1 from cli, 2 default); --verbose to list");
|
|
88
|
+
expect(logged.some((line) => line.includes("[param]"))).toBe(false);
|
|
89
|
+
} finally {
|
|
90
|
+
errorSpy.mockRestore();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("--verbose lists every parameter with its source", () => {
|
|
95
|
+
const defs: BuildParamsConfig = {
|
|
96
|
+
tier: { type: "string", default: "light" },
|
|
97
|
+
target: { type: "string", default: "k3d" },
|
|
98
|
+
};
|
|
99
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
100
|
+
try {
|
|
101
|
+
resolveCliBuildParams(defs, { cli: { tier: "ha" }, verbose: true });
|
|
102
|
+
const logged = errorSpy.mock.calls.map((call) => String(call[0]));
|
|
86
103
|
expect(logged.some((line) => line.includes("[param] tier") && line.includes("ha") && line.includes("cli"))).toBe(true);
|
|
87
|
-
expect(logged.some((line) => line.includes("
|
|
88
|
-
expect(logged.some((line) => line.includes("
|
|
104
|
+
expect(logged.some((line) => line.includes("[param] target") && line.includes("k3d") && line.includes("default"))).toBe(true);
|
|
105
|
+
expect(logged.some((line) => line.includes("--verbose to list"))).toBe(false);
|
|
89
106
|
} finally {
|
|
90
107
|
errorSpy.mockRestore();
|
|
91
108
|
}
|
|
@@ -104,7 +121,7 @@ describe("resolveCliBuildParams", () => {
|
|
|
104
121
|
const logged = errorSpy.mock.calls.map((call) => String(call[0]));
|
|
105
122
|
expect(logged.some((line) => line.includes("[param] tier") && line.includes("ha"))).toBe(true);
|
|
106
123
|
expect(logged.some((line) => line.includes("[param] target") && line.includes("k3d") && line.includes("default"))).toBe(true);
|
|
107
|
-
expect(logged.some((line) => line.includes("
|
|
124
|
+
expect(logged.some((line) => line.includes("--verbose to list"))).toBe(false);
|
|
108
125
|
} finally {
|
|
109
126
|
if (prev === undefined) delete process.env.CHANT_PARAM_ECHO;
|
|
110
127
|
else process.env.CHANT_PARAM_ECHO = prev;
|
|
@@ -38,6 +38,8 @@ export interface CliBuildParamsArgs {
|
|
|
38
38
|
cli?: Record<string, string>;
|
|
39
39
|
/** `--params-file <path>` — a JSON file of `{ "name": value }` values, read and parsed here. */
|
|
40
40
|
paramsFile?: string;
|
|
41
|
+
/** `--verbose` — list every resolved parameter one per line instead of the one-line count (#1424). */
|
|
42
|
+
verbose?: boolean;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
export interface CliBuildParamsResolution {
|
|
@@ -62,13 +64,12 @@ export interface CliBuildParamsResolution {
|
|
|
62
64
|
* chant #1064's acceptance criterion that this never surfaces as a thrown
|
|
63
65
|
* error from inside user source.
|
|
64
66
|
*
|
|
65
|
-
* On success,
|
|
66
|
-
* `
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* never varies, which is exactly why they can be a count.
|
|
67
|
+
* On success, the resolution is summarized on one `console.error` line —
|
|
68
|
+
* `N build parameters resolved (a default, b from cli, ...); --verbose to
|
|
69
|
+
* list` — so silence never means "no parameters reached us", and the count
|
|
70
|
+
* by source says whether an override arrived. Under `--verbose` (or
|
|
71
|
+
* `CHANT_PARAM_ECHO=all`) every parameter is listed as
|
|
72
|
+
* `[param] <name> = <value> (<source>)` instead (#1424).
|
|
72
73
|
*/
|
|
73
74
|
export function resolveCliBuildParams(
|
|
74
75
|
buildParamsConfig: BuildParamsConfig | undefined,
|
|
@@ -102,27 +103,40 @@ export function resolveCliBuildParams(
|
|
|
102
103
|
return { success: false, provenance: [], errors };
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
console.error(formatInfo(
|
|
117
|
-
}
|
|
118
|
-
if (!echoAll && defaulted > 0) {
|
|
119
|
-
const more = explicit.length > 0 ? `${defaulted} more` : `${defaulted} parameters`;
|
|
120
|
-
console.error(formatInfo(`[param] ${more} at their defaults (CHANT_PARAM_ECHO=all lists them)`));
|
|
106
|
+
// #1424 — one line by default. A project with two dozen declared
|
|
107
|
+
// parameters printed two dozen grey lines on every build, ahead of the
|
|
108
|
+
// warnings the reader actually needed. The count by source still answers
|
|
109
|
+
// "did my override reach the build" (the failure mode
|
|
110
|
+
// warnIfParamsCannotReachProject exists for); --verbose names every value.
|
|
111
|
+
const verbose = args.verbose === true || process.env.CHANT_PARAM_ECHO === "all";
|
|
112
|
+
if (verbose) {
|
|
113
|
+
for (const p of resolution.provenance) {
|
|
114
|
+
console.error(formatInfo(`[param] ${p.name} = ${JSON.stringify(p.value)} (${p.source})`));
|
|
115
|
+
}
|
|
116
|
+
} else if (resolution.provenance.length > 0) {
|
|
117
|
+
console.error(formatInfo(summarizeBuildParams(resolution.provenance)));
|
|
121
118
|
}
|
|
122
119
|
|
|
123
120
|
return { success: true, provenance: resolution.provenance, errors: [] };
|
|
124
121
|
}
|
|
125
122
|
|
|
123
|
+
/**
|
|
124
|
+
* `N build parameters resolved (1 from cli, 3 default); --verbose to list` —
|
|
125
|
+
* the non-verbose echo of a successful resolution (#1424). Sources appear in
|
|
126
|
+
* precedence order; a source nobody used is left out.
|
|
127
|
+
*/
|
|
128
|
+
export function summarizeBuildParams(provenance: readonly { source: string }[]): string {
|
|
129
|
+
const counts = new Map<string, number>();
|
|
130
|
+
for (const p of provenance) counts.set(p.source, (counts.get(p.source) ?? 0) + 1);
|
|
131
|
+
const order = ["cli", "params-file", "env", "default"];
|
|
132
|
+
const rank = (s: string) => (order.includes(s) ? order.indexOf(s) : order.length);
|
|
133
|
+
const parts = [...counts.keys()]
|
|
134
|
+
.sort((a, b) => rank(a) - rank(b))
|
|
135
|
+
.map((source) => (source === "default" ? `${counts.get(source)} default` : `${counts.get(source)} from ${source}`));
|
|
136
|
+
const n = provenance.length;
|
|
137
|
+
return `${n} build parameter${n === 1 ? "" : "s"} resolved (${parts.join(", ")}); --verbose to list`;
|
|
138
|
+
}
|
|
139
|
+
|
|
126
140
|
/**
|
|
127
141
|
* Resolve a command's declared build-time parameters, the same way `chant
|
|
128
142
|
* build` does, so a `build()`/`discover()` on the declared side sees the values
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Getting Started
|
|
3
|
+
description: First project with the Fixture lexicon
|
|
4
|
+
diataxis: tutorial
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Install the lexicon, declare one resource, and build it.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install --save-dev @intentius/chant-lexicon-fixture
|
|
11
|
+
chant build
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Replace this with a walkthrough a new user can follow start to finish. Other
|
|
15
|
+
pages go beside this file in `docs/pages/`, each with a `diataxis` field
|
|
16
|
+
(`tutorial`, `how-to`, `reference` or `explanation`).
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import { defineCollection } from 'astro:content';
|
|
1
|
+
import { defineCollection, z } from 'astro:content';
|
|
2
2
|
import { docsLoader } from '@astrojs/starlight/loaders';
|
|
3
3
|
import { docsSchema } from '@astrojs/starlight/schema';
|
|
4
4
|
|
|
5
5
|
export const collections = {
|
|
6
|
-
docs: defineCollection({
|
|
6
|
+
docs: defineCollection({
|
|
7
|
+
loader: docsLoader(),
|
|
8
|
+
schema: docsSchema({
|
|
9
|
+
extend: z.object({
|
|
10
|
+
// Diátaxis quadrant (https://diataxis.fr), chant #1731.
|
|
11
|
+
diataxis: z.enum(['tutorial', 'how-to', 'reference', 'explanation']).optional(),
|
|
12
|
+
}),
|
|
13
|
+
}),
|
|
14
|
+
}),
|
|
7
15
|
};
|
|
@@ -3,6 +3,8 @@ import { generate, writeGeneratedFiles } from "./generate";
|
|
|
3
3
|
import { dirname } from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// src/codegen/generate-cli.ts -> src/codegen -> src -> package root.
|
|
7
|
+
// Two dirnames land in src/ and the first generate writes src/src/generated/ (#1614).
|
|
8
|
+
const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
7
9
|
const result = await generate({ verbose: true });
|
|
8
10
|
writeGeneratedFiles(result, pkgDir);
|