@intentius/chant 0.97.0 → 0.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/import.d.ts +67 -4
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/lint.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/main.d.ts.map +1 -1
- package/dist/cli/mcp/tools/import.d.ts +4 -0
- package/dist/cli/mcp/tools/import.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +10 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/import/embedded.d.ts +184 -0
- package/dist/import/embedded.d.ts.map +1 -0
- package/dist/import/generator.d.ts +13 -0
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/parser.d.ts +15 -1
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/lexicon.d.ts +19 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lint/engine.d.ts +6 -1
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lint/rule.d.ts +10 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/lint/rules/file-declarable-limit.d.ts.map +1 -1
- package/dist/lint/rules/flat-declarations.d.ts.map +1 -1
- package/dist/lint/rules/no-unused-declarable.d.ts.map +1 -1
- package/dist/lint/rules/property-kind.d.ts +7 -0
- package/dist/lint/rules/property-kind.d.ts.map +1 -0
- package/dist/workspace/conformance/index.d.ts +9 -0
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/yaml.d.ts +44 -6
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/import-layout.test.ts +142 -0
- package/src/cli/commands/import-no-parser.test.ts +70 -0
- package/src/cli/commands/import.test.ts +284 -2
- package/src/cli/commands/import.ts +325 -86
- package/src/cli/commands/lint.ts +21 -8
- package/src/cli/handlers/misc.ts +3 -0
- package/src/cli/handlers/operator-steward-signal.e2e.test.ts +19 -11
- package/src/cli/handlers/operator.ts +17 -3
- package/src/cli/main.ts +3 -1
- package/src/cli/mcp/tools/import.ts +6 -0
- package/src/cli/plugins.ts +41 -2
- package/src/deep-observation.test.ts +21 -0
- package/src/deep-observation.ts +9 -0
- package/src/import/embedded.test.ts +153 -0
- package/src/import/embedded.ts +376 -0
- package/src/import/generator.ts +14 -0
- package/src/import/parser.ts +17 -1
- package/src/lexicon.ts +21 -0
- package/src/lint/engine.ts +7 -0
- package/src/lint/rule.ts +10 -0
- package/src/lint/rules/file-declarable-limit.ts +11 -4
- package/src/lint/rules/flat-declarations.ts +6 -1
- package/src/lint/rules/no-unused-declarable.ts +59 -2
- package/src/lint/rules/property-kind.test.ts +99 -0
- package/src/lint/rules/property-kind.ts +56 -0
- package/src/workspace/conformance/index.mjs +1 -0
- package/src/workspace/conformance/index.ts +23 -1
- package/src/yaml.test.ts +244 -1
- package/src/yaml.ts +443 -241
|
@@ -45,7 +45,7 @@ function sleep(ms: number): Promise<void> {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
describe("chant operator --steward — a repeated SIGTERM (#2872)", () => {
|
|
48
|
-
test("
|
|
48
|
+
test("repeated SIGTERMs sent in quick succession still release the steward's lease and exit 0", async () => {
|
|
49
49
|
const dir = repo(
|
|
50
50
|
{
|
|
51
51
|
"chant.config.ts": "export default { lexicons: [] };\n",
|
|
@@ -77,16 +77,24 @@ describe("chant operator --steward — a repeated SIGTERM (#2872)", () => {
|
|
|
77
77
|
await sleep(50);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
// listener left and the process exits
|
|
84
|
-
// `finally` that releases the lease.
|
|
85
|
-
child
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
// A SIGTERM, then more every few ms until the child is gone, each given
|
|
81
|
+
// a moment to reach the child's event loop before the next. With
|
|
82
|
+
// `process.once`, the first one's auto-removal happens the instant it
|
|
83
|
+
// fires, so the second finds no listener left and the process exits
|
|
84
|
+
// right there, skipping the `finally` that releases the lease. Firing
|
|
85
|
+
// until the child closes (not a fixed three) also covers the stretch
|
|
86
|
+
// after that `finally`, up to `process.exit`: when the handler removed
|
|
87
|
+
// its listeners there, a SIGTERM arriving in that stretch ended the
|
|
88
|
+
// process by signal (exit code null) though the lease was released. With
|
|
89
|
+
// three signals 30ms apart that failed CI now and then (runs
|
|
90
|
+
// 36357297831, 36358285603, 36368551353); signalling to the end hit it
|
|
91
|
+
// every time.
|
|
92
|
+
let closed = false;
|
|
93
|
+
child.on("close", () => (closed = true));
|
|
94
|
+
for (let sent = 0; !closed && sent < 1000; sent++) {
|
|
95
|
+
child.kill("SIGTERM");
|
|
96
|
+
await sleep(5);
|
|
97
|
+
}
|
|
90
98
|
|
|
91
99
|
const code = await exited;
|
|
92
100
|
expect(code, `chant operator --steward did not exit cleanly; stderr:\n${stderr}`).toBe(0);
|
|
@@ -216,7 +216,12 @@ export async function runOperator(ctx: CommandContext): Promise<number> {
|
|
|
216
216
|
});
|
|
217
217
|
return 0;
|
|
218
218
|
} finally {
|
|
219
|
-
|
|
219
|
+
// Only when no signal started this shutdown. After one did, the process
|
|
220
|
+
// exits right after this returns, and removing the listener would put
|
|
221
|
+
// SIGINT back to its default disposition for that last stretch: a repeat
|
|
222
|
+
// signal arriving there ends the process by signal instead of the exit
|
|
223
|
+
// code (see the steward's `finally` below).
|
|
224
|
+
if (!stopping) process.removeListener("SIGINT", onSigint);
|
|
220
225
|
}
|
|
221
226
|
}
|
|
222
227
|
|
|
@@ -347,8 +352,17 @@ export async function runStewardOperator(ctx: CommandContext): Promise<number> {
|
|
|
347
352
|
// A courtesy: the lease expires on its own if this never runs.
|
|
348
353
|
const { record } = await readLease(stewardLeaseName(steward.name)).catch(() => ({ record: undefined }));
|
|
349
354
|
if (record?.holder === holder) await releaseLease(stewardLeaseName(steward.name), holder, record.token).catch(() => false);
|
|
350
|
-
|
|
351
|
-
process.
|
|
355
|
+
// Removed only when no signal started this shutdown. After one did, the
|
|
356
|
+
// CLI goes on to `flushAndExit` (../main.ts) and `process.exit`, and
|
|
357
|
+
// removing the last SIGTERM listener hands the signal back to its
|
|
358
|
+
// default disposition for that stretch: a repeat SIGTERM arriving there
|
|
359
|
+
// ended the process by signal (exit code null) with the lease already
|
|
360
|
+
// released. That was the operator-steward-signal e2e test's intermittent
|
|
361
|
+
// failure; a SIGTERM every 2ms after the first hit it 8 runs of 8.
|
|
362
|
+
if (!stopping) {
|
|
363
|
+
process.removeListener("SIGINT", onSigint);
|
|
364
|
+
process.removeListener("SIGTERM", onSigint);
|
|
365
|
+
}
|
|
352
366
|
}
|
|
353
367
|
}
|
|
354
368
|
|
package/src/cli/main.ts
CHANGED
|
@@ -1091,7 +1091,8 @@ Options:
|
|
|
1091
1091
|
- build: json (default) or yaml
|
|
1092
1092
|
- list: text (default) or json
|
|
1093
1093
|
- lint: stylish (default), json, or sarif
|
|
1094
|
-
-d, --lexicon <name> Build only the specified lexicon (e.g. aws, gitlab)
|
|
1094
|
+
-d, --lexicon <name> Build only the specified lexicon (e.g. aws, gitlab);
|
|
1095
|
+
import: use this lexicon instead of detecting one
|
|
1095
1096
|
--env <name> Active environment: sets CHANT_ENV so env-aware source
|
|
1096
1097
|
re-evaluates for that environment (build + graph), and
|
|
1097
1098
|
drives organizational policy. Must be in chant.config
|
|
@@ -1201,6 +1202,7 @@ Examples:
|
|
|
1201
1202
|
chant run --components all --env production
|
|
1202
1203
|
chant components export prod --component search-service -o ./dist/search-service
|
|
1203
1204
|
chant import template.json --output ./infra/
|
|
1205
|
+
chant import manifest.yaml --lexicon k8s --output ./infra/
|
|
1204
1206
|
chant import --from prod --name my-bucket --output src/
|
|
1205
1207
|
chant lint ./infra/
|
|
1206
1208
|
chant lint ./infra/ --format sarif
|
|
@@ -17,6 +17,10 @@ export const importTool = {
|
|
|
17
17
|
type: "string",
|
|
18
18
|
description: "Output directory (default: ./infra/)",
|
|
19
19
|
},
|
|
20
|
+
lexicon: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Lexicon to import with (e.g. k8s); skips template detection",
|
|
23
|
+
},
|
|
20
24
|
},
|
|
21
25
|
required: ["source"],
|
|
22
26
|
},
|
|
@@ -28,11 +32,13 @@ export const importTool = {
|
|
|
28
32
|
export async function handleImport(params: Record<string, unknown>): Promise<unknown> {
|
|
29
33
|
const source = params.source as string;
|
|
30
34
|
const output = params.output as string | undefined;
|
|
35
|
+
const lexicon = params.lexicon as string | undefined;
|
|
31
36
|
|
|
32
37
|
const result = await importCommand({
|
|
33
38
|
templatePath: source,
|
|
34
39
|
output,
|
|
35
40
|
force: true,
|
|
41
|
+
lexicon,
|
|
36
42
|
});
|
|
37
43
|
|
|
38
44
|
if (!result.success) {
|
package/src/cli/plugins.ts
CHANGED
|
@@ -2,8 +2,9 @@ import { importLexiconModule, importLexiconPackage, lexiconModulePath, lexiconNa
|
|
|
2
2
|
import { readLexiconDeclarationsStatically, type StaticLexiconRead } from "../config-static";
|
|
3
3
|
export { unknownPathLexiconsNotice } from "../config-static";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
6
|
-
import {
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { existsSync, readdirSync, readFileSync, type Dirent } from "node:fs";
|
|
7
8
|
import { isLexiconPlugin, type LexiconPlugin } from "../lexicon";
|
|
8
9
|
import { loadChantConfigUpward } from "../config";
|
|
9
10
|
import { findInfraFiles, detectLexicons } from "../index";
|
|
@@ -80,6 +81,44 @@ export async function loadPlugin(lexiconName: string): Promise<LexiconPlugin> {
|
|
|
80
81
|
throw new Error(`Package ${packageName} does not export a LexiconPlugin or Serializer`);
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
const LEXICON_PACKAGE_PREFIX = "chant-lexicon-";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The names of the lexicon packages installed where {@link loadPlugin} can
|
|
88
|
+
* load them (#2965), sorted. `loadPlugin` imports
|
|
89
|
+
* `@intentius/chant-lexicon-<name>` from chant's own install and then from the
|
|
90
|
+
* project (`importLexiconPackage`), so this lists the `@intentius` scope of
|
|
91
|
+
* every `node_modules` directory Node would search from either place: each
|
|
92
|
+
* ancestor of chant's own module and each ancestor of `projectDir`. A name
|
|
93
|
+
* being listed does not promise it loads; the caller still handles a failure.
|
|
94
|
+
*/
|
|
95
|
+
export function listInstalledLexicons(projectDir: string = process.cwd()): string[] {
|
|
96
|
+
const names = new Set<string>();
|
|
97
|
+
const starts = [dirname(fileURLToPath(import.meta.url)), resolve(projectDir)];
|
|
98
|
+
for (const start of starts) {
|
|
99
|
+
let dir = start;
|
|
100
|
+
for (;;) {
|
|
101
|
+
const scope = join(dir, "node_modules", "@intentius");
|
|
102
|
+
let entries: Dirent[] = [];
|
|
103
|
+
try {
|
|
104
|
+
entries = readdirSync(scope, { withFileTypes: true });
|
|
105
|
+
} catch {
|
|
106
|
+
// No @intentius scope here.
|
|
107
|
+
}
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
if (!entry.name.startsWith(LEXICON_PACKAGE_PREFIX)) continue;
|
|
110
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
111
|
+
const name = entry.name.slice(LEXICON_PACKAGE_PREFIX.length);
|
|
112
|
+
if (name !== "") names.add(name);
|
|
113
|
+
}
|
|
114
|
+
const parent = dirname(dir);
|
|
115
|
+
if (parent === dir) break;
|
|
116
|
+
dir = parent;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return [...names].sort();
|
|
120
|
+
}
|
|
121
|
+
|
|
83
122
|
/**
|
|
84
123
|
* The installed version of each named lexicon package (chant #1442).
|
|
85
124
|
*
|
|
@@ -309,6 +309,27 @@ describe("deepPathSet", () => {
|
|
|
309
309
|
const set = deepPathSet({ Tags: [{ Key: "a" }] });
|
|
310
310
|
expect([...set].sort()).toEqual(["Tags", "Tags[0]", "Tags[0].Key", "Tags[]", "Tags[].Key"]);
|
|
311
311
|
});
|
|
312
|
+
|
|
313
|
+
// #2946: the path set has to agree with the normalized tree, which inlines
|
|
314
|
+
// a property-kind declarable's props (#1314). Otherwise a live node under
|
|
315
|
+
// one reads `counterpart: "absent"` and is pruned as an undeclared default.
|
|
316
|
+
test("walks a property-kind declarable as its props, as normalization inlines it", () => {
|
|
317
|
+
class Panel {
|
|
318
|
+
readonly entityType = "Grafana::Panel::stat";
|
|
319
|
+
readonly kind = "property";
|
|
320
|
+
constructor(readonly props: Record<string, unknown>) {}
|
|
321
|
+
}
|
|
322
|
+
const set = deepPathSet({ panels: [new Panel({ gridPos: { w: 6 } })] });
|
|
323
|
+
expect(set.has("panels[0].gridPos.w")).toBe(true);
|
|
324
|
+
expect(set.has("panels[].gridPos.w")).toBe(true);
|
|
325
|
+
|
|
326
|
+
const hooks = { prune: (n: { side: string; counterpart: string }) => n.side === "live" && n.counterpart === "absent" };
|
|
327
|
+
const live = normalizeDeepProperties(
|
|
328
|
+
{ panels: [{ gridPos: { w: 6, x: 0 } }] },
|
|
329
|
+
{ entityType: "T", side: "live", hooks, counterpartPaths: set },
|
|
330
|
+
);
|
|
331
|
+
expect(live).toEqual({ panels: [{ gridPos: { w: 6 } }] });
|
|
332
|
+
});
|
|
312
333
|
});
|
|
313
334
|
|
|
314
335
|
describe("deepValueEqual", () => {
|
package/src/deep-observation.ts
CHANGED
|
@@ -363,6 +363,15 @@ export function deepPathSet(tree: Record<string, unknown>): Set<string> {
|
|
|
363
363
|
// gives it, so this path set agrees with what the normalized tree
|
|
364
364
|
// actually looks like a path down.
|
|
365
365
|
if (isHeldElsewhere(value)) return;
|
|
366
|
+
// A property-kind declarable is walked as its props, exactly as
|
|
367
|
+
// `normalizeDeepProperties` inlines it (#1314). Stopping at it left every
|
|
368
|
+
// path under a typed nested property out of the set, so the other tree's
|
|
369
|
+
// nodes there read `counterpart: "absent"` and default subtraction fired
|
|
370
|
+
// on properties the author did declare.
|
|
371
|
+
if (isPropertyDeclarableValue(value)) {
|
|
372
|
+
walk((value as { props?: unknown }).props ?? {}, path, pattern);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
366
375
|
if (Array.isArray(value)) {
|
|
367
376
|
value.forEach((el, i) => walk(el, joinIndex(path, i), joinPattern(pattern)));
|
|
368
377
|
return;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
EmbeddedImports,
|
|
4
|
+
EmbeddedImportScope,
|
|
5
|
+
embeddedDocument,
|
|
6
|
+
exportedNames,
|
|
7
|
+
isEmbeddedReference,
|
|
8
|
+
renderEmbeddedReference,
|
|
9
|
+
type EmbeddedContent,
|
|
10
|
+
type EmbeddedContentImporter,
|
|
11
|
+
} from "./embedded";
|
|
12
|
+
|
|
13
|
+
const site = (over: Partial<EmbeddedContent> = {}): EmbeddedContent => ({
|
|
14
|
+
host: "k8s",
|
|
15
|
+
hostType: "K8s::Core::ConfigMap",
|
|
16
|
+
location: 'ConfigMap agent data["config.yaml"]',
|
|
17
|
+
directory: "agent",
|
|
18
|
+
text: "kind: widget\nsize: 3\n",
|
|
19
|
+
...over,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/** Imports any `kind: widget` document as `widgets.ts`, passed through `render`. */
|
|
23
|
+
const widgets: EmbeddedContentImporter = {
|
|
24
|
+
what: "a widget",
|
|
25
|
+
matches: (c) => (c.document as { kind?: string } | undefined)?.kind === "widget",
|
|
26
|
+
import: () => ({
|
|
27
|
+
files: [{ path: "widgets.ts", content: "const w = 1;\n\nexport { w };\n" }],
|
|
28
|
+
value: { bindings: [{ from: "widgets.ts", name: "w" }], shape: "list", through: { from: "@acme/widgets", name: "render" } },
|
|
29
|
+
warnings: ["dropped the colour"],
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe("EmbeddedImports", () => {
|
|
34
|
+
test("an importer that matches gets the content, with its document parsed from the text", () => {
|
|
35
|
+
const seen: EmbeddedContent[] = [];
|
|
36
|
+
const resolver = new EmbeddedImports([{ lexicon: "acme", importer: { ...widgets, import: (c) => (seen.push(c), widgets.import(c)) } }]);
|
|
37
|
+
const ref = resolver.resolve(site());
|
|
38
|
+
expect(seen[0].document).toEqual({ kind: "widget", size: 3 });
|
|
39
|
+
expect(isEmbeddedReference(ref)).toBe(true);
|
|
40
|
+
// Local modules are rebased under the content's directory; packages stay as they are.
|
|
41
|
+
expect(ref!.$embedded).toEqual({
|
|
42
|
+
lexicon: "acme",
|
|
43
|
+
what: "a widget",
|
|
44
|
+
location: 'ConfigMap agent data["config.yaml"]',
|
|
45
|
+
value: { bindings: [{ from: "agent/widgets.ts", name: "w" }], shape: "list", through: { from: "@acme/widgets", name: "render" } },
|
|
46
|
+
});
|
|
47
|
+
expect(resolver.files).toEqual([{ path: "agent/widgets.ts", content: "const w = 1;\n\nexport { w };\n" }]);
|
|
48
|
+
expect(resolver.warnings).toEqual(['ConfigMap agent data["config.yaml"]: dropped the colour']);
|
|
49
|
+
expect(resolver.offered).toHaveLength(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("two pieces of content wanting one directory get two", () => {
|
|
53
|
+
const resolver = new EmbeddedImports([{ lexicon: "acme", importer: widgets }]);
|
|
54
|
+
resolver.resolve(site({ directory: "Agent Config" }));
|
|
55
|
+
resolver.resolve(site({ directory: "agent-config" }));
|
|
56
|
+
expect(resolver.files.map((f) => f.path)).toEqual(["agent-config/widgets.ts", "agent-config-2/widgets.ts"]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("content nobody claims is kept, with a warning only when the host names its expected owner", () => {
|
|
60
|
+
const resolver = new EmbeddedImports([{ lexicon: "acme", importer: widgets }]);
|
|
61
|
+
expect(resolver.resolve(site({ text: "kind: gadget\n" }))).toBeUndefined();
|
|
62
|
+
expect(resolver.warnings).toEqual([]);
|
|
63
|
+
expect(resolver.resolve(site({ text: "kind: gadget\n", expectedOwner: { lexicon: "gadgets", what: "a gadget" } }))).toBeUndefined();
|
|
64
|
+
expect(resolver.warnings).toHaveLength(1);
|
|
65
|
+
expect(resolver.warnings[0]).toContain("looks like a gadget");
|
|
66
|
+
expect(resolver.warnings[0]).toContain("@intentius/chant-lexicon-gadgets");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("a quiet probe collects what is offered and warns about nothing", () => {
|
|
70
|
+
const probe = new EmbeddedImports([], { quiet: true });
|
|
71
|
+
expect(probe.resolve(site({ expectedOwner: { lexicon: "gadgets", what: "a gadget" } }))).toBeUndefined();
|
|
72
|
+
expect(probe.offered).toHaveLength(1);
|
|
73
|
+
expect(probe.warnings).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("a failing import keeps the content and says why; a throwing matcher is a non-match", () => {
|
|
77
|
+
const resolver = new EmbeddedImports([
|
|
78
|
+
{ lexicon: "broken", importer: { what: "a widget", matches: () => { throw new Error("no"); }, import: widgets.import } },
|
|
79
|
+
{ lexicon: "acme", importer: { ...widgets, import: () => { throw new Error("bad size"); } } },
|
|
80
|
+
]);
|
|
81
|
+
expect(resolver.resolve(site())).toBeUndefined();
|
|
82
|
+
expect(resolver.files).toEqual([]);
|
|
83
|
+
expect(resolver.warnings).toEqual([
|
|
84
|
+
'ConfigMap agent data["config.yaml"] looks like a widget, but the acme import failed, so it is kept as written: bad size',
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("when two lexicons match, the first imports it and the other is named", () => {
|
|
89
|
+
const resolver = new EmbeddedImports([
|
|
90
|
+
{ lexicon: "acme", importer: widgets },
|
|
91
|
+
{ lexicon: "other", importer: widgets },
|
|
92
|
+
]);
|
|
93
|
+
expect(resolver.resolve(site())!.$embedded.lexicon).toBe("acme");
|
|
94
|
+
expect(resolver.warnings[0]).toContain("also importable by other");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("renderEmbeddedReference", () => {
|
|
99
|
+
const ref = (bindings: Array<{ from: string; name: string; member?: string }>, shape: "list" | "single", through?: { from: string; name: string }) => ({
|
|
100
|
+
$embedded: { lexicon: "acme", what: "a widget", location: "here", value: { bindings, shape, ...(through ? { through } : {}) } },
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("imports each name once, relative to the module, and aliases a name already taken", () => {
|
|
104
|
+
const scope = new EmbeddedImportScope("", ["otlp"]);
|
|
105
|
+
const a = renderEmbeddedReference(
|
|
106
|
+
ref([{ from: "agent/receivers.ts", name: "otlp" }, { from: "agent/pipelines.ts", name: "traces" }], "list", { from: "@acme/otel", name: "collectorYaml" }),
|
|
107
|
+
scope,
|
|
108
|
+
);
|
|
109
|
+
const b = renderEmbeddedReference(ref([{ from: "gateway/receivers.ts", name: "otlp" }], "list"), scope);
|
|
110
|
+
expect(a).toBe("collectorYaml([otlp2, traces])");
|
|
111
|
+
expect(b).toBe("[otlp3]");
|
|
112
|
+
expect(scope.lines()).toEqual([
|
|
113
|
+
'import { collectorYaml } from "@acme/otel";',
|
|
114
|
+
'import { traces } from "./agent/pipelines";',
|
|
115
|
+
'import { otlp as otlp2 } from "./agent/receivers";',
|
|
116
|
+
'import { otlp as otlp3 } from "./gateway/receivers";',
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("a single binding, a member, and a module in a subdirectory", () => {
|
|
121
|
+
const scope = new EmbeddedImportScope("infra");
|
|
122
|
+
expect(renderEmbeddedReference(ref([{ from: "rules/slos.ts", name: "api", member: "rules" }], "single"), scope)).toBe("api.rules");
|
|
123
|
+
expect(scope.lines()).toEqual(['import { api } from "../rules/slos";']);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("a long list is broken one binding per line", () => {
|
|
127
|
+
const names = Array.from({ length: 12 }, (_, i) => ({ from: "c/x.ts", name: `component${i}` }));
|
|
128
|
+
const out = renderEmbeddedReference(ref(names, "list"), new EmbeddedImportScope(""), 4);
|
|
129
|
+
expect(out.split("\n")).toHaveLength(14);
|
|
130
|
+
expect(out.split("\n")[1]).toBe(" component0,");
|
|
131
|
+
expect(out.endsWith("\n ]")).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("helpers", () => {
|
|
136
|
+
test("embeddedDocument reads JSON or one YAML document", () => {
|
|
137
|
+
expect(embeddedDocument('{"a": 1}')).toEqual({ a: 1 });
|
|
138
|
+
expect(embeddedDocument("a: 1\n")).toEqual({ a: 1 });
|
|
139
|
+
expect(embeddedDocument("a: 1\n---\nb: 2\n")).toBeUndefined();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("exportedNames reads one-line and multi-line export lists", () => {
|
|
143
|
+
expect(exportedNames("const a = 1;\n\nexport { a, b };\n")).toEqual(["a", "b"]);
|
|
144
|
+
expect(exportedNames("export {\n one,\n two as three,\n};\n")).toEqual(["one", "three"]);
|
|
145
|
+
expect(exportedNames("export const x = 1;\n")).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("isEmbeddedReference", () => {
|
|
149
|
+
expect(isEmbeddedReference({ $embedded: { value: { bindings: [] } } })).toBe(true);
|
|
150
|
+
expect(isEmbeddedReference({ $embedded: true })).toBe(false);
|
|
151
|
+
expect(isEmbeddedReference("x")).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
});
|