@intentius/chant 0.18.17 → 0.18.18

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.
@@ -1 +1 @@
1
- {"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../../../../../src/cli/commands/init-lexicon/templates/lint.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAoB1E;AAED,wBAAgB,wBAAwB,IAAI,MAAM,CAGjD"}
1
+ {"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../../../../../src/cli/commands/init-lexicon/templates/lint.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAoB1E;AAED,wBAAgB,wBAAwB,IAAI,MAAM,CASjD"}
@@ -5,6 +5,14 @@ export declare function generatePackageJson(name: string, names: {
5
5
  packageName: string;
6
6
  }): string;
7
7
  export declare function generateTsConfig(): string;
8
+ /**
9
+ * Build config used by `npm run build` (and CI's `tsc --noEmit`). Uses `bundler`
10
+ * resolution + the `development` condition so `@intentius/chant/*` resolves to the
11
+ * workspace source, and excludes tests/docs — the same setup the shipped lexicons
12
+ * use. (The plain `tsconfig.json` extends the monorepo root and can't be tsc'd on
13
+ * its own.)
14
+ */
15
+ export declare function generateTsConfigBuild(): string;
8
16
  export declare function generateJustfile(name: string): string;
9
17
  export declare function generateGitignore(): string;
10
18
  export declare function generateReadme(name: string, names: {
@@ -1 +1 @@
1
- {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../../../../src/cli/commands/init-lexicon/templates/project.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CA6BxF;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAWzC;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA4BrD;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAK1C;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CA4BnF;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE;IAAE,iBAAiB,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CA0BnH;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA8BvD;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAM9C"}
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../../../../src/cli/commands/init-lexicon/templates/project.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CA+BxF;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAWzC;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAmB9C;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA4BrD;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAK1C;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CA4BnF;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE;IAAE,iBAAiB,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CA0BnH;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA8BvD;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAO9C"}
@@ -1 +1 @@
1
- {"version":3,"file":"spec.d.ts","sourceRoot":"","sources":["../../../../../src/cli/commands/init-lexicon/templates/spec.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,wBAAgB,mBAAmB,IAAI,MAAM,CAuB5C;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CA2B5C"}
1
+ {"version":3,"file":"spec.d.ts","sourceRoot":"","sources":["../../../../../src/cli/commands/init-lexicon/templates/spec.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,wBAAgB,mBAAmB,IAAI,MAAM,CAwB5C;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CA2B5C"}
@@ -1 +1 @@
1
- {"version":3,"file":"init-lexicon.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/init-lexicon.ts"],"names":[],"mappings":"AAeA;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,0BAA0B;IAC1B,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAQD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM;;;;;EAQvC;AAqBD;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAoHhG;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBrF"}
1
+ {"version":3,"file":"init-lexicon.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/init-lexicon.ts"],"names":[],"mappings":"AAeA;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,0BAA0B;IAC1B,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAQD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM;;;;;EAQvC;AAqBD;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAqHhG;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBrF"}
@@ -1 +1 @@
1
- {"version":3,"file":"yaml.d.ts","sourceRoot":"","sources":["../src/yaml.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CA2G/D;AAMD,sCAAsC;AACtC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASlE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,WAAW,CAuEb;AAsDD;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,WAAW,CAkEb;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAelD"}
1
+ {"version":3,"file":"yaml.d.ts","sourceRoot":"","sources":["../src/yaml.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CA2G/D;AAMD,sCAAsC;AACtC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;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;AA+DD;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,WAAW,CAyEb;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAelD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.18.17",
3
+ "version": "0.18.18",
4
4
  "description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -18,12 +18,12 @@
18
18
  "generate": "npx tsx src/codegen/generate-cli.ts",
19
19
  "validate": "npx tsx src/validate-cli.ts",
20
20
  "docs": "npx tsx src/codegen/docs-cli.ts",
21
- "prepack": "npm run generate && npm run validate"
22
- },
23
- "dependencies": {
24
- "@intentius/chant": "workspace:*"
21
+ "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
22
+ "prepack": "npm run generate && npm run validate && npm run build"
25
23
  },
26
24
  "devDependencies": {
25
+ "@intentius/chant": "*",
26
+ "tsc-alias": "^1.8.17",
27
27
  "typescript": "^5.9.3"
28
28
  }
29
29
  }
@@ -8,7 +8,7 @@ import { fileURLToPath } from "url";
8
8
  *
9
9
  * Each callback has a TODO describing what to implement.
10
10
  */
11
- export async function generate(options?: { verbose?: boolean }): Promise<GenerateResult> {
11
+ export async function generate(options?: { verbose?: boolean; force?: boolean }): Promise<GenerateResult> {
12
12
  const result = await generatePipeline({
13
13
  // Must return Map<typeName, Buffer> — each entry is one schema file.
14
14
  // Example: fetch a zip, extract JSON files, key by type name.
@@ -49,7 +49,7 @@ export async function generate(options?: { verbose?: boolean }): Promise<Generat
49
49
  generateRuntimeIndex: (results, naming) => {
50
50
  throw new Error("TODO: implement generateRuntimeIndex — produce index.ts content");
51
51
  },
52
- });
52
+ }, options);
53
53
 
54
54
  if (options?.verbose) {
55
55
  console.error(`Generated ${result.resources} resources, ${result.properties} property types`);
@@ -1 +1,7 @@
1
+ import type { LintRule } from "@intentius/chant/lint/rule";
2
+ import { sampleRule } from "./sample";
3
+
1
4
  export { sampleRule } from "./sample";
5
+
6
+ /** All lint rules provided by this lexicon (imported by plugin.ts's lintRules()). */
7
+ export const rules: LintRule[] = [sampleRule];
@@ -9,13 +9,14 @@ const CACHE_FILE = ".cache/schemas.zip";
9
9
  *
10
10
  * TODO: Point SCHEMA_URL at your real upstream schema source.
11
11
  */
12
- export async function fetchSchemas(options?: { force?: boolean }): Promise<Map<string, string>> {
13
- const zipData = await fetchWithCache({
14
- url: SCHEMA_URL,
15
- cacheFile: CACHE_FILE,
16
- force: options?.force,
17
- });
12
+ export async function fetchSchemas(options?: { force?: boolean }): Promise<Map<string, Buffer>> {
13
+ // `force` is the second argument to fetchWithCache (not a FetchConfig field).
14
+ const zipData = await fetchWithCache(
15
+ { url: SCHEMA_URL, cacheFile: CACHE_FILE },
16
+ options?.force,
17
+ );
18
18
 
19
- // TODO: Adjust the filter to match your schema file names
19
+ // TODO: Adjust the filter to match your schema file names. extractFromZip yields
20
+ // Buffers — decode with `.toString()` in parse.ts as needed.
20
21
  return extractFromZip(zipData, (name) => name.endsWith(".json"));
21
22
  }
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { validate } from "./validate";
3
3
 
4
- await validate({ verbose: true });
4
+ // `validate` takes an optional { basePath }; defaults to the lexicon root.
5
+ await validate();
@@ -0,0 +1,27 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": false,
5
+ "declaration": true,
6
+ "declarationMap": true,
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "paths": {},
10
+ "moduleResolution": "bundler",
11
+ "customConditions": [
12
+ "development"
13
+ ]
14
+ },
15
+ "include": [
16
+ "src/**/*"
17
+ ],
18
+ "exclude": [
19
+ "**/*.test.ts",
20
+ "node_modules",
21
+ "dist",
22
+ "docs"
23
+ ],
24
+ "tsc-alias": {
25
+ "resolveFullPaths": true
26
+ }
27
+ }
@@ -11,7 +11,7 @@ import { fileURLToPath } from "url";
11
11
  *
12
12
  * Each callback has a TODO describing what to implement.
13
13
  */
14
- export async function generate(options?: { verbose?: boolean }): Promise<GenerateResult> {
14
+ export async function generate(options?: { verbose?: boolean; force?: boolean }): Promise<GenerateResult> {
15
15
  const result = await generatePipeline({
16
16
  // Must return Map<typeName, Buffer> — each entry is one schema file.
17
17
  // Example: fetch a zip, extract JSON files, key by type name.
@@ -52,7 +52,7 @@ export async function generate(options?: { verbose?: boolean }): Promise<Generat
52
52
  generateRuntimeIndex: (results, naming) => {
53
53
  throw new Error("TODO: implement generateRuntimeIndex — produce index.ts content");
54
54
  },
55
- });
55
+ }, options);
56
56
 
57
57
  if (options?.verbose) {
58
58
  console.error(\`Generated \${result.resources} resources, \${result.properties} property types\`);
@@ -13,7 +13,7 @@ import { fileURLToPath } from "url";
13
13
  *
14
14
  * Each callback has a TODO describing what to implement.
15
15
  */
16
- export async function generate(options?: { verbose?: boolean }): Promise<GenerateResult> {
16
+ export async function generate(options?: { verbose?: boolean; force?: boolean }): Promise<GenerateResult> {
17
17
  const result = await generatePipeline({
18
18
  // Must return Map<typeName, Buffer> — each entry is one schema file.
19
19
  // Example: fetch a zip, extract JSON files, key by type name.
@@ -54,7 +54,7 @@ export async function generate(options?: { verbose?: boolean }): Promise<Generat
54
54
  generateRuntimeIndex: (results, naming) => {
55
55
  throw new Error("TODO: implement generateRuntimeIndex — produce index.ts content");
56
56
  },
57
- });
57
+ }, options);
58
58
 
59
59
  if (options?.verbose) {
60
60
  console.error(\`Generated \${result.resources} resources, \${result.properties} property types\`);
@@ -25,6 +25,12 @@ export const sampleRule: LintRule = {
25
25
  }
26
26
 
27
27
  export function generateLintRulesIndexTs(): string {
28
- return `export { sampleRule } from "./sample";
28
+ return `import type { LintRule } from "@intentius/chant/lint/rule";
29
+ import { sampleRule } from "./sample";
30
+
31
+ export { sampleRule } from "./sample";
32
+
33
+ /** All lint rules provided by this lexicon (imported by plugin.ts's lintRules()). */
34
+ export const rules: LintRule[] = [sampleRule];
29
35
  `;
30
36
  }
@@ -20,12 +20,14 @@ export function generatePackageJson(name: string, names: { packageName: string }
20
20
  generate: "npx tsx src/codegen/generate-cli.ts",
21
21
  validate: "npx tsx src/validate-cli.ts",
22
22
  docs: "npx tsx src/codegen/docs-cli.ts",
23
- prepack: "npm run generate && npm run validate",
24
- },
25
- dependencies: {
26
- "@intentius/chant": "workspace:*",
23
+ build: "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
24
+ prepack: "npm run generate && npm run validate && npm run build",
27
25
  },
28
26
  devDependencies: {
27
+ // `*` (not `workspace:*`) so a fresh lexicon `npm install`s under plain npm
28
+ // (the `workspace:` protocol is rejected outside a workspace).
29
+ "@intentius/chant": "*",
30
+ "tsc-alias": "^1.8.17",
29
31
  typescript: "^5.9.3",
30
32
  },
31
33
  };
@@ -46,6 +48,34 @@ export function generateTsConfig(): string {
46
48
  return JSON.stringify(config, null, 2) + "\n";
47
49
  }
48
50
 
51
+ /**
52
+ * Build config used by `npm run build` (and CI's `tsc --noEmit`). Uses `bundler`
53
+ * resolution + the `development` condition so `@intentius/chant/*` resolves to the
54
+ * workspace source, and excludes tests/docs — the same setup the shipped lexicons
55
+ * use. (The plain `tsconfig.json` extends the monorepo root and can't be tsc'd on
56
+ * its own.)
57
+ */
58
+ export function generateTsConfigBuild(): string {
59
+ const config = {
60
+ extends: "../../tsconfig.json",
61
+ compilerOptions: {
62
+ noEmit: false,
63
+ declaration: true,
64
+ declarationMap: true,
65
+ outDir: "dist",
66
+ rootDir: "src",
67
+ paths: {},
68
+ moduleResolution: "bundler",
69
+ customConditions: ["development"],
70
+ },
71
+ include: ["src/**/*"],
72
+ exclude: ["**/*.test.ts", "node_modules", "dist", "docs"],
73
+ "tsc-alias": { resolveFullPaths: true },
74
+ };
75
+
76
+ return JSON.stringify(config, null, 2) + "\n";
77
+ }
78
+
49
79
  export function generateJustfile(name: string): string {
50
80
  return `# Default recipe - list all available commands
51
81
  default:
@@ -177,6 +207,7 @@ export function generateValidateCliTs(): string {
177
207
  return `#!/usr/bin/env tsx
178
208
  import { validate } from "./validate";
179
209
 
180
- await validate({ verbose: true });
210
+ // \`validate\` takes an optional { basePath }; defaults to the lexicon root.
211
+ await validate();
181
212
  `;
182
213
  }
@@ -14,14 +14,15 @@ const CACHE_FILE = ".cache/schemas.zip";
14
14
  *
15
15
  * TODO: Point SCHEMA_URL at your real upstream schema source.
16
16
  */
17
- export async function fetchSchemas(options?: { force?: boolean }): Promise<Map<string, string>> {
18
- const zipData = await fetchWithCache({
19
- url: SCHEMA_URL,
20
- cacheFile: CACHE_FILE,
21
- force: options?.force,
22
- });
17
+ export async function fetchSchemas(options?: { force?: boolean }): Promise<Map<string, Buffer>> {
18
+ // \`force\` is the second argument to fetchWithCache (not a FetchConfig field).
19
+ const zipData = await fetchWithCache(
20
+ { url: SCHEMA_URL, cacheFile: CACHE_FILE },
21
+ options?.force,
22
+ );
23
23
 
24
- // TODO: Adjust the filter to match your schema file names
24
+ // TODO: Adjust the filter to match your schema file names. extractFromZip yields
25
+ // Buffers — decode with \`.toString()\` in parse.ts as needed.
25
26
  return extractFromZip(zipData, (name) => name.endsWith(".json"));
26
27
  }
27
28
  `;
@@ -232,6 +232,39 @@ describe("scaffold content validation", () => {
232
232
  // AWS references
233
233
  expect(content).toContain("lexicons/aws/");
234
234
  });
235
+
236
+ // Drift guards — keep the scaffold compiling/installing against current core (#749).
237
+ test("package.json installs under plain npm and can build (#749)", () => {
238
+ const pkg = JSON.parse(readFileSync(join(targetDir, "package.json"), "utf-8"));
239
+ expect(JSON.stringify(pkg)).not.toContain("workspace:"); // rejected by plain npm
240
+ expect(pkg.devDependencies["@intentius/chant"]).toBe("*");
241
+ expect(pkg.dependencies).toBeUndefined();
242
+ expect(pkg.devDependencies["tsc-alias"]).toBeTruthy();
243
+ expect(pkg.scripts.build).toContain("tsconfig.build.json");
244
+ });
245
+
246
+ test("tsconfig.build.json uses bundler resolution + the development condition (#749)", () => {
247
+ const cfg = JSON.parse(readFileSync(join(targetDir, "tsconfig.build.json"), "utf-8"));
248
+ expect(cfg.compilerOptions.moduleResolution).toBe("bundler");
249
+ expect(cfg.compilerOptions.customConditions).toContain("development");
250
+ });
251
+
252
+ test("spec/fetch.ts matches fetchWithCache(config, force) → Map<string, Buffer> (#749)", () => {
253
+ const content = readFileSync(join(targetDir, "src/spec/fetch.ts"), "utf-8");
254
+ expect(content).toContain("Promise<Map<string, Buffer>>");
255
+ expect(content).not.toMatch(/force:\s*options/); // force is the 2nd arg, not a FetchConfig field
256
+ });
257
+
258
+ test("validate-cli.ts calls validate() with no verbose arg (#749)", () => {
259
+ const content = readFileSync(join(targetDir, "src/validate-cli.ts"), "utf-8");
260
+ expect(content).toContain("await validate();");
261
+ expect(content).not.toContain("verbose");
262
+ });
263
+
264
+ test("lint/rules/index.ts exports a `rules` array (plugin.ts imports it) (#749)", () => {
265
+ const content = readFileSync(join(targetDir, "src/lint/rules/index.ts"), "utf-8");
266
+ expect(content).toContain("export const rules");
267
+ });
235
268
  });
236
269
 
237
270
  // ── Fixture snapshot tests ──────────────────────────────────────────
@@ -8,7 +8,7 @@ import { generateCodegenGenerateTs, generateCodegenGenerateCliTs, generateCodege
8
8
  import { generateSpecFetchTs, generateSpecParseTs } from "./init-lexicon/templates/spec";
9
9
  import { generateSampleRuleTs, generateLintRulesIndexTs } from "./init-lexicon/templates/lint";
10
10
  import { generateLspCompletionsTs, generateLspHoverTs } from "./init-lexicon/templates/lsp";
11
- import { generatePackageJson, generateTsConfig, generateJustfile, generateGitignore, generateReadme, generateSerializerTs, generateValidateTs, generateValidateCliTs } from "./init-lexicon/templates/project";
11
+ import { generatePackageJson, generateTsConfig, generateTsConfigBuild, generateJustfile, generateGitignore, generateReadme, generateSerializerTs, generateValidateTs, generateValidateCliTs } from "./init-lexicon/templates/project";
12
12
  import { generatePluginTestTs, generateSerializerTestTs, generateCompletionsTestTs, generateHoverTestTs } from "./init-lexicon/templates/tests";
13
13
  import { generateDocsPackageJson, generateDocsTsConfig, generateDocsAstroConfig, generateDocsContentConfig, generateDocsIndexMdx } from "./init-lexicon/templates/docs";
14
14
  import { generateExamplePackageJson, generateExampleInfraTs } from "./init-lexicon/templates/examples";
@@ -150,6 +150,7 @@ export async function initLexiconCommand(options: InitLexiconOptions): Promise<I
150
150
  "src/validate-cli.ts": generateValidateCliTs(),
151
151
  "package.json": generatePackageJson(name, names),
152
152
  "tsconfig.json": generateTsConfig(),
153
+ "tsconfig.build.json": generateTsConfigBuild(),
153
154
  "justfile": generateJustfile(name),
154
155
  ".gitignore": generateGitignore(),
155
156
  "README.md": generateReadme(name, names),
package/src/yaml.test.ts CHANGED
@@ -211,3 +211,36 @@ describe("parseScalar", () => {
211
211
  expect(parseScalar("hello")).toBe("hello");
212
212
  });
213
213
  });
214
+
215
+ // Block scalars (#910) — round-trip parity with js-yaml for literal/folded/chomping,
216
+ // including block scalars nested inside array items (the case that mis-parsed).
217
+ describe("parseYAML block scalars (#910)", () => {
218
+ test("top-level literal | keeps newlines, clips to one trailing", () => {
219
+ expect(parseYAML("run: |\n line1\n line2\n")).toEqual({ run: "line1\nline2\n" });
220
+ });
221
+
222
+ test("literal | nested inside a later array item (the reported bug)", () => {
223
+ expect(
224
+ parseYAML("steps:\n - name: first\n run: echo hi\n - name: second\n run: |\n line1\n line2\n"),
225
+ ).toEqual({
226
+ steps: [
227
+ { name: "first", run: "echo hi" },
228
+ { name: "second", run: "line1\nline2\n" },
229
+ ],
230
+ });
231
+ });
232
+
233
+ test("block scalar as an array item's first key, with a sibling key after", () => {
234
+ expect(parseYAML("steps:\n - run: |\n a\n b\n name: x\n")).toEqual({
235
+ steps: [{ run: "a\nb\n", name: "x" }],
236
+ });
237
+ });
238
+
239
+ test("strip |- drops the trailing newline; a sibling key after still parses", () => {
240
+ expect(parseYAML("a: |-\n x\n y\nb: 2\n")).toEqual({ a: "x\ny", b: 2 });
241
+ });
242
+
243
+ test("folded > folds line breaks to spaces, blank line to one newline", () => {
244
+ expect(parseYAML("msg: >\n a\n b\n\n c\n")).toEqual({ msg: "a b\nc\n" });
245
+ });
246
+ });
package/src/yaml.ts CHANGED
@@ -155,6 +155,85 @@ export function parseYAML(content: string): Record<string, unknown> {
155
155
  return parseYAMLLines(lines, 0, 0).value as Record<string, unknown>;
156
156
  }
157
157
 
158
+ /**
159
+ * A block-scalar header: `|` (literal, keep newlines) or `>` (folded, newlines →
160
+ * spaces), with a chomping indicator (`-` strip trailing newlines, `+` keep them,
161
+ * default clip to a single one). Returns null when `inline` isn't a block header.
162
+ */
163
+ interface BlockScalarHeader {
164
+ style: "literal" | "folded";
165
+ chomp: "clip" | "strip" | "keep";
166
+ }
167
+ function blockScalarHeader(inline: string): BlockScalarHeader | null {
168
+ const m = inline.match(/^([|>])([+-]?)(?:\s+#.*)?$/);
169
+ if (!m) return null;
170
+ return {
171
+ style: m[1] === "|" ? "literal" : "folded",
172
+ chomp: m[2] === "-" ? "strip" : m[2] === "+" ? "keep" : "clip",
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Parse a block scalar's body — the lines indented past `parentIndent`, dedented
178
+ * by the block's own indent (the first content line's). Handles literal/folded
179
+ * styles and clip/strip/keep chomping, matching js-yaml. Returns the string value
180
+ * and the index of the first line that is NOT part of the block.
181
+ */
182
+ function parseBlockScalar(
183
+ lines: string[],
184
+ startIndex: number,
185
+ parentIndent: number,
186
+ header: BlockScalarHeader,
187
+ ): { value: string; endIndex: number } {
188
+ const raw: string[] = [];
189
+ let blockIndent = -1;
190
+ let i = startIndex;
191
+ for (; i < lines.length; i++) {
192
+ const line = lines[i];
193
+ if (line.trim() === "") {
194
+ raw.push(""); // a blank line is part of the block (kept/chomped later)
195
+ continue;
196
+ }
197
+ const ni = line.search(/\S/);
198
+ if (ni <= parentIndent) break; // dedented out of the block
199
+ if (blockIndent === -1) blockIndent = ni;
200
+ if (ni < blockIndent) break;
201
+ raw.push(line.slice(blockIndent));
202
+ }
203
+ // Trailing blank lines belong to the block (chomping decides their fate).
204
+ let text: string;
205
+ if (header.style === "folded") {
206
+ // Fold each run of non-empty lines into a space-joined paragraph; a run of N
207
+ // blank lines between paragraphs becomes N newlines (a single blank → one
208
+ // newline), matching YAML folded semantics.
209
+ text = "";
210
+ let buf: string[] = [];
211
+ let blankRun = 0;
212
+ let wrote = false;
213
+ const flush = (): void => {
214
+ if (buf.length === 0) return;
215
+ if (wrote) text += "\n".repeat(blankRun);
216
+ text += buf.join(" ");
217
+ buf = [];
218
+ blankRun = 0;
219
+ wrote = true;
220
+ };
221
+ for (const l of raw) {
222
+ if (l === "") { flush(); blankRun++; }
223
+ else buf.push(l);
224
+ }
225
+ flush();
226
+ } else {
227
+ text = raw.join("\n");
228
+ }
229
+ const trailing = text.match(/\n*$/)?.[0].length ?? 0;
230
+ const stripped = text.replace(/\n+$/, "");
231
+ if (header.chomp === "strip") text = stripped;
232
+ else if (header.chomp === "keep") text = stripped + "\n".repeat(Math.max(trailing, raw.length > 0 ? 1 : 0));
233
+ else text = stripped === "" ? "" : stripped + "\n"; // clip
234
+ return { value: text, endIndex: i };
235
+ }
236
+
158
237
  /**
159
238
  * Parse indentation-based YAML lines into a key-value object.
160
239
  */
@@ -222,8 +301,15 @@ export function parseYAMLLines(
222
301
  }
223
302
  i++;
224
303
  } else {
225
- result[key] = parseScalar(inlineValue);
226
- i++;
304
+ const header = blockScalarHeader(inlineValue);
305
+ if (header) {
306
+ const block = parseBlockScalar(lines, i + 1, indent, header);
307
+ result[key] = block.value;
308
+ i = block.endIndex;
309
+ } else {
310
+ result[key] = parseScalar(inlineValue);
311
+ i++;
312
+ }
227
313
  }
228
314
  } else if (line.trimStart().startsWith("- ")) {
229
315
  break;
@@ -246,6 +332,15 @@ function parseArrayItemValue(
246
332
  childIndent: number,
247
333
  ): unknown {
248
334
  if (inlineValue !== "" && !inlineValue.startsWith("#")) {
335
+ const header = blockScalarHeader(inlineValue);
336
+ if (header) {
337
+ // Block scalar nested under an array-item key (e.g. `- name: x\n run: |`).
338
+ // The body is indented past the key's column. On a `- key: |` line the key
339
+ // sits 2 cols past the dash; on its own line it's the line's indent (#910).
340
+ const dash = lines[currentIndex].match(/^(\s*)- /);
341
+ const keyIndent = dash ? dash[1].length + 2 : lines[currentIndex].search(/\S/);
342
+ return parseBlockScalar(lines, currentIndex + 1, keyIndent, header).value;
343
+ }
249
344
  if (inlineValue.startsWith("[")) {
250
345
  try { return JSON.parse(inlineValue); } catch { return inlineValue; }
251
346
  }
@@ -323,9 +418,13 @@ export function parseYAMLArray(
323
418
  obj[kvMatch[1].trim()] = parseArrayItemValue(kvMatch[2].trim(), lines, i, indent + 2);
324
419
  // Check for more keys at indent+2
325
420
  const nextIndent = indent + 2;
326
- let j = kvMatch[2].trim() === "" || kvMatch[2].trim().startsWith("#")
421
+ const firstVal = kvMatch[2].trim();
422
+ let j = firstVal === "" || firstVal.startsWith("#")
327
423
  ? skipNestedBlock(lines, i + 1, nextIndent)
328
- : i + 1;
424
+ : blockScalarHeader(firstVal)
425
+ // Block body is indented past the key (nextIndent); skip it (#910).
426
+ ? skipNestedBlock(lines, i + 1, nextIndent + 1)
427
+ : i + 1;
329
428
  while (j < lines.length) {
330
429
  const nextLine = lines[j];
331
430
  if (nextLine.trim() === "" || nextLine.trim().startsWith("#")) {
@@ -341,6 +440,9 @@ export function parseYAMLArray(
341
440
  obj[nextKV[2].trim()] = parseArrayItemValue(nextVal, lines, j, ni + 2);
342
441
  if (nextVal === "" || nextVal.startsWith("#")) {
343
442
  j = skipNestedBlock(lines, j + 1, ni + 2);
443
+ } else if (blockScalarHeader(nextVal)) {
444
+ // Skip the block body (indented past this key at `ni`) (#910).
445
+ j = skipNestedBlock(lines, j + 1, ni + 1);
344
446
  } else {
345
447
  j++;
346
448
  }