@intentius/chant 0.97.0 → 0.98.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 +50 -3
- package/dist/cli/commands/import.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/yaml.d.ts +16 -0
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/import-no-parser.test.ts +70 -0
- package/src/cli/commands/import.test.ts +259 -2
- package/src/cli/commands/import.ts +173 -56
- 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/yaml.test.ts +53 -1
- package/src/yaml.ts +48 -0
|
@@ -9,6 +9,11 @@ export interface ImportOptions {
|
|
|
9
9
|
output?: string;
|
|
10
10
|
/** Force overwrite existing files */
|
|
11
11
|
force?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Lexicon whose parser handles the file (#2935). Skips detection and the
|
|
14
|
+
* JSON/YAML check: the raw content goes straight to that plugin's parser.
|
|
15
|
+
*/
|
|
16
|
+
lexicon?: string;
|
|
12
17
|
}
|
|
13
18
|
/**
|
|
14
19
|
* Import command result
|
|
@@ -22,9 +27,50 @@ export interface ImportResult {
|
|
|
22
27
|
warnings: string[];
|
|
23
28
|
/** Error message if failed */
|
|
24
29
|
error?: string;
|
|
25
|
-
/**
|
|
30
|
+
/** The lexicon that handled the template */
|
|
26
31
|
lexicon?: string;
|
|
32
|
+
/**
|
|
33
|
+
* True when `lexicon` was found by template detection, false or absent when
|
|
34
|
+
* it was named (`--lexicon`, `--kustomize`) (#2965).
|
|
35
|
+
*/
|
|
36
|
+
detected?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parse template content for detection (#2935): JSON first, then YAML. A YAML
|
|
40
|
+
* file is split into documents with core's `splitYAMLDocuments` (the k8s
|
|
41
|
+
* parser splits the same way), and each document is parsed on its own, so
|
|
42
|
+
* detection sees the same per-document objects the plugin's parser will. A
|
|
43
|
+
* document whose top level is a list parses as that list (#2965). JSON yields
|
|
44
|
+
* one document, the parsed value, exactly as before. Returns undefined when
|
|
45
|
+
* the content is neither: core's YAML reader is lenient and turns unparseable
|
|
46
|
+
* text into an empty mapping, so a file with no non-empty document is
|
|
47
|
+
* rejected.
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseTemplateDocuments(content: string): unknown[] | undefined;
|
|
50
|
+
/** The outcome of template detection (#2965). */
|
|
51
|
+
export interface TemplateDetection {
|
|
52
|
+
/** The plugin that handles the template. */
|
|
53
|
+
plugin: LexiconPlugin;
|
|
54
|
+
/**
|
|
55
|
+
* Where it came from: one of the project's lexicons, or an installed
|
|
56
|
+
* lexicon package tried because none of the project's matched.
|
|
57
|
+
*/
|
|
58
|
+
source: "project" | "installed";
|
|
59
|
+
/** Other installed lexicons that also recognized the deciding document. */
|
|
60
|
+
alsoMatched: string[];
|
|
27
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Detect which lexicon handles a template (#2965). The project's lexicons
|
|
64
|
+
* (from chant.config, or the lexicons its source imports) are asked first, so
|
|
65
|
+
* inside a project nothing changes when one of them matches. When none does,
|
|
66
|
+
* or there is no project, every installed `@intentius/chant-lexicon-*`
|
|
67
|
+
* package that is not already a project lexicon is asked, in name order,
|
|
68
|
+
* and one with a `templateParser` is preferred over one without.
|
|
69
|
+
* Installed lexicons are loaded one at a time and without `init()`; one that
|
|
70
|
+
* fails to load is skipped. The chosen plugin is initialized before it is
|
|
71
|
+
* returned.
|
|
72
|
+
*/
|
|
73
|
+
export declare function detectTemplateLexicon(documents: unknown[], projectDir: string): Promise<TemplateDetection | undefined>;
|
|
28
74
|
/**
|
|
29
75
|
* Execute the import command
|
|
30
76
|
*/
|
|
@@ -33,8 +79,9 @@ export declare function importCommand(options: ImportOptions): Promise<ImportRes
|
|
|
33
79
|
* Import from an in-memory template string through a KNOWN plugin — no
|
|
34
80
|
* detection, no JSON assumption (#1548). This is the seam
|
|
35
81
|
* `chant import --kustomize <dir>` drives with `kustomize build` output
|
|
36
|
-
* through the k8s plugin's YAML parser
|
|
37
|
-
*
|
|
82
|
+
* through the k8s plugin's YAML parser, and `chant import <file> --lexicon
|
|
83
|
+
* <name>` drives with the file's content (#2935); `importCommand` above is
|
|
84
|
+
* the same pipeline behind file reading + JSON/YAML detection.
|
|
38
85
|
*/
|
|
39
86
|
export interface ContentImportOptions {
|
|
40
87
|
/** The raw template content (YAML or JSON — the plugin's parser decides). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/import.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/import.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"misc.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/misc.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA8ClD,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAqGnE;AAED,wBAAsB,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAuClE;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA8CtE;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"misc.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/misc.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA8ClD,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAqGnE;AAED,wBAAsB,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAuClE;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA8CtE;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAyIpE;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAKpE;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBpE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operator.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/operator.ts"],"names":[],"mappings":"AAgCA,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAE9F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAC3F,OAAO,EACuE,KAAK,UAAU,EAC5F,MAAM,6BAA6B,CAAC;AAOrC,OAAO,EAEuC,KAAK,mBAAmB,EAAE,KAAK,oBAAoB,EAChG,MAAM,wBAAwB,CAAC;AAKhC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAqFlD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"operator.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/operator.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/main.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAMA,OAAO,EAAkB,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAuG9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAyhBpD;
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAMA,OAAO,EAAkB,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAuG9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAyhBpD;AA+rBD;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuBzE;AAID;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,EAAE,UAAU,EAoIvC,CAAC;AA4KF;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB5E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../../../src/cli/mcp/tools/import.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,eAAO,MAAM,UAAU
|
|
1
|
+
{"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../../../src/cli/mcp/tools/import.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;CAqBtB,CAAC;AAEF;;GAEG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAsBpF"}
|
package/dist/cli/plugins.d.ts
CHANGED
|
@@ -20,6 +20,16 @@ export declare function recordProjectLexicons(dir: string): StaticLexiconRead;
|
|
|
20
20
|
* LexiconPlugin export. Falls back to wrapping a raw Serializer export.
|
|
21
21
|
*/
|
|
22
22
|
export declare function loadPlugin(lexiconName: string): Promise<LexiconPlugin>;
|
|
23
|
+
/**
|
|
24
|
+
* The names of the lexicon packages installed where {@link loadPlugin} can
|
|
25
|
+
* load them (#2965), sorted. `loadPlugin` imports
|
|
26
|
+
* `@intentius/chant-lexicon-<name>` from chant's own install and then from the
|
|
27
|
+
* project (`importLexiconPackage`), so this lists the `@intentius` scope of
|
|
28
|
+
* every `node_modules` directory Node would search from either place: each
|
|
29
|
+
* ancestor of chant's own module and each ancestor of `projectDir`. A name
|
|
30
|
+
* being listed does not promise it loads; the caller still handles a failure.
|
|
31
|
+
*/
|
|
32
|
+
export declare function listInstalledLexicons(projectDir?: string): string[];
|
|
23
33
|
/**
|
|
24
34
|
* The installed version of each named lexicon package (chant #1442).
|
|
25
35
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugins.d.ts","sourceRoot":"","sources":["../../src/cli/plugins.ts"],"names":[],"mappings":"AACA,OAAO,EAAqC,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC7F,OAAO,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"plugins.d.ts","sourceRoot":"","sources":["../../src/cli/plugins.ts"],"names":[],"mappings":"AACA,OAAO,EAAqC,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC7F,OAAO,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAK7D,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAKjE;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAIpE;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA8C5E;AAID;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,GAAE,MAAsB,GAAG,MAAM,EAAE,CAyBlF;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA0C9F;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,SAAS,aAAa,EAAE,GAAG,SAAS,EAC7C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAW,EAAE,MAAM,GAClB,KAAK,CAAC,OAAO,YAAY,EAAE,oBAAoB,CAAC,CAMlD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,SAAS,aAAa,EAAE,GAAG,SAAS,EAC7C,OAAO,EAAE;IACP,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC,CAAC;CAC5F,GACA,KAAK,CAAC,OAAO,gBAAgB,EAAE,gBAAgB,CAAC,CAgBlD;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CA2BlF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAUnF"}
|
package/dist/yaml.d.ts
CHANGED
|
@@ -19,6 +19,22 @@ export interface ParseResult {
|
|
|
19
19
|
* handles the subset of YAML commonly found in CI configuration files.
|
|
20
20
|
*/
|
|
21
21
|
export declare function parseYAML(content: string): Record<string, unknown>;
|
|
22
|
+
/**
|
|
23
|
+
* Parse one YAML (or JSON) document whose top level may be a sequence
|
|
24
|
+
* (#2965). {@link parseYAML} reads only a top-level mapping and returns `{}`
|
|
25
|
+
* for a file like `- a\n- b`; its callers rely on getting a mapping back, so
|
|
26
|
+
* it keeps doing that. This returns the list instead, and otherwise defers to
|
|
27
|
+
* `parseYAML`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseYAMLDocument(content: string): unknown;
|
|
30
|
+
/**
|
|
31
|
+
* Split a YAML stream into its documents (#2965). A line holding only `---`
|
|
32
|
+
* starts a document and a line holding only `...` ends one; either may carry
|
|
33
|
+
* a trailing `# comment`. Documents that are empty or hold only comments are
|
|
34
|
+
* dropped. The text of each document is returned unparsed, with `\r\n`
|
|
35
|
+
* normalized to `\n`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function splitYAMLDocuments(content: string): string[];
|
|
22
38
|
/**
|
|
23
39
|
* Parse indentation-based YAML lines into a key-value object.
|
|
24
40
|
*/
|
package/dist/yaml.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"yaml.d.ts","sourceRoot":"","sources":["../src/yaml.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA0BH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAgH/D;AAMD,sCAAsC;AACtC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAuBD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASlE;AAiFD;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,WAAW,CA8Eb;AA0GD;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,WAAW,CAqGb;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAwBlD"}
|
|
1
|
+
{"version":3,"file":"yaml.d.ts","sourceRoot":"","sources":["../src/yaml.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA0BH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAgH/D;AAMD,sCAAsC;AACtC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAuBD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASlE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAa1D;AAKD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAc5D;AAiFD;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,WAAW,CA8Eb;AA0GD;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,WAAW,CAqGb;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAwBlD"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import type { LexiconPlugin } from "../../lexicon";
|
|
7
|
+
import { importCommand, importFromContent } from "./import";
|
|
8
|
+
|
|
9
|
+
// A lexicon that recognizes a template but has no templateParser, as grafana
|
|
10
|
+
// does today (#2940). Import must say it cannot import, not throw
|
|
11
|
+
// "plugin.templateParser is not a function".
|
|
12
|
+
const { detectOnly } = vi.hoisted(() => ({
|
|
13
|
+
detectOnly: {
|
|
14
|
+
name: "detect-only",
|
|
15
|
+
detectTemplate: (data: unknown) =>
|
|
16
|
+
typeof data === "object" && data !== null && "detectOnlyMarker" in data,
|
|
17
|
+
} as unknown as LexiconPlugin,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("../plugins", () => ({
|
|
21
|
+
resolveProjectLexicons: async () => ["detect-only"],
|
|
22
|
+
loadPlugins: async (names: string[]) => names.map(() => detectOnly),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const EXPECTED = 'lexicon "detect-only" does not support template import';
|
|
26
|
+
|
|
27
|
+
describe("import through a lexicon with detectTemplate and no templateParser (#2940)", () => {
|
|
28
|
+
let testDir: string;
|
|
29
|
+
let templatePath: string;
|
|
30
|
+
let outputDir: string;
|
|
31
|
+
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
testDir = join(tmpdir(), `chant-import-no-parser-${Date.now()}-${Math.random()}`);
|
|
34
|
+
await mkdir(testDir, { recursive: true });
|
|
35
|
+
templatePath = join(testDir, "dashboard.json");
|
|
36
|
+
outputDir = join(testDir, "out");
|
|
37
|
+
await writeFile(templatePath, JSON.stringify({ detectOnlyMarker: true, panels: [] }));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(async () => {
|
|
41
|
+
await rm(testDir, { recursive: true, force: true });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("file import with detection fails cleanly", async () => {
|
|
45
|
+
const result = await importCommand({ templatePath, output: outputDir });
|
|
46
|
+
expect(result.success).toBe(false);
|
|
47
|
+
expect(result.error).toBe(EXPECTED);
|
|
48
|
+
expect(result.error).not.toContain("is not a function");
|
|
49
|
+
expect(result.lexicon).toBe("detect-only");
|
|
50
|
+
expect(existsSync(outputDir)).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("file import with --lexicon fails cleanly", async () => {
|
|
54
|
+
const result = await importCommand({ templatePath, output: outputDir, lexicon: "detect-only" });
|
|
55
|
+
expect(result.success).toBe(false);
|
|
56
|
+
expect(result.error).toBe(EXPECTED);
|
|
57
|
+
expect(existsSync(outputDir)).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("content import fails cleanly", async () => {
|
|
61
|
+
const result = await importFromContent({
|
|
62
|
+
content: JSON.stringify({ detectOnlyMarker: true }),
|
|
63
|
+
lexicon: "detect-only",
|
|
64
|
+
output: outputDir,
|
|
65
|
+
});
|
|
66
|
+
expect(result.success).toBe(false);
|
|
67
|
+
expect(result.error).toBe(EXPECTED);
|
|
68
|
+
expect(existsSync(outputDir)).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import {
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
detectTemplateLexicon,
|
|
4
|
+
importCommand,
|
|
5
|
+
parseTemplateDocuments,
|
|
6
|
+
printImportResult,
|
|
7
|
+
type ImportOptions,
|
|
8
|
+
} from "./import";
|
|
9
|
+
import { listInstalledLexicons } from "../plugins";
|
|
3
10
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
4
11
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
12
|
import { join } from "node:path";
|
|
@@ -377,3 +384,253 @@ describe("importCommand", () => {
|
|
|
377
384
|
}
|
|
378
385
|
});
|
|
379
386
|
});
|
|
387
|
+
|
|
388
|
+
// #2935 — YAML templates reach detection and the plugin's parser instead of
|
|
389
|
+
// failing JSON.parse, and `--lexicon` skips detection.
|
|
390
|
+
describe("importCommand with YAML templates", () => {
|
|
391
|
+
let testDir: string;
|
|
392
|
+
let outputDir: string;
|
|
393
|
+
|
|
394
|
+
const configMap = `apiVersion: v1
|
|
395
|
+
kind: ConfigMap
|
|
396
|
+
metadata:
|
|
397
|
+
name: app-config
|
|
398
|
+
data:
|
|
399
|
+
LOG_LEVEL: info
|
|
400
|
+
`;
|
|
401
|
+
|
|
402
|
+
const deployment = `apiVersion: apps/v1
|
|
403
|
+
kind: Deployment
|
|
404
|
+
metadata:
|
|
405
|
+
name: web
|
|
406
|
+
spec:
|
|
407
|
+
replicas: 2
|
|
408
|
+
selector:
|
|
409
|
+
matchLabels:
|
|
410
|
+
app: web
|
|
411
|
+
template:
|
|
412
|
+
metadata:
|
|
413
|
+
labels:
|
|
414
|
+
app: web
|
|
415
|
+
spec:
|
|
416
|
+
containers:
|
|
417
|
+
- name: web
|
|
418
|
+
image: nginx:1.27
|
|
419
|
+
`;
|
|
420
|
+
|
|
421
|
+
beforeEach(async () => {
|
|
422
|
+
testDir = join(tmpdir(), `chant-import-yaml-test-${Date.now()}-${Math.random()}`);
|
|
423
|
+
outputDir = join(testDir, "output");
|
|
424
|
+
await mkdir(testDir, { recursive: true });
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
afterEach(async () => {
|
|
428
|
+
await rm(testDir, { recursive: true, force: true });
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
/** A project that declares the k8s lexicon, so detection can find it. */
|
|
432
|
+
async function k8sProject(): Promise<void> {
|
|
433
|
+
await writeFile(join(testDir, "chant.config.json"), JSON.stringify({ lexicons: ["k8s"] }));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function generated(files: string[]): string {
|
|
437
|
+
return files.map((f) => readFileSync(join(outputDir, f), "utf-8")).join("\n");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
test("detects and imports a single-document k8s manifest", async () => {
|
|
441
|
+
await k8sProject();
|
|
442
|
+
const templatePath = join(testDir, "manifest.yaml");
|
|
443
|
+
await writeFile(templatePath, configMap);
|
|
444
|
+
|
|
445
|
+
const result = await importCommand({ templatePath, output: outputDir });
|
|
446
|
+
|
|
447
|
+
expect(result.error).toBeUndefined();
|
|
448
|
+
expect(result.success).toBe(true);
|
|
449
|
+
expect(result.lexicon).toBe("k8s");
|
|
450
|
+
expect(generated(result.generatedFiles)).toContain("ConfigMap");
|
|
451
|
+
}, 30000);
|
|
452
|
+
|
|
453
|
+
test("detects and imports a multi-document k8s manifest", async () => {
|
|
454
|
+
await k8sProject();
|
|
455
|
+
const templatePath = join(testDir, "manifests.yaml");
|
|
456
|
+
await writeFile(templatePath, `# app manifests\n---\n${configMap}---\n${deployment}`);
|
|
457
|
+
|
|
458
|
+
const result = await importCommand({ templatePath, output: outputDir });
|
|
459
|
+
|
|
460
|
+
expect(result.error).toBeUndefined();
|
|
461
|
+
expect(result.success).toBe(true);
|
|
462
|
+
expect(result.lexicon).toBe("k8s");
|
|
463
|
+
const code = generated(result.generatedFiles);
|
|
464
|
+
expect(code).toContain("ConfigMap");
|
|
465
|
+
expect(code).toContain("Deployment");
|
|
466
|
+
}, 30000);
|
|
467
|
+
|
|
468
|
+
test("--lexicon imports without detection", async () => {
|
|
469
|
+
const templatePath = join(testDir, "manifest.yaml");
|
|
470
|
+
await writeFile(templatePath, configMap);
|
|
471
|
+
|
|
472
|
+
const result = await importCommand({ templatePath, output: outputDir, lexicon: "k8s" });
|
|
473
|
+
|
|
474
|
+
expect(result.error).toBeUndefined();
|
|
475
|
+
expect(result.success).toBe(true);
|
|
476
|
+
expect(result.lexicon).toBe("k8s");
|
|
477
|
+
expect(result.detected).toBeFalsy();
|
|
478
|
+
expect(generated(result.generatedFiles)).toContain("ConfigMap");
|
|
479
|
+
}, 30000);
|
|
480
|
+
|
|
481
|
+
// #2965 — with no chant.config and no source imports, every installed
|
|
482
|
+
// lexicon is tried, not only aws.
|
|
483
|
+
test("detects a k8s manifest outside a project from the installed lexicons", async () => {
|
|
484
|
+
const templatePath = join(testDir, "manifest.yaml");
|
|
485
|
+
await writeFile(templatePath, `--- # config\n${configMap}...\n`);
|
|
486
|
+
|
|
487
|
+
const result = await importCommand({ templatePath, output: outputDir });
|
|
488
|
+
|
|
489
|
+
expect(result.error).toBeUndefined();
|
|
490
|
+
expect(result.success).toBe(true);
|
|
491
|
+
expect(result.lexicon).toBe("k8s");
|
|
492
|
+
expect(result.detected).toBe(true);
|
|
493
|
+
expect(generated(result.generatedFiles)).toContain("ConfigMap");
|
|
494
|
+
}, 60000);
|
|
495
|
+
|
|
496
|
+
test("--lexicon with an unknown lexicon fails", async () => {
|
|
497
|
+
const templatePath = join(testDir, "manifest.yaml");
|
|
498
|
+
await writeFile(templatePath, configMap);
|
|
499
|
+
|
|
500
|
+
const result = await importCommand({ templatePath, output: outputDir, lexicon: "no-such-lexicon" });
|
|
501
|
+
|
|
502
|
+
expect(result.success).toBe(false);
|
|
503
|
+
expect(result.error).toContain("no-such-lexicon");
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("content that is neither JSON nor YAML names both formats", async () => {
|
|
507
|
+
const templatePath = join(testDir, "notes.txt");
|
|
508
|
+
await writeFile(templatePath, "this is not a template\n{ nor is this");
|
|
509
|
+
|
|
510
|
+
const result = await importCommand({ templatePath, output: outputDir });
|
|
511
|
+
|
|
512
|
+
expect(result.success).toBe(false);
|
|
513
|
+
expect(result.error).toContain("JSON");
|
|
514
|
+
expect(result.error).toContain("YAML");
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// #2965 — detection outside a project, and inside one when the project's
|
|
519
|
+
// lexicons don't match.
|
|
520
|
+
describe("detectTemplateLexicon", () => {
|
|
521
|
+
let testDir: string;
|
|
522
|
+
|
|
523
|
+
const collector = {
|
|
524
|
+
receivers: { otlp: { protocols: { grpc: {} } } },
|
|
525
|
+
exporters: { debug: {} },
|
|
526
|
+
service: { pipelines: { traces: { receivers: ["otlp"], exporters: ["debug"] } } },
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
beforeEach(async () => {
|
|
530
|
+
testDir = join(tmpdir(), `chant-import-detect-test-${Date.now()}-${Math.random()}`);
|
|
531
|
+
await mkdir(testDir, { recursive: true });
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
afterEach(async () => {
|
|
535
|
+
await rm(testDir, { recursive: true, force: true });
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test("an empty directory detects an otel collector config from the installed lexicons", async () => {
|
|
539
|
+
const detection = await detectTemplateLexicon([collector], testDir);
|
|
540
|
+
expect(detection?.plugin.name).toBe("otel");
|
|
541
|
+
expect(detection?.source).toBe("installed");
|
|
542
|
+
}, 60000);
|
|
543
|
+
|
|
544
|
+
test("a project lexicon that matches decides", async () => {
|
|
545
|
+
await writeFile(join(testDir, "chant.config.json"), JSON.stringify({ lexicons: ["aws"] }));
|
|
546
|
+
const detection = await detectTemplateLexicon([{ AWSTemplateFormatVersion: "2010-09-09", Resources: {} }], testDir);
|
|
547
|
+
expect(detection?.plugin.name).toBe("aws");
|
|
548
|
+
expect(detection?.source).toBe("project");
|
|
549
|
+
}, 60000);
|
|
550
|
+
|
|
551
|
+
test("inside a project, installed lexicons are tried when the project's don't match", async () => {
|
|
552
|
+
await writeFile(join(testDir, "chant.config.json"), JSON.stringify({ lexicons: ["aws"] }));
|
|
553
|
+
const detection = await detectTemplateLexicon([collector], testDir);
|
|
554
|
+
expect(detection?.plugin.name).toBe("otel");
|
|
555
|
+
expect(detection?.source).toBe("installed");
|
|
556
|
+
}, 60000);
|
|
557
|
+
|
|
558
|
+
test("names the other installed lexicons that also recognized the template", async () => {
|
|
559
|
+
// github and forgejo both read a GitHub-Actions-shaped workflow.
|
|
560
|
+
const workflow = { on: { push: {} }, jobs: { build: { "runs-on": "ubuntu-latest", steps: [] } } };
|
|
561
|
+
const detection = await detectTemplateLexicon([workflow], testDir);
|
|
562
|
+
expect(detection).toBeDefined();
|
|
563
|
+
// github has a template parser and forgejo does not, so github is chosen.
|
|
564
|
+
expect(detection!.plugin.name).toBe("github");
|
|
565
|
+
expect(detection!.alsoMatched).toContain("forgejo");
|
|
566
|
+
}, 60000);
|
|
567
|
+
|
|
568
|
+
test("returns undefined when no installed lexicon recognizes the template", async () => {
|
|
569
|
+
expect(await detectTemplateLexicon([{ version: "1.0", unknownField: {} }], testDir)).toBeUndefined();
|
|
570
|
+
}, 60000);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
describe("listInstalledLexicons", () => {
|
|
574
|
+
test("lists the @intentius/chant-lexicon-* packages installed alongside chant", () => {
|
|
575
|
+
const names = listInstalledLexicons(tmpdir());
|
|
576
|
+
expect(names).toContain("aws");
|
|
577
|
+
expect(names).toContain("k8s");
|
|
578
|
+
expect(names).toContain("otel");
|
|
579
|
+
expect(names).toEqual([...names].sort());
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
describe("printImportResult", () => {
|
|
584
|
+
function printed(result: Parameters<typeof printImportResult>[0]): string {
|
|
585
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
586
|
+
try {
|
|
587
|
+
printImportResult(result);
|
|
588
|
+
return log.mock.calls.map((c) => c.join(" ")).join("\n");
|
|
589
|
+
} finally {
|
|
590
|
+
log.mockRestore();
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
test("says Detected lexicon when the lexicon was detected", () => {
|
|
595
|
+
expect(printed({ success: true, generatedFiles: [], warnings: [], lexicon: "k8s", detected: true })).toContain(
|
|
596
|
+
"Detected lexicon: k8s",
|
|
597
|
+
);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test("says Lexicon when the lexicon was named", () => {
|
|
601
|
+
const out = printed({ success: true, generatedFiles: [], warnings: [], lexicon: "k8s" });
|
|
602
|
+
expect(out).toContain("Lexicon: k8s");
|
|
603
|
+
expect(out).not.toContain("Detected");
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
describe("parseTemplateDocuments", () => {
|
|
608
|
+
test("JSON yields the parsed value as one document", () => {
|
|
609
|
+
expect(parseTemplateDocuments('{"a": 1}')).toEqual([{ a: 1 }]);
|
|
610
|
+
expect(parseTemplateDocuments("[1, 2]")).toEqual([[1, 2]]);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test("YAML splits on document separators and skips empty or comment-only documents", () => {
|
|
614
|
+
const docs = parseTemplateDocuments("---\n# only a comment\n---\na: 1\n---\nb: two\n---\n");
|
|
615
|
+
expect(docs).toEqual([{ a: 1 }, { b: "two" }]);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test("a top-level YAML list parses as a list (#2965)", () => {
|
|
619
|
+
expect(parseTemplateDocuments("- name: one\n expr: up\n- name: two\n")).toEqual([
|
|
620
|
+
[{ name: "one", expr: "up" }, { name: "two" }],
|
|
621
|
+
]);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test("commented separators and document ends split documents (#2965)", () => {
|
|
625
|
+
expect(parseTemplateDocuments("--- # first\na: 1\n...\n--- # second\nb: two\n...\n")).toEqual([
|
|
626
|
+
{ a: 1 },
|
|
627
|
+
{ b: "two" },
|
|
628
|
+
]);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
test("returns undefined for content that is neither", () => {
|
|
632
|
+
expect(parseTemplateDocuments("")).toBeUndefined();
|
|
633
|
+
expect(parseTemplateDocuments("just some words")).toBeUndefined();
|
|
634
|
+
expect(parseTemplateDocuments("{ broken json")).toBeUndefined();
|
|
635
|
+
});
|
|
636
|
+
});
|
|
@@ -3,8 +3,9 @@ import { join, resolve, basename, dirname } from "path";
|
|
|
3
3
|
import { formatSuccess, formatWarning, formatError } from "../format";
|
|
4
4
|
import type { TemplateIR, ResourceIR, ParameterIR, TemplateParser } from "../../import/parser";
|
|
5
5
|
import type { GeneratedFile, TypeScriptGenerator } from "../../import/generator";
|
|
6
|
-
import { loadPlugins, resolveProjectLexicons } from "../plugins";
|
|
6
|
+
import { listInstalledLexicons, loadPlugin, loadPlugins, resolveProjectLexicons } from "../plugins";
|
|
7
7
|
import type { LexiconPlugin, ResourceSelector } from "../../lexicon";
|
|
8
|
+
import { parseYAMLDocument, splitYAMLDocuments } from "../../yaml";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Import command options
|
|
@@ -16,6 +17,11 @@ export interface ImportOptions {
|
|
|
16
17
|
output?: string;
|
|
17
18
|
/** Force overwrite existing files */
|
|
18
19
|
force?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Lexicon whose parser handles the file (#2935). Skips detection and the
|
|
22
|
+
* JSON/YAML check: the raw content goes straight to that plugin's parser.
|
|
23
|
+
*/
|
|
24
|
+
lexicon?: string;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
/**
|
|
@@ -30,8 +36,13 @@ export interface ImportResult {
|
|
|
30
36
|
warnings: string[];
|
|
31
37
|
/** Error message if failed */
|
|
32
38
|
error?: string;
|
|
33
|
-
/**
|
|
39
|
+
/** The lexicon that handled the template */
|
|
34
40
|
lexicon?: string;
|
|
41
|
+
/**
|
|
42
|
+
* True when `lexicon` was found by template detection, false or absent when
|
|
43
|
+
* it was named (`--lexicon`, `--kustomize`) (#2965).
|
|
44
|
+
*/
|
|
45
|
+
detected?: boolean;
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
/**
|
|
@@ -40,18 +51,117 @@ export interface ImportResult {
|
|
|
40
51
|
type ResourceCategory = "storage" | "compute" | "network" | "other";
|
|
41
52
|
|
|
42
53
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
54
|
+
* Parse template content for detection (#2935): JSON first, then YAML. A YAML
|
|
55
|
+
* file is split into documents with core's `splitYAMLDocuments` (the k8s
|
|
56
|
+
* parser splits the same way), and each document is parsed on its own, so
|
|
57
|
+
* detection sees the same per-document objects the plugin's parser will. A
|
|
58
|
+
* document whose top level is a list parses as that list (#2965). JSON yields
|
|
59
|
+
* one document, the parsed value, exactly as before. Returns undefined when
|
|
60
|
+
* the content is neither: core's YAML reader is lenient and turns unparseable
|
|
61
|
+
* text into an empty mapping, so a file with no non-empty document is
|
|
62
|
+
* rejected.
|
|
63
|
+
*/
|
|
64
|
+
export function parseTemplateDocuments(content: string): unknown[] | undefined {
|
|
65
|
+
try {
|
|
66
|
+
return [JSON.parse(content)];
|
|
67
|
+
} catch {
|
|
68
|
+
// Not JSON: try YAML.
|
|
69
|
+
}
|
|
70
|
+
const documents: unknown[] = [];
|
|
71
|
+
for (const chunk of splitYAMLDocuments(content)) {
|
|
72
|
+
let doc: unknown;
|
|
73
|
+
try {
|
|
74
|
+
doc = parseYAMLDocument(chunk);
|
|
75
|
+
} catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (typeof doc === "object" && doc !== null && Object.keys(doc).length > 0) {
|
|
79
|
+
documents.push(doc);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return documents.length > 0 ? documents : undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The outcome of template detection (#2965). */
|
|
86
|
+
export interface TemplateDetection {
|
|
87
|
+
/** The plugin that handles the template. */
|
|
88
|
+
plugin: LexiconPlugin;
|
|
89
|
+
/**
|
|
90
|
+
* Where it came from: one of the project's lexicons, or an installed
|
|
91
|
+
* lexicon package tried because none of the project's matched.
|
|
92
|
+
*/
|
|
93
|
+
source: "project" | "installed";
|
|
94
|
+
/** Other installed lexicons that also recognized the deciding document. */
|
|
95
|
+
alsoMatched: string[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Find the plugins that recognize a template. Documents are tried in file
|
|
100
|
+
* order and the first one some plugin recognizes decides; every plugin that
|
|
101
|
+
* recognizes that document is returned, in the order given. For a JSON
|
|
102
|
+
* template there is exactly one document, so this is the old behaviour.
|
|
103
|
+
*/
|
|
104
|
+
function matchingPlugins(documents: unknown[], plugins: LexiconPlugin[]): LexiconPlugin[] {
|
|
105
|
+
for (const data of documents) {
|
|
106
|
+
const matches = plugins.filter((plugin) => {
|
|
107
|
+
try {
|
|
108
|
+
return plugin.detectTemplate?.(data) === true;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
if (matches.length > 0) return matches;
|
|
114
|
+
}
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Detect which lexicon handles a template (#2965). The project's lexicons
|
|
120
|
+
* (from chant.config, or the lexicons its source imports) are asked first, so
|
|
121
|
+
* inside a project nothing changes when one of them matches. When none does,
|
|
122
|
+
* or there is no project, every installed `@intentius/chant-lexicon-*`
|
|
123
|
+
* package that is not already a project lexicon is asked, in name order,
|
|
124
|
+
* and one with a `templateParser` is preferred over one without.
|
|
125
|
+
* Installed lexicons are loaded one at a time and without `init()`; one that
|
|
126
|
+
* fails to load is skipped. The chosen plugin is initialized before it is
|
|
127
|
+
* returned.
|
|
47
128
|
*/
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
129
|
+
export async function detectTemplateLexicon(
|
|
130
|
+
documents: unknown[],
|
|
131
|
+
projectDir: string,
|
|
132
|
+
): Promise<TemplateDetection | undefined> {
|
|
133
|
+
let projectNames: string[] = [];
|
|
134
|
+
let projectPlugins: LexiconPlugin[] = [];
|
|
135
|
+
try {
|
|
136
|
+
projectNames = await resolveProjectLexicons(projectDir);
|
|
137
|
+
projectPlugins = await loadPlugins(projectNames);
|
|
138
|
+
} catch {
|
|
139
|
+
projectPlugins = [];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const [fromProject] = matchingPlugins(documents, projectPlugins);
|
|
143
|
+
if (fromProject) return { plugin: fromProject, source: "project", alsoMatched: [] };
|
|
144
|
+
|
|
145
|
+
const installed: LexiconPlugin[] = [];
|
|
146
|
+
for (const name of listInstalledLexicons(projectDir)) {
|
|
147
|
+
if (projectNames.includes(name)) continue;
|
|
148
|
+
try {
|
|
149
|
+
installed.push(await loadPlugin(name));
|
|
150
|
+
} catch {
|
|
151
|
+
// Not loadable from here: it cannot handle the template either.
|
|
52
152
|
}
|
|
53
153
|
}
|
|
54
|
-
|
|
154
|
+
|
|
155
|
+
// A lexicon that can import the template goes ahead of one that only
|
|
156
|
+
// recognizes it (github and forgejo both read an Actions workflow).
|
|
157
|
+
const matches = matchingPlugins(documents, installed);
|
|
158
|
+
const [plugin, ...others] = [
|
|
159
|
+
...matches.filter((p) => p.templateParser),
|
|
160
|
+
...matches.filter((p) => !p.templateParser),
|
|
161
|
+
];
|
|
162
|
+
if (!plugin) return undefined;
|
|
163
|
+
await plugin.init?.();
|
|
164
|
+
return { plugin, source: "installed", alsoMatched: others.map((p) => p.name) };
|
|
55
165
|
}
|
|
56
166
|
|
|
57
167
|
/**
|
|
@@ -195,61 +305,63 @@ export async function importCommand(options: ImportOptions): Promise<ImportResul
|
|
|
195
305
|
};
|
|
196
306
|
}
|
|
197
307
|
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
308
|
+
// `--lexicon <name>` names the plugin, so there is nothing to detect and no
|
|
309
|
+
// format to check: the plugin's parser decides what it accepts (#2935).
|
|
310
|
+
if (options.lexicon) {
|
|
311
|
+
return importFromContent({
|
|
312
|
+
content,
|
|
313
|
+
lexicon: options.lexicon,
|
|
314
|
+
output: options.output,
|
|
315
|
+
force: options.force,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Parse for detection: JSON, falling back to YAML.
|
|
320
|
+
const documents = parseTemplateDocuments(content);
|
|
321
|
+
if (!documents) {
|
|
203
322
|
return {
|
|
204
323
|
success: false,
|
|
205
324
|
generatedFiles: [],
|
|
206
325
|
warnings: [],
|
|
207
|
-
error: "Template is
|
|
326
|
+
error: "Template is neither valid JSON nor YAML.",
|
|
208
327
|
};
|
|
209
328
|
}
|
|
210
329
|
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
330
|
+
// Detect from the output directory (or CWD) so that project config is
|
|
331
|
+
// found relative to where the user is working, not an arbitrary monorepo
|
|
332
|
+
// root.
|
|
214
333
|
const projectDir = resolve(options.output ? dirname(options.output) : ".");
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const lexiconNames = await resolveProjectLexicons(projectDir);
|
|
218
|
-
plugins = await loadPlugins(lexiconNames);
|
|
219
|
-
} catch {
|
|
220
|
-
plugins = [];
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// If no plugins resolved (no config, no source files), try common lexicons
|
|
224
|
-
if (plugins.length === 0) {
|
|
225
|
-
try {
|
|
226
|
-
plugins = await loadPlugins(["aws"]);
|
|
227
|
-
} catch {
|
|
228
|
-
// No lexicons available at all
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const plugin = detectPlugin(data, plugins);
|
|
233
|
-
if (!plugin) {
|
|
334
|
+
const detection = await detectTemplateLexicon(documents, projectDir);
|
|
335
|
+
if (!detection) {
|
|
234
336
|
return {
|
|
235
337
|
success: false,
|
|
236
338
|
generatedFiles: [],
|
|
237
339
|
warnings: [],
|
|
238
|
-
error:
|
|
340
|
+
error:
|
|
341
|
+
"Could not detect template lexicon. No installed lexicon recognizes this template. " +
|
|
342
|
+
"Pass --lexicon <name> to import it with a specific lexicon.",
|
|
239
343
|
};
|
|
240
344
|
}
|
|
241
345
|
|
|
242
|
-
const
|
|
346
|
+
const { plugin } = detection;
|
|
347
|
+
if (detection.alsoMatched.length > 0) {
|
|
348
|
+
warnings.push(
|
|
349
|
+
`The template is also recognized by ${detection.alsoMatched.join(", ")}. ` +
|
|
350
|
+
`Importing with ${plugin.name}; pass --lexicon <name> to choose another.`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
243
353
|
|
|
244
|
-
|
|
354
|
+
const result = parseAndWrite(plugin, content, outputDir, options.force, warnings, generatedFiles, plugin.name);
|
|
355
|
+
return { ...result, detected: true };
|
|
245
356
|
}
|
|
246
357
|
|
|
247
358
|
/**
|
|
248
359
|
* Import from an in-memory template string through a KNOWN plugin — no
|
|
249
360
|
* detection, no JSON assumption (#1548). This is the seam
|
|
250
361
|
* `chant import --kustomize <dir>` drives with `kustomize build` output
|
|
251
|
-
* through the k8s plugin's YAML parser
|
|
252
|
-
*
|
|
362
|
+
* through the k8s plugin's YAML parser, and `chant import <file> --lexicon
|
|
363
|
+
* <name>` drives with the file's content (#2935); `importCommand` above is
|
|
364
|
+
* the same pipeline behind file reading + JSON/YAML detection.
|
|
253
365
|
*/
|
|
254
366
|
export interface ContentImportOptions {
|
|
255
367
|
/** The raw template content (YAML or JSON — the plugin's parser decides). */
|
|
@@ -277,15 +389,6 @@ export async function importFromContent(options: ContentImportOptions): Promise<
|
|
|
277
389
|
if (!plugin) {
|
|
278
390
|
return { success: false, generatedFiles: [], warnings: [], error: `Lexicon "${options.lexicon}" not available.` };
|
|
279
391
|
}
|
|
280
|
-
if (!plugin.templateParser || !plugin.templateGenerator) {
|
|
281
|
-
return {
|
|
282
|
-
success: false,
|
|
283
|
-
generatedFiles: [],
|
|
284
|
-
warnings: [],
|
|
285
|
-
error: `Lexicon "${plugin.name}" does not support template import.`,
|
|
286
|
-
lexicon: plugin.name,
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
392
|
return parseAndWrite(plugin, options.content, outputDir, options.force, [], [], plugin.name);
|
|
290
393
|
}
|
|
291
394
|
|
|
@@ -299,10 +402,23 @@ function parseAndWrite(
|
|
|
299
402
|
generatedFiles: string[],
|
|
300
403
|
lexicon: string,
|
|
301
404
|
): ImportResult {
|
|
405
|
+
// A lexicon can recognize a template (detectTemplate) without being able to
|
|
406
|
+
// import it (grafana has no parser). Every path funnels through here, so
|
|
407
|
+
// this one check covers detection, --lexicon and content import (#2940).
|
|
408
|
+
if (!plugin.templateParser || !plugin.templateGenerator) {
|
|
409
|
+
return {
|
|
410
|
+
success: false,
|
|
411
|
+
generatedFiles: [],
|
|
412
|
+
warnings: [],
|
|
413
|
+
error: `lexicon "${plugin.name}" does not support template import`,
|
|
414
|
+
lexicon: plugin.name,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
302
418
|
// Parse template
|
|
303
419
|
let ir: TemplateIR;
|
|
304
420
|
try {
|
|
305
|
-
const parser = plugin.templateParser
|
|
421
|
+
const parser = plugin.templateParser();
|
|
306
422
|
ir = parser.parse(content);
|
|
307
423
|
} catch (err) {
|
|
308
424
|
return {
|
|
@@ -319,7 +435,7 @@ function parseAndWrite(
|
|
|
319
435
|
warnings.push(...ir.warnings);
|
|
320
436
|
}
|
|
321
437
|
|
|
322
|
-
const generator = plugin.templateGenerator
|
|
438
|
+
const generator = plugin.templateGenerator();
|
|
323
439
|
|
|
324
440
|
// Check output directory
|
|
325
441
|
if (existsSync(outputDir) && !force) {
|
|
@@ -545,6 +661,7 @@ export async function liveImportFromPlugins(
|
|
|
545
661
|
generatedFiles,
|
|
546
662
|
warnings,
|
|
547
663
|
lexicon: generatorLexicon.name,
|
|
664
|
+
detected: !options.lexicon,
|
|
548
665
|
};
|
|
549
666
|
}
|
|
550
667
|
|
|
@@ -562,7 +679,7 @@ export function printImportResult(result: ImportResult): void {
|
|
|
562
679
|
}
|
|
563
680
|
|
|
564
681
|
if (result.lexicon) {
|
|
565
|
-
console.log(
|
|
682
|
+
console.log(`${result.detected ? "Detected lexicon" : "Lexicon"}: ${result.lexicon}`);
|
|
566
683
|
}
|
|
567
684
|
|
|
568
685
|
if (result.generatedFiles.length > 0) {
|
package/src/cli/handlers/misc.ts
CHANGED
|
@@ -374,10 +374,13 @@ export async function runImport(ctx: CommandContext): Promise<number> {
|
|
|
374
374
|
return result.success ? 0 : 1;
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
// A template file: JSON or YAML, detected, unless `--lexicon` names the
|
|
378
|
+
// plugin (#2935).
|
|
377
379
|
const result = await importCommand({
|
|
378
380
|
templatePath: ctx.args.path,
|
|
379
381
|
output: ctx.args.output,
|
|
380
382
|
force: ctx.args.force,
|
|
383
|
+
lexicon: ctx.args.lexicon,
|
|
381
384
|
});
|
|
382
385
|
|
|
383
386
|
printImportResult(result);
|
|
@@ -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
|
*
|
package/src/yaml.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
|
-
import { emitYAML, parseYAML, parseScalar } from "./yaml";
|
|
2
|
+
import { emitYAML, parseYAML, parseYAMLDocument, parseScalar, splitYAMLDocuments } from "./yaml";
|
|
3
3
|
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
// emitYAML
|
|
@@ -538,3 +538,55 @@ describe("parseYAML — a colon without following space is not a mapping (#2013)
|
|
|
538
538
|
});
|
|
539
539
|
});
|
|
540
540
|
});
|
|
541
|
+
|
|
542
|
+
// ---------------------------------------------------------------------------
|
|
543
|
+
// parseYAMLDocument / splitYAMLDocuments (#2965)
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
|
|
546
|
+
describe("parseYAMLDocument", () => {
|
|
547
|
+
test("a top-level list parses as a list", () => {
|
|
548
|
+
expect(parseYAMLDocument("- a\n- b\n")).toEqual(["a", "b"]);
|
|
549
|
+
expect(parseYAMLDocument("# rules\n\n- name: one\n expr: up\n- name: two\n")).toEqual([
|
|
550
|
+
{ name: "one", expr: "up" },
|
|
551
|
+
{ name: "two" },
|
|
552
|
+
]);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
test("parseYAML still returns a mapping for the same input", () => {
|
|
556
|
+
expect(parseYAML("- a\n- b\n")).toEqual({});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("a mapping and JSON parse as parseYAML parses them", () => {
|
|
560
|
+
expect(parseYAMLDocument("a: 1\nb:\n - x\n")).toEqual({ a: 1, b: ["x"] });
|
|
561
|
+
expect(parseYAMLDocument("[1, 2]")).toEqual([1, 2]);
|
|
562
|
+
expect(parseYAMLDocument("")).toEqual({});
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
describe("splitYAMLDocuments", () => {
|
|
567
|
+
test("splits on bare --- separators", () => {
|
|
568
|
+
expect(splitYAMLDocuments("a: 1\n---\nb: 2\n")).toEqual(["a: 1", "b: 2\n"]);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
test("a separator may carry a trailing comment", () => {
|
|
572
|
+
expect(splitYAMLDocuments("--- # first\na: 1\n--- # second\nb: 2")).toEqual(["a: 1", "b: 2"]);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test("... ends a document", () => {
|
|
576
|
+
expect(splitYAMLDocuments("a: 1\n...\nb: 2\n... # done\n")).toEqual(["a: 1", "b: 2"]);
|
|
577
|
+
expect(splitYAMLDocuments("---\na: 1\n...\n---\nb: 2\n...\n")).toEqual(["a: 1", "b: 2"]);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test("drops empty and comment-only documents", () => {
|
|
581
|
+
expect(splitYAMLDocuments("# header\n---\n\n---\na: 1\n---\n# trailing\n")).toEqual(["a: 1"]);
|
|
582
|
+
expect(splitYAMLDocuments("")).toEqual([]);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test("only a whole-line marker splits", () => {
|
|
586
|
+
expect(splitYAMLDocuments("a: ---\nb: |\n ---\n---x: 1\n")).toEqual(["a: ---\nb: |\n ---\n---x: 1\n"]);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test("normalizes CRLF line endings", () => {
|
|
590
|
+
expect(splitYAMLDocuments("a: 1\r\n--- # c\r\nb: 2\r\n")).toEqual(["a: 1", "b: 2\n"]);
|
|
591
|
+
});
|
|
592
|
+
});
|
package/src/yaml.ts
CHANGED
|
@@ -193,6 +193,54 @@ export function parseYAML(content: string): Record<string, unknown> {
|
|
|
193
193
|
return parseYAMLLines(lines, 0, 0).value as Record<string, unknown>;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Parse one YAML (or JSON) document whose top level may be a sequence
|
|
198
|
+
* (#2965). {@link parseYAML} reads only a top-level mapping and returns `{}`
|
|
199
|
+
* for a file like `- a\n- b`; its callers rely on getting a mapping back, so
|
|
200
|
+
* it keeps doing that. This returns the list instead, and otherwise defers to
|
|
201
|
+
* `parseYAML`.
|
|
202
|
+
*/
|
|
203
|
+
export function parseYAMLDocument(content: string): unknown {
|
|
204
|
+
try {
|
|
205
|
+
return JSON.parse(content);
|
|
206
|
+
} catch {
|
|
207
|
+
// Fall through to YAML parsing
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const lines = content.replace(/\r\n?/g, "\n").split("\n");
|
|
211
|
+
const first = lines.findIndex((line) => line.trim() !== "" && !line.trim().startsWith("#"));
|
|
212
|
+
if (first !== -1 && /^\s*- /.test(lines[first])) {
|
|
213
|
+
return parseYAMLArray(lines, first, lines[first].search(/\S/)).value;
|
|
214
|
+
}
|
|
215
|
+
return parseYAMLLines(lines, 0, 0).value;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** A document start (`---`) or end (`...`) marker, optionally followed by a comment. */
|
|
219
|
+
const DOCUMENT_MARKER = /^(?:---|\.\.\.)(?:[ \t]+#.*)?[ \t]*$/;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Split a YAML stream into its documents (#2965). A line holding only `---`
|
|
223
|
+
* starts a document and a line holding only `...` ends one; either may carry
|
|
224
|
+
* a trailing `# comment`. Documents that are empty or hold only comments are
|
|
225
|
+
* dropped. The text of each document is returned unparsed, with `\r\n`
|
|
226
|
+
* normalized to `\n`.
|
|
227
|
+
*/
|
|
228
|
+
export function splitYAMLDocuments(content: string): string[] {
|
|
229
|
+
const documents: string[] = [];
|
|
230
|
+
let current: string[] = [];
|
|
231
|
+
const flush = (): void => {
|
|
232
|
+
const text = current.join("\n");
|
|
233
|
+
if (text.replace(/#[^\n]*/g, "").trim() !== "") documents.push(text);
|
|
234
|
+
current = [];
|
|
235
|
+
};
|
|
236
|
+
for (const line of content.replace(/\r\n?/g, "\n").split("\n")) {
|
|
237
|
+
if (DOCUMENT_MARKER.test(line)) flush();
|
|
238
|
+
else current.push(line);
|
|
239
|
+
}
|
|
240
|
+
flush();
|
|
241
|
+
return documents;
|
|
242
|
+
}
|
|
243
|
+
|
|
196
244
|
/**
|
|
197
245
|
* A block-scalar header: `|` (literal, keep newlines) or `>` (folded, newlines →
|
|
198
246
|
* spaces), with a chomping indicator (`-` strip trailing newlines, `+` keep them,
|