@intentius/chant 0.79.0 → 0.80.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/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +38 -0
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/terraform-state.d.ts +14 -0
- package/dist/audit/terraform-state.d.ts.map +1 -1
- package/dist/cli/command-group.d.ts +6 -0
- package/dist/cli/command-group.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +27 -2
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/import-agents.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/conflict-check.d.ts +6 -0
- package/dist/cli/conflict-check.d.ts.map +1 -1
- package/dist/cli/handlers/dev.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/misc.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/promote.d.ts +17 -0
- package/dist/cli/handlers/promote.d.ts.map +1 -0
- package/dist/cli/handlers/run-generate.d.ts +39 -0
- package/dist/cli/handlers/run-generate.d.ts.map +1 -0
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +9 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/capability-plugin-loader.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/fan-out-support.d.ts.map +1 -1
- package/dist/components/pilots/alb-ecs.pilot.d.ts +7 -1
- package/dist/components/pilots/alb-ecs.pilot.d.ts.map +1 -1
- package/dist/components/presets/ecs-fargate.d.ts +10 -4
- package/dist/components/presets/ecs-fargate.d.ts.map +1 -1
- package/dist/components/promote.d.ts +211 -0
- package/dist/components/promote.d.ts.map +1 -0
- package/dist/components/unbound-gate-approval.d.ts +24 -0
- package/dist/components/unbound-gate-approval.d.ts.map +1 -0
- package/dist/config.d.ts +82 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/convergence.d.ts +97 -0
- package/dist/discovery/convergence.d.ts.map +1 -0
- package/dist/discovery/files.d.ts +41 -2
- package/dist/discovery/files.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -0
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +13 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/lexicon-module.d.ts +65 -0
- package/dist/lexicon-module.d.ts.map +1 -0
- package/dist/lexicon.d.ts +24 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
- package/dist/lifecycle/legacy-digest.d.ts +26 -0
- package/dist/lifecycle/legacy-digest.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +25 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts +2 -3
- package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts.map +1 -1
- package/dist/op/activity-contract-registry.d.ts.map +1 -1
- package/dist/op/activity-registry.d.ts.map +1 -1
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/generate-pipeline.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/workspace/record-source.d.ts +22 -0
- package/dist/workspace/record-source.d.ts.map +1 -0
- package/dist/workspace/records-cli.d.ts +57 -0
- package/dist/workspace/records-cli.d.ts.map +1 -0
- package/dist/workspace/records.d.ts +118 -0
- package/dist/workspace/records.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/audit/core.ts +13 -2
- package/src/audit/discover.ts +90 -12
- package/src/audit/terraform-state.test.ts +86 -1
- package/src/audit/terraform-state.ts +26 -0
- package/src/cli/command-group.ts +7 -0
- package/src/cli/commands/audit-walk-warnings.test.ts +156 -0
- package/src/cli/commands/audit.ts +93 -19
- package/src/cli/commands/check-lexicon-examples.ts +2 -1
- package/src/cli/commands/doctor.ts +3 -0
- package/src/cli/commands/import-agents.ts +11 -4
- package/src/cli/commands/init.ts +57 -25
- package/src/cli/commands/lint.test.ts +58 -1
- package/src/cli/commands/lint.ts +48 -18
- package/src/cli/commands/onboard.ts +15 -0
- package/src/cli/commands/update.ts +5 -1
- package/src/cli/conflict-check.test.ts +18 -1
- package/src/cli/conflict-check.ts +13 -0
- package/src/cli/discovery-skip.test.ts +168 -0
- package/src/cli/handlers/build.test.ts +11 -0
- package/src/cli/handlers/build.ts +1 -1
- package/src/cli/handlers/dev.ts +3 -0
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +4 -2
- package/src/cli/handlers/lifecycle.ts +2 -1
- package/src/cli/handlers/misc.ts +16 -0
- package/src/cli/handlers/operator.ts +2 -1
- package/src/cli/handlers/promote.test.ts +261 -0
- package/src/cli/handlers/promote.ts +325 -0
- package/src/cli/handlers/run-generate.test.ts +160 -0
- package/src/cli/handlers/run-generate.ts +133 -0
- package/src/cli/handlers/run.ts +13 -1
- package/src/cli/main.test.ts +33 -1
- package/src/cli/main.ts +69 -16
- package/src/cli/mcp/op-tools.ts +2 -1
- package/src/cli/path-lexicon-messages.test.ts +152 -0
- package/src/cli/plugins.ts +44 -6
- package/src/cli/registry.ts +14 -1
- package/src/components/__fixtures__/alb-ecs-service.json +0 -8
- package/src/components/capability-plugin-loader.ts +3 -1
- package/src/components/cli-support.ts +3 -2
- package/src/components/discover.ts +26 -2
- package/src/components/driver.test.ts +12 -11
- package/src/components/driver.ts +2 -0
- package/src/components/fan-out-support.ts +2 -1
- package/src/components/pilots/README.md +1 -1
- package/src/components/pilots/alb-ecs.pilot.ts +7 -7
- package/src/components/presets/ecs-fargate.ts +10 -5
- package/src/components/promote.test.ts +371 -0
- package/src/components/promote.ts +537 -0
- package/src/components/unbound-gate-approval.test.ts +82 -0
- package/src/components/unbound-gate-approval.ts +51 -0
- package/src/config.test.ts +40 -0
- package/src/config.ts +128 -11
- package/src/discovery/convergence.test.ts +231 -0
- package/src/discovery/convergence.ts +399 -0
- package/src/discovery/files.test.ts +127 -1
- package/src/discovery/files.ts +97 -9
- package/src/discovery/fold-import.ts +83 -9
- package/src/discovery/sandbox/path-lexicon-parity.test.ts +162 -0
- package/src/graph-ir.ts +14 -0
- package/src/lexicon-module.test.ts +203 -0
- package/src/lexicon-module.ts +117 -0
- package/src/lexicon.ts +26 -0
- package/src/lifecycle/build-ledger-store.ts +11 -1
- package/src/lifecycle/legacy-digest.test.ts +114 -0
- package/src/lifecycle/legacy-digest.ts +62 -0
- package/src/lifecycle/release-ledger.ts +34 -0
- package/src/lint/rules/comp/comp003-mutating-no-rollback.ts +2 -3
- package/src/op/activity-contract-registry.ts +18 -4
- package/src/op/activity-registry.ts +26 -0
- package/src/op/discover-convergence.test.ts +70 -0
- package/src/op/discover.ts +4 -0
- package/src/op/gate-approval.ts +3 -1
- package/src/op/generate-pipeline.ts +2 -1
- package/src/op/runtimes/local.ts +2 -1
- package/src/workspace/record-source.ts +117 -0
- package/src/workspace/records-cli.ts +126 -0
- package/src/workspace/records-contract.test.ts +88 -0
- package/src/workspace/records.schema.json +120 -0
- package/src/workspace/records.test.ts +243 -0
- package/src/workspace/records.ts +383 -0
package/src/discovery/files.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { readdir, stat, open } from "node:fs/promises";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { join, relative } from "node:path";
|
|
3
|
+
import { join, relative, sep, isAbsolute } from "node:path";
|
|
4
|
+
// @ts-ignore — picomatch has no types declaration
|
|
5
|
+
import picomatch from "picomatch";
|
|
6
|
+
import { resolveDiscoveryGlobs, type DiscoveryGlobs } from "../config";
|
|
7
|
+
import { warnDiscoveryChanges } from "./convergence";
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Marker chant writes at the top of files it generates (a hosting lexicon's
|
|
@@ -12,13 +16,38 @@ import { join, relative } from "node:path";
|
|
|
12
16
|
*/
|
|
13
17
|
export const GENERATED_MARKER = "Generated by chant — do not edit directly";
|
|
14
18
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Comment a hand-written file carries to keep discovery out of it (#2519): a
|
|
21
|
+
* runnable script with top-level side effects, say, that lives next to the
|
|
22
|
+
* project's declarations. It must be a comment of its own (`// chant-discovery-skip`
|
|
23
|
+
* or inside a block comment) within the first {@link SKIP_MARKER_HEAD_BYTES}
|
|
24
|
+
* bytes of the file. See {@link hasSkipMarker}.
|
|
25
|
+
*/
|
|
26
|
+
export const SKIP_MARKER = "chant-discovery-skip";
|
|
27
|
+
|
|
28
|
+
/** How much of a file's head {@link hasSkipMarker} reads: room for a shebang and a license header. */
|
|
29
|
+
const SKIP_MARKER_HEAD_BYTES = 1024;
|
|
30
|
+
|
|
31
|
+
/** {@link SKIP_MARKER} opening a `//`, `/*` or ` * ` comment line, as a whole word. */
|
|
32
|
+
const SKIP_MARKER_PATTERN = /^[ \t]*(?:\/\/|\/\*+|\*)[ \t]*chant-discovery-skip(?![\w-])/m;
|
|
33
|
+
|
|
34
|
+
/** True when `head`, the start of a file, carries the {@link SKIP_MARKER} comment. */
|
|
35
|
+
export function hasSkipMarker(head: string): boolean {
|
|
36
|
+
return SKIP_MARKER_PATTERN.test(head.slice(0, SKIP_MARKER_HEAD_BYTES));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether a file's head carries the {@link GENERATED_MARKER} or the
|
|
41
|
+
* {@link SKIP_MARKER}. Reads only the head. The generated marker is looked for
|
|
42
|
+
* in the first 256 bytes, as it always was.
|
|
43
|
+
*/
|
|
44
|
+
export async function hasDiscoveryMarker(file: string): Promise<boolean> {
|
|
17
45
|
let fh;
|
|
18
46
|
try {
|
|
19
47
|
fh = await open(file, "r");
|
|
20
|
-
const { buffer, bytesRead } = await fh.read({ buffer: Buffer.alloc(
|
|
21
|
-
|
|
48
|
+
const { buffer, bytesRead } = await fh.read({ buffer: Buffer.alloc(SKIP_MARKER_HEAD_BYTES), position: 0 });
|
|
49
|
+
if (buffer.toString("utf8", 0, Math.min(bytesRead, 256)).includes(GENERATED_MARKER)) return true;
|
|
50
|
+
return hasSkipMarker(buffer.toString("utf8", 0, bytesRead));
|
|
22
51
|
} catch {
|
|
23
52
|
return false;
|
|
24
53
|
} finally {
|
|
@@ -26,14 +55,55 @@ async function isGeneratedFile(file: string): Promise<boolean> {
|
|
|
26
55
|
}
|
|
27
56
|
}
|
|
28
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Compile a project's `exclude`/`include` globs (#2519) into a predicate that
|
|
60
|
+
* is true for a file discovery must skip, or `undefined` when there is nothing
|
|
61
|
+
* to skip.
|
|
62
|
+
*
|
|
63
|
+
* Each pattern is matched, with picomatch and dot files included, against the
|
|
64
|
+
* file's path relative to `globs.root` (forward slashes) and against the path
|
|
65
|
+
* of every directory above it, so a pattern naming a directory covers
|
|
66
|
+
* everything under it. A file is skipped when some `exclude` pattern matches
|
|
67
|
+
* and no `include` pattern does: `include` always wins. Files outside
|
|
68
|
+
* `globs.root` are never skipped.
|
|
69
|
+
*/
|
|
70
|
+
export function compileDiscoveryFilter(globs: DiscoveryGlobs | undefined): ((file: string) => boolean) | undefined {
|
|
71
|
+
if (!globs || globs.exclude.length === 0) return undefined;
|
|
72
|
+
const excluded = picomatch(globs.exclude, { dot: true });
|
|
73
|
+
const included = globs.include.length > 0 ? picomatch(globs.include, { dot: true }) : () => false;
|
|
74
|
+
return (file: string): boolean => {
|
|
75
|
+
const rel = relative(globs.root, file);
|
|
76
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return false;
|
|
77
|
+
const parts = rel.split(sep);
|
|
78
|
+
const candidates = parts.map((_, i) => parts.slice(0, i + 1).join("/"));
|
|
79
|
+
return candidates.some((c) => excluded(c)) && !candidates.some((c) => included(c));
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Options for {@link findInfraFiles}. */
|
|
84
|
+
export interface FindInfraFilesOptions {
|
|
85
|
+
/**
|
|
86
|
+
* The project's discovery globs. Omitted, they are read from the project
|
|
87
|
+
* config `path` belongs to ({@link resolveDiscoveryGlobs}); `null` applies none.
|
|
88
|
+
*/
|
|
89
|
+
globs?: DiscoveryGlobs | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
29
92
|
/**
|
|
30
93
|
* Recursively find all TypeScript infrastructure files in a directory
|
|
31
94
|
* @param path - The directory path to search
|
|
32
|
-
* @returns Array of file paths to .ts files (excluding test files
|
|
95
|
+
* @returns Array of file paths to .ts files (excluding test files, chant-generated
|
|
96
|
+
* files, files carrying the skip marker, and files the project's `exclude` globs name)
|
|
33
97
|
*/
|
|
34
|
-
export async function findInfraFiles(path: string): Promise<string[]> {
|
|
98
|
+
export async function findInfraFiles(path: string, options?: FindInfraFilesOptions): Promise<string[]> {
|
|
35
99
|
const files: string[] = [];
|
|
100
|
+
const skip = compileDiscoveryFilter(
|
|
101
|
+
options?.globs === null ? undefined : (options?.globs ?? (await resolveDiscoveryGlobs(path))),
|
|
102
|
+
);
|
|
36
103
|
let sourceRoot: string | null = null;
|
|
104
|
+
// Child projects skipped because the source root was already set: the
|
|
105
|
+
// next release reads them when `path` is outside a project (#2527).
|
|
106
|
+
const skippedChildren: string[] = [];
|
|
37
107
|
|
|
38
108
|
async function scanDirectory(dir: string): Promise<void> {
|
|
39
109
|
let entries;
|
|
@@ -63,17 +133,20 @@ export async function findInfraFiles(path: string): Promise<string[]> {
|
|
|
63
133
|
if (sourceRoot === null) {
|
|
64
134
|
sourceRoot = fullPath;
|
|
65
135
|
} else {
|
|
136
|
+
skippedChildren.push(fullPath);
|
|
66
137
|
continue;
|
|
67
138
|
}
|
|
68
139
|
}
|
|
69
140
|
await scanDirectory(fullPath);
|
|
70
141
|
} else if (entry.isFile()) {
|
|
71
|
-
// Include only .ts files, exclude test files
|
|
142
|
+
// Include only .ts files, exclude test files, files the project's
|
|
143
|
+
// globs exclude, and chant-generated or skip-marked files
|
|
72
144
|
if (
|
|
73
145
|
entry.name.endsWith(".ts") &&
|
|
74
146
|
!entry.name.endsWith(".test.ts") &&
|
|
75
147
|
!entry.name.endsWith(".spec.ts") &&
|
|
76
|
-
!(
|
|
148
|
+
!skip?.(fullPath) &&
|
|
149
|
+
!(await hasDiscoveryMarker(fullPath))
|
|
77
150
|
) {
|
|
78
151
|
files.push(fullPath);
|
|
79
152
|
}
|
|
@@ -82,5 +155,20 @@ export async function findInfraFiles(path: string): Promise<string[]> {
|
|
|
82
155
|
}
|
|
83
156
|
|
|
84
157
|
await scanDirectory(path);
|
|
158
|
+
// The warning release for #2527: say which files the converged walker will
|
|
159
|
+
// read differently. The list returned is today's, unchanged.
|
|
160
|
+
await warnDiscoveryChanges({
|
|
161
|
+
walker: "source",
|
|
162
|
+
root: path,
|
|
163
|
+
files,
|
|
164
|
+
sourceRoot,
|
|
165
|
+
skippedChildren,
|
|
166
|
+
fileOk: async (name, full) =>
|
|
167
|
+
name.endsWith(".ts") &&
|
|
168
|
+
!name.endsWith(".test.ts") &&
|
|
169
|
+
!name.endsWith(".spec.ts") &&
|
|
170
|
+
!skip?.(full) &&
|
|
171
|
+
!(await hasDiscoveryMarker(full)),
|
|
172
|
+
});
|
|
85
173
|
return files;
|
|
86
174
|
}
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type FoldedCompositeStepCall,
|
|
28
28
|
} from "../fold/fold";
|
|
29
29
|
import { isChantOwnedSpecifier, isFoldableHelperName } from "../fold/foldable-helpers";
|
|
30
|
+
import { lexiconModulePath } from "../lexicon-module";
|
|
30
31
|
import {
|
|
31
32
|
briefNodeText,
|
|
32
33
|
callExpressionMessage,
|
|
@@ -247,6 +248,21 @@ export interface FoldSession {
|
|
|
247
248
|
* established one.
|
|
248
249
|
*/
|
|
249
250
|
readonly lexiconPackages: ReadonlySet<string>;
|
|
251
|
+
/**
|
|
252
|
+
* chant#2577 — the absolute module paths of this build's lexicons that
|
|
253
|
+
* `chant.config.ts` declares by path (#2520), each resolved to the file an
|
|
254
|
+
* import of it reaches. Such a lexicon has no package specifier, so a
|
|
255
|
+
* project imports it by a relative or absolute path. A binding whose path
|
|
256
|
+
* resolves to one of these is treated exactly like a binding of a
|
|
257
|
+
* {@link lexiconPackages} entry: its data exports resolve as values, and
|
|
258
|
+
* under `--sandbox` it is on {@link isTrustedExecutableBinding}'s allowlist,
|
|
259
|
+
* because `loadPlugins` has already imported and run it in this process.
|
|
260
|
+
*
|
|
261
|
+
* Only the declared module itself is in the set. A package has a root that
|
|
262
|
+
* bounds its subpaths, and a module path has none, so other files beside it
|
|
263
|
+
* stay project files.
|
|
264
|
+
*/
|
|
265
|
+
readonly lexiconModules: ReadonlySet<string>;
|
|
250
266
|
/**
|
|
251
267
|
* chant #1093 — this build asked for the #1045 sandbox
|
|
252
268
|
* (`DiscoveryOptions.sandbox`, `chant build --sandbox`), so fold must not
|
|
@@ -327,6 +343,15 @@ export function createFoldSession(
|
|
|
327
343
|
/** chant#2455 — `ι = executing`. Mutually exclusive with `sandbox`. */
|
|
328
344
|
executing = false,
|
|
329
345
|
): FoldSession {
|
|
346
|
+
// chant#2577 — a lexicon declared by path is that module and no package, so
|
|
347
|
+
// its name contributes the module path and not `@intentius/chant-lexicon-<name>`.
|
|
348
|
+
const packages = new Set(lexiconPackages);
|
|
349
|
+
const modules = new Set<string>();
|
|
350
|
+
for (const name of lexicons) {
|
|
351
|
+
const modulePath = lexiconModulePath(name);
|
|
352
|
+
if (modulePath === undefined) packages.add(lexiconPackageName(name));
|
|
353
|
+
else modules.add(resolveModulePath(modulePath, modulePath));
|
|
354
|
+
}
|
|
330
355
|
return {
|
|
331
356
|
intrinsics,
|
|
332
357
|
cache: new Map(),
|
|
@@ -334,7 +359,8 @@ export function createFoldSession(
|
|
|
334
359
|
importCache: new Map(),
|
|
335
360
|
resolvePathCache: new Map(),
|
|
336
361
|
buildParams,
|
|
337
|
-
lexiconPackages:
|
|
362
|
+
lexiconPackages: packages,
|
|
363
|
+
lexiconModules: modules,
|
|
338
364
|
sandbox,
|
|
339
365
|
executing,
|
|
340
366
|
factoryModules: new Map(),
|
|
@@ -1104,6 +1130,29 @@ function activeLexiconPackage(specifier: string, lexiconPackages: ReadonlySet<st
|
|
|
1104
1130
|
return lexiconPackages.has(specifier) ? specifier : undefined;
|
|
1105
1131
|
}
|
|
1106
1132
|
|
|
1133
|
+
/**
|
|
1134
|
+
* chant#2577 — the path-declared counterpart of {@link activeLexiconPackage}:
|
|
1135
|
+
* the resolved module path when a relative or absolute `specifier` reaches
|
|
1136
|
+
* one of this build's {@link FoldSession.lexiconModules}, `undefined`
|
|
1137
|
+
* otherwise. A bare specifier is never resolved here, and a build with no
|
|
1138
|
+
* path-declared lexicon resolves nothing.
|
|
1139
|
+
*/
|
|
1140
|
+
function activeLexiconModule(
|
|
1141
|
+
specifier: string,
|
|
1142
|
+
fromFile: string,
|
|
1143
|
+
lexiconModules: ReadonlySet<string>,
|
|
1144
|
+
resolvePathCache: Map<string, string>,
|
|
1145
|
+
): string | undefined {
|
|
1146
|
+
if (lexiconModules.size === 0 || !isProjectFileSpecifier(specifier)) return undefined;
|
|
1147
|
+
let modulePath: string;
|
|
1148
|
+
try {
|
|
1149
|
+
modulePath = resolveModulePathMemoized(specifier, fromFile, resolvePathCache);
|
|
1150
|
+
} catch {
|
|
1151
|
+
return undefined;
|
|
1152
|
+
}
|
|
1153
|
+
return lexiconModules.has(modulePath) ? modulePath : undefined;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1107
1156
|
/**
|
|
1108
1157
|
* chant #1995 — the package-ROOT portion of a bare subpath specifier
|
|
1109
1158
|
* (`@intentius/chant-lexicon-azure/generated/index` ->
|
|
@@ -1168,13 +1217,17 @@ async function resolveActiveLexiconExport(
|
|
|
1168
1217
|
fromFile: string,
|
|
1169
1218
|
session: FoldSession,
|
|
1170
1219
|
): Promise<{ value: unknown } | undefined> {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1220
|
+
let modulePath: string | undefined;
|
|
1221
|
+
if (activeLexiconPackage(binding.specifier, session.lexiconPackages)) {
|
|
1222
|
+
try {
|
|
1223
|
+
modulePath = resolveModulePathMemoized(binding.specifier, fromFile, session.resolvePathCache);
|
|
1224
|
+
} catch {
|
|
1225
|
+
return undefined;
|
|
1226
|
+
}
|
|
1227
|
+
} else {
|
|
1228
|
+
// chant#2577 — a lexicon declared by path, imported by its path.
|
|
1229
|
+
modulePath = activeLexiconModule(binding.specifier, fromFile, session.lexiconModules, session.resolvePathCache);
|
|
1230
|
+
if (modulePath === undefined) return undefined;
|
|
1178
1231
|
}
|
|
1179
1232
|
|
|
1180
1233
|
let mod: Record<string, unknown>;
|
|
@@ -1278,6 +1331,8 @@ interface ResolveCtx {
|
|
|
1278
1331
|
* site that imports and executes a module.
|
|
1279
1332
|
*/
|
|
1280
1333
|
lexiconPackages: ReadonlySet<string>;
|
|
1334
|
+
/** chant#2577 — see {@link FoldSession.lexiconModules}. */
|
|
1335
|
+
lexiconModules: ReadonlySet<string>;
|
|
1281
1336
|
/** chant #1093 — see {@link FoldSession.sandbox}. */
|
|
1282
1337
|
sandbox: boolean;
|
|
1283
1338
|
/** chant#2455 — see {@link FoldSession.executing}. */
|
|
@@ -2053,7 +2108,10 @@ function findCompositeDefinition(
|
|
|
2053
2108
|
// chant build the set holds only `@intentius/chant-lexicon-*`, and no
|
|
2054
2109
|
// lexicon exports a `Composite`, so nothing about a chant project moves.
|
|
2055
2110
|
const chantOwned = isChantOwnedHelperBinding(compositeBinding, { ...ctx, file: scope.file });
|
|
2056
|
-
const hostOwned =
|
|
2111
|
+
const hostOwned =
|
|
2112
|
+
activeLexiconPackage(compositeBinding.specifier, ctx.lexiconPackages) !== undefined ||
|
|
2113
|
+
activeLexiconModule(compositeBinding.specifier, scope.file, ctx.lexiconModules, ctx.resolvePathCache) !==
|
|
2114
|
+
undefined;
|
|
2057
2115
|
if (!chantOwned && !hostOwned) return undefined;
|
|
2058
2116
|
|
|
2059
2117
|
const [fnArg, nameArg] = init.arguments;
|
|
@@ -2425,6 +2483,7 @@ async function interpretCompositeFactory(
|
|
|
2425
2483
|
importCache: ctx.importCache,
|
|
2426
2484
|
resolvePathCache: ctx.resolvePathCache,
|
|
2427
2485
|
lexiconPackages: ctx.lexiconPackages,
|
|
2486
|
+
lexiconModules: ctx.lexiconModules,
|
|
2428
2487
|
sandbox: ctx.sandbox,
|
|
2429
2488
|
executing: ctx.executing,
|
|
2430
2489
|
session: ctx.session,
|
|
@@ -3048,6 +3107,9 @@ function isTrustedExecutableBinding(binding: ImportBinding, ctx: ResolveCtx): bo
|
|
|
3048
3107
|
} catch {
|
|
3049
3108
|
return false;
|
|
3050
3109
|
}
|
|
3110
|
+
// chant#2577 — arm 1 for a lexicon declared by path: the module
|
|
3111
|
+
// `loadPlugins` already imported, reached by the path the project imports it by.
|
|
3112
|
+
if (ctx.lexiconModules.has(targetPath)) return true;
|
|
3051
3113
|
const root = chantCoreRoot();
|
|
3052
3114
|
return targetPath === root || targetPath.startsWith(root + sep);
|
|
3053
3115
|
}
|
|
@@ -3607,6 +3669,14 @@ async function buildExternals(
|
|
|
3607
3669
|
} catch {
|
|
3608
3670
|
continue;
|
|
3609
3671
|
}
|
|
3672
|
+
// chant#2577 — a path-declared lexicon is a lexicon, not a project file:
|
|
3673
|
+
// resolve its data export the way the bare-specifier branch above does,
|
|
3674
|
+
// with no `liveSources` edge for the same reason.
|
|
3675
|
+
if (session.lexiconModules.has(targetPath)) {
|
|
3676
|
+
const lexiconExport = await resolveActiveLexiconExport(binding, file, session);
|
|
3677
|
+
if (lexiconExport) externals.set(localName, lexiconExport.value);
|
|
3678
|
+
continue;
|
|
3679
|
+
}
|
|
3610
3680
|
const result = await foldFileMemoized(targetPath, session);
|
|
3611
3681
|
if (!result.ok) {
|
|
3612
3682
|
failures.set(localName, locatedMessage(binding.specifierNode, result.reason));
|
|
@@ -3632,6 +3702,9 @@ async function buildExternals(
|
|
|
3632
3702
|
} catch {
|
|
3633
3703
|
continue;
|
|
3634
3704
|
}
|
|
3705
|
+
// chant#2577 — a namespace import of a lexicon is left alone, as the
|
|
3706
|
+
// bare-specifier check above leaves a package's alone.
|
|
3707
|
+
if (session.lexiconModules.has(targetPath)) continue;
|
|
3635
3708
|
const result = await foldFileMemoized(targetPath, session);
|
|
3636
3709
|
if (!result.ok) {
|
|
3637
3710
|
failures.set(localName, locatedMessage(binding.specifierNode, result.reason));
|
|
@@ -3738,6 +3811,7 @@ async function tryFoldFileCore(file: string, session: FoldSession): Promise<Fold
|
|
|
3738
3811
|
importCache: session.importCache,
|
|
3739
3812
|
resolvePathCache: session.resolvePathCache,
|
|
3740
3813
|
lexiconPackages: session.lexiconPackages,
|
|
3814
|
+
lexiconModules: session.lexiconModules,
|
|
3741
3815
|
sandbox: session.sandbox,
|
|
3742
3816
|
executing: session.executing,
|
|
3743
3817
|
session,
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chant#2577 — a lexicon declared by path in `chant.config.ts` (#2520) gets the
|
|
3
|
+
* same fold treatment as the same lexicon installed as a package, with and
|
|
4
|
+
* without `--sandbox`.
|
|
5
|
+
*
|
|
6
|
+
* One lexicon source is written twice: once as the package
|
|
7
|
+
* `@intentius/chant-lexicon-demo` under the fixture's own `node_modules`, and
|
|
8
|
+
* once as `./lexicon/index.ts` named by path. The two projects differ only in
|
|
9
|
+
* how `chant.config.ts` names the lexicon and in the specifier `main.ts`
|
|
10
|
+
* imports it by. Each project's config is loaded, which is what records a
|
|
11
|
+
* path-declared lexicon, and each is built with the lexicon list a CLI build
|
|
12
|
+
* passes (the loaded plugins' names). The outputs and fold verdicts are compared.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, test, expect, beforeAll, afterAll, afterEach } from "vitest";
|
|
15
|
+
import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
|
|
16
|
+
import { join, resolve } from "node:path";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { build } from "../../build";
|
|
19
|
+
import { loadChantConfig } from "../../config";
|
|
20
|
+
import { resetLexiconModules } from "../../lexicon-module";
|
|
21
|
+
import type { LexiconPlugin } from "../../lexicon";
|
|
22
|
+
|
|
23
|
+
const runtimePath = resolve(import.meta.dirname, "../../runtime");
|
|
24
|
+
const compositePath = resolve(import.meta.dirname, "../../composite");
|
|
25
|
+
|
|
26
|
+
const LEXICON_SOURCE = `
|
|
27
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
28
|
+
import { Composite } from ${JSON.stringify(compositePath)};
|
|
29
|
+
|
|
30
|
+
export const Bucket = createResource("Demo::Bucket", "demo", { arn: "Arn" });
|
|
31
|
+
export const Role = createResource("Demo::Role", "demo", {});
|
|
32
|
+
export const Regions = { primary: "us-east-1" };
|
|
33
|
+
|
|
34
|
+
export const WebApp = Composite((props) => {
|
|
35
|
+
const bucket = new Bucket({ bucketName: props.name });
|
|
36
|
+
const role = new Role({ resource: bucket.arn });
|
|
37
|
+
return { bucket, role };
|
|
38
|
+
}, "WebApp");
|
|
39
|
+
|
|
40
|
+
function plain(value) {
|
|
41
|
+
if (value && typeof value === "object" && typeof value.getLogicalName === "function") {
|
|
42
|
+
return { ref: value.getLogicalName(), attr: value.attribute };
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(value)) return value.map(plain);
|
|
45
|
+
if (value && typeof value === "object") {
|
|
46
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, plain(v)]));
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const demoPlugin = {
|
|
52
|
+
name: "demo",
|
|
53
|
+
serializer: {
|
|
54
|
+
name: "demo",
|
|
55
|
+
rulePrefix: "DEMO",
|
|
56
|
+
serialize: (entities) =>
|
|
57
|
+
JSON.stringify(
|
|
58
|
+
Object.fromEntries(
|
|
59
|
+
[...entities.keys()].sort().map((name) => {
|
|
60
|
+
const entity = entities.get(name);
|
|
61
|
+
return [name, { type: entity.entityType, props: plain(entity.props) }];
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
generate: async () => {},
|
|
67
|
+
validate: async () => {},
|
|
68
|
+
coverage: async () => {},
|
|
69
|
+
package: async () => {},
|
|
70
|
+
};
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
function mainSource(specifier: string): string {
|
|
74
|
+
return `
|
|
75
|
+
import { Bucket, Regions, WebApp } from ${JSON.stringify(specifier)};
|
|
76
|
+
|
|
77
|
+
export const logs = new Bucket({ bucketName: Regions.primary });
|
|
78
|
+
export const web = WebApp({ name: "data" });
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let root: string;
|
|
83
|
+
|
|
84
|
+
beforeAll(async () => {
|
|
85
|
+
root = await realpath(await mkdtemp(join(tmpdir(), "chant-2577-")));
|
|
86
|
+
|
|
87
|
+
const pkg = join(root, "pkg");
|
|
88
|
+
const pkgDir = join(pkg, "node_modules", "@intentius", "chant-lexicon-demo");
|
|
89
|
+
await mkdir(pkgDir, { recursive: true });
|
|
90
|
+
await writeFile(
|
|
91
|
+
join(pkgDir, "package.json"),
|
|
92
|
+
JSON.stringify({ name: "@intentius/chant-lexicon-demo", type: "module", main: "index.ts" }),
|
|
93
|
+
);
|
|
94
|
+
await writeFile(join(pkgDir, "index.ts"), LEXICON_SOURCE);
|
|
95
|
+
await mkdir(join(pkg, "src"));
|
|
96
|
+
await writeFile(join(pkg, "src", "main.ts"), mainSource("@intentius/chant-lexicon-demo"));
|
|
97
|
+
await writeFile(join(pkg, "chant.config.ts"), `export default { lexicons: ["demo"] };\n`);
|
|
98
|
+
|
|
99
|
+
const path = join(root, "path");
|
|
100
|
+
await mkdir(join(path, "lexicon"), { recursive: true });
|
|
101
|
+
await writeFile(join(path, "lexicon", "index.ts"), LEXICON_SOURCE);
|
|
102
|
+
await mkdir(join(path, "src"));
|
|
103
|
+
await writeFile(join(path, "src", "main.ts"), mainSource("../lexicon"));
|
|
104
|
+
await writeFile(
|
|
105
|
+
join(path, "chant.config.ts"),
|
|
106
|
+
`export default { lexicons: [{ name: "demo", module: "./lexicon/index.ts" }] };\n`,
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
afterAll(async () => {
|
|
111
|
+
await rm(root, { recursive: true, force: true });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
afterEach(() => {
|
|
115
|
+
resetLexiconModules();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
async function buildProject(project: "pkg" | "path", modes: { fold: boolean; sandbox: boolean }) {
|
|
119
|
+
const dir = join(root, project);
|
|
120
|
+
const lexiconFile =
|
|
121
|
+
project === "pkg"
|
|
122
|
+
? join(dir, "node_modules", "@intentius", "chant-lexicon-demo", "index.ts")
|
|
123
|
+
: join(dir, "lexicon", "index.ts");
|
|
124
|
+
await loadChantConfig(dir);
|
|
125
|
+
const plugin = (await import(lexiconFile)).demoPlugin as LexiconPlugin;
|
|
126
|
+
const result = await build(join(dir, "src"), [plugin.serializer], undefined, {
|
|
127
|
+
...modes,
|
|
128
|
+
lexicons: [plugin.name],
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
errors: result.errors,
|
|
132
|
+
output: result.outputs.get("demo") as string,
|
|
133
|
+
decisions: result.foldDecisions.map((d) => ({ file: d.file.slice(dir.length), mode: d.mode, reason: d.reason })),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
describe("a lexicon declared by path folds like the same lexicon installed as a package (chant#2577)", () => {
|
|
138
|
+
for (const modes of [
|
|
139
|
+
{ fold: true, sandbox: true },
|
|
140
|
+
{ fold: true, sandbox: false },
|
|
141
|
+
]) {
|
|
142
|
+
test(
|
|
143
|
+
`--fold${modes.sandbox ? " --sandbox" : ""}: same output, same fold verdicts`,
|
|
144
|
+
async () => {
|
|
145
|
+
const asPackage = await buildProject("pkg", modes);
|
|
146
|
+
const byPath = await buildProject("path", modes);
|
|
147
|
+
|
|
148
|
+
expect(asPackage.errors).toEqual([]);
|
|
149
|
+
expect(byPath.errors).toEqual([]);
|
|
150
|
+
expect(byPath.output).toBe(asPackage.output);
|
|
151
|
+
expect(JSON.parse(byPath.output)).toEqual({
|
|
152
|
+
logs: { type: "Demo::Bucket", props: { bucketName: "us-east-1" } },
|
|
153
|
+
webBucket: { type: "Demo::Bucket", props: { bucketName: "data" } },
|
|
154
|
+
webRole: { type: "Demo::Role", props: { resource: { ref: "webBucket", attr: "Arn" } } },
|
|
155
|
+
});
|
|
156
|
+
expect(byPath.decisions).toEqual(asPackage.decisions);
|
|
157
|
+
expect(byPath.decisions).toEqual([{ file: "/src/main.ts", mode: "fold", reason: undefined }]);
|
|
158
|
+
},
|
|
159
|
+
60_000,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
});
|
package/src/graph-ir.ts
CHANGED
|
@@ -290,8 +290,22 @@ export interface IRPipeline {
|
|
|
290
290
|
edges: IRPipelineEdge[];
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
/**
|
|
294
|
+
* The version `chant graph --format ir` writes on its output (#2529, #2524 D8).
|
|
295
|
+
* The format changes only additively: a new field or edge kind never bumps
|
|
296
|
+
* it, and a consumer ignores the fields and edge kinds it does not know. The
|
|
297
|
+
* number moves only for a change a consumer could not ignore.
|
|
298
|
+
*/
|
|
299
|
+
export const GRAPH_IR_VERSION = 1;
|
|
300
|
+
|
|
293
301
|
/** The full graph IR for a project at the default (declarable) detail level. */
|
|
294
302
|
export interface GraphIR {
|
|
303
|
+
/**
|
|
304
|
+
* {@link GRAPH_IR_VERSION}, stamped by `chant graph --format ir` when it
|
|
305
|
+
* writes the IR (#2529). Absent on an IR built in memory, and on output
|
|
306
|
+
* from a chant older than the field, which a reader treats as version 1.
|
|
307
|
+
*/
|
|
308
|
+
version?: number;
|
|
295
309
|
nodes: IRNode[];
|
|
296
310
|
edges: IREdge[];
|
|
297
311
|
groups: IRGroups;
|