diff --git a/package.json b/package.json index 1613af20f..bb9659f9e 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "input-from-file-json": "^0.5.4", "input-from-script": "^0.5.4", "js-yaml": "^4.1.0", + "json5": "^2.2.3", "lazy-value": "^3.0.0", "lodash": "^4.17.21", "npm-user": "^6.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c7fd6363f..dd97a067b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: js-yaml: specifier: ^4.1.0 version: 4.1.0 + json5: + specifier: ^2.2.3 + version: 2.2.3 lazy-value: specifier: ^3.0.0 version: 3.0.0 @@ -2938,6 +2941,11 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + jsonc-eslint-parser@2.4.0: resolution: {integrity: sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7203,6 +7211,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json5@2.2.3: {} + jsonc-eslint-parser@2.4.0: dependencies: acorn: 8.14.1 diff --git a/src/blocks/blockVitest.test.ts b/src/blocks/blockVitest.test.ts index 87d2329bf..e2be3aaf4 100644 --- a/src/blocks/blockVitest.test.ts +++ b/src/blocks/blockVitest.test.ts @@ -1,5 +1,5 @@ -import { testBlock } from "bingo-stratum-testers"; -import { describe, expect, test, vi } from "vitest"; +import { testBlock, testIntake } from "bingo-stratum-testers"; +import { describe, expect, it, test, vi } from "vitest"; import { blockVitest } from "./blockVitest.js"; import { optionsBase } from "./options.fakes.js"; @@ -759,4 +759,111 @@ describe("blockVitest", () => { } `); }); + + describe("intake", () => { + it("returns undefined when vitest.config.ts does not exist", () => { + const actual = testIntake(blockVitest, { + files: { + src: {}, + }, + }); + + expect(actual).toEqual(undefined); + }); + + it("returns undefined when vitest.config.ts does not contain the expected defineConfig", () => { + const actual = testIntake(blockVitest, { + files: { + "vitest.config.ts": [`invalid`], + }, + }); + + expect(actual).toEqual(undefined); + }); + + it("returns undefined when vitest.config.ts passes a non-object to defineConfig", () => { + const actual = testIntake(blockVitest, { + files: { + "vitest.config.ts": [`defineConfig("invalid")`], + }, + }); + + expect(actual).toEqual(undefined); + }); + + it("returns undefined when vitest.config.ts does not pass a test to defineConfig", () => { + const actual = testIntake(blockVitest, { + files: { + "vitest.config.ts": [`defineConfig({ other: true })`], + }, + }); + + expect(actual).toEqual(undefined); + }); + + it("returns undefined when vitest.config.ts passes unknown test data to defineConfig", () => { + const actual = testIntake(blockVitest, { + files: { + "vitest.config.ts": [`defineConfig({ test: true })`], + }, + }); + + expect(actual).toEqual(undefined); + }); + + it("returns undefined when vitest.config.ts passes invalid test syntax to defineConfig", () => { + const actual = testIntake(blockVitest, { + files: { + "vitest.config.ts": [`defineConfig({ test: { ! } })`], + }, + }); + + expect(actual).toEqual(undefined); + }); + + it("returns undefined when vitest.config.ts passes invalid test data to defineConfig", () => { + const actual = testIntake(blockVitest, { + files: { + "vitest.config.ts": [ + `defineConfig({ test: { coverage: 'invalid' } })`, + ], + }, + }); + + expect(actual).toEqual(undefined); + }); + + it("returns coverage and exclude when they exist in vitest.config.ts", () => { + const actual = testIntake(blockVitest, { + files: { + "vitest.config.ts": [ + `import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + clearMocks: true, + coverage: { + all: true, + exclude: ["src/index.ts"], + include: ["src", "other"], + reporter: ["html", "lcov"], + }, + exclude: ["lib", "node_modules"], + setupFiles: ["console-fail-test/setup"], + }, +}); +`, + ], + }, + }); + + expect(actual).toEqual({ + coverage: { + exclude: ["src/index.ts"], + include: ["src", "other"], + }, + exclude: ["lib", "node_modules"], + }); + }); + }); }); diff --git a/src/blocks/blockVitest.ts b/src/blocks/blockVitest.ts index 2e62267b8..43c152ced 100644 --- a/src/blocks/blockVitest.ts +++ b/src/blocks/blockVitest.ts @@ -1,3 +1,4 @@ +import JSON5 from "json5"; import { z } from "zod"; import { base } from "../base.js"; @@ -15,6 +16,28 @@ import { blockRemoveFiles } from "./blockRemoveFiles.js"; import { blockRemoveWorkflows } from "./blockRemoveWorkflows.js"; import { blockTSup } from "./blockTSup.js"; import { blockVSCode } from "./blockVSCode.js"; +import { intakeFile } from "./intake/intakeFile.js"; + +function tryParseJSON5(text: string) { + try { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + return JSON5.parse(text) as Record | undefined; + } catch { + return undefined; + } +} + +const zCoverage = z.object({ + exclude: z.array(z.string()).optional(), + include: z.array(z.string()).optional(), +}); + +const zExclude = z.array(z.string()); + +const zTest = z.object({ + coverage: zCoverage, + exclude: zExclude, +}); export const blockVitest = base.createBlock({ about: { @@ -22,15 +45,37 @@ export const blockVitest = base.createBlock({ }, addons: { actionSteps: z.array(zActionStep).default([]), - coverage: z - .object({ - exclude: z.array(z.string()).optional(), - include: z.array(z.string()).optional(), - }) - .default({}), - exclude: z.array(z.string()).default([]), + coverage: zCoverage.default({}), + exclude: zExclude.default([]), flags: z.array(z.string()).default([]), }, + intake({ files }) { + const file = intakeFile(files, ["vitest.config.ts"]); + if (!file) { + return undefined; + } + + const normalized = file[0].replaceAll(/[\n\r]/g, ""); + const matched = /defineConfig\(\{(.+)\}\)\s*(?:;\s*)?$/u.exec(normalized); + if (!matched) { + return undefined; + } + + const rawData = tryParseJSON5(`{${matched[1]}}`); + if (typeof rawData !== "object" || typeof rawData.test !== "object") { + return undefined; + } + + const parsedData = zTest.safeParse(rawData.test).data; + if (!parsedData) { + return undefined; + } + + return { + coverage: parsedData.coverage, + exclude: parsedData.exclude, + }; + }, produce({ addons }) { const { actionSteps, coverage, exclude = [] } = addons; const excludeText = JSON.stringify(exclude);