From 5ecaad12af6dfc3c9074aba0ac2a71f2c0136fa7 Mon Sep 17 00:00:00 2001 From: Taylor Buchanan Date: Mon, 20 Apr 2026 07:25:54 -0500 Subject: [PATCH] Support JS/JSX/MJS/CJS in ESLint, Vitest, and tsconfig Add shared `scriptFileExtensions` constants covering all 8 script extensions (cjs, cts, js, jsx, mjs, mts, ts, tsx). Replace hardcoded `['**/*.ts']` patterns across ESLint plugin configs with the shared constant so all script files are linted and type-checked. - eslint-config: Shared `files.ts` with `scriptFiles` and `tsOnlyFiles`. All plugin configs target all script extensions. JSDoc/TSDoc and TS-syntax rules (`explicit-function-return-type`, `consistent-type- exports`, `no-import-type-side-effects`) scoped to TS-only files. `disableTypeChecked` ignores all script extensions so JS files stay type-checked when `allowJs` is enabled. - vitest-config: Shared `files.ts` with `scriptFileExtensions`. Unit and e2e test includes cover all extensions. Coverage extensions now include `jsx` for consistency. - cli: `typeCheckInclude` broadened to `['*', '.*']` so root-level config files and dotfiles (e.g. `.markdownlint.mjs`) are included in tsconfig projects. Turbo test inputs use `vitest.config.*` with `!vitest.config.e2e.*` negation for extension-agnostic cache keys. - turbo:init regenerated turbo.json and all tsconfig.json files. Closes #40 Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cli/src/lib/tsconfig-gen.ts | 4 +- packages/cli/src/lib/turbo-config.ts | 28 ++++- packages/cli/test/turbo-json.test.ts | 27 ++++- packages/cli/tsconfig.json | 3 +- packages/eslint-config/src/files.ts | 23 ++++ packages/eslint-config/src/index.ts | 17 ++- packages/eslint-config/src/plugins/core.ts | 7 +- .../eslint-config/src/plugins/import-x.ts | 3 +- packages/eslint-config/src/plugins/jsdoc.ts | 3 +- packages/eslint-config/src/plugins/node.ts | 5 +- packages/eslint-config/src/plugins/regexp.ts | 3 +- .../eslint-config/src/plugins/stylistic.ts | 7 +- .../eslint-config/src/plugins/typescript.ts | 41 ++++--- packages/eslint-config/src/plugins/unicorn.ts | 8 +- packages/eslint-config/src/plugins/vitest.ts | 9 +- packages/eslint-config/test/configure.test.ts | 103 +++++++++++++++++- packages/eslint-config/tsconfig.json | 3 +- packages/markdownlint-config/tsconfig.json | 3 +- packages/test-utils/tsconfig.json | 3 +- packages/tsconfig/tsconfig.json | 3 +- packages/vitest-config/src/configure-e2e.ts | 5 +- packages/vitest-config/src/configure.ts | 7 +- packages/vitest-config/src/files.ts | 7 ++ .../vitest-config/test/configure-e2e.test.ts | 9 +- packages/vitest-config/test/configure.test.ts | 11 +- packages/vitest-config/tsconfig.json | 3 +- tsconfig.json | 3 +- turbo.json | 24 +++- 28 files changed, 301 insertions(+), 71 deletions(-) create mode 100644 packages/eslint-config/src/files.ts create mode 100644 packages/vitest-config/src/files.ts diff --git a/packages/cli/src/lib/tsconfig-gen.ts b/packages/cli/src/lib/tsconfig-gen.ts index a35377e..e483590 100644 --- a/packages/cli/src/lib/tsconfig-gen.ts +++ b/packages/cli/src/lib/tsconfig-gen.ts @@ -4,8 +4,8 @@ import * as v from 'valibot'; import type { PackageCapabilities } from './discovery.ts'; import { readJsonFile } from './file-writer.ts'; -/** Directories included in tsconfig.json for type-checking. */ -const typeCheckInclude = ['bin', 'scripts', 'src', 'test', 'e2e', '*.config.*'] as const; +/** Directories and file patterns included in tsconfig.json for type-checking. */ +export const typeCheckInclude = ['bin', 'scripts', 'src', 'test', 'e2e', '*', '.*'] as const; /** Directories included in tsconfig.build.json for compilation. */ export const buildInclude = ['bin', 'src'] as const; diff --git a/packages/cli/src/lib/turbo-config.ts b/packages/cli/src/lib/turbo-config.ts index a9e38b1..2b394f4 100644 --- a/packages/cli/src/lib/turbo-config.ts +++ b/packages/cli/src/lib/turbo-config.ts @@ -4,6 +4,7 @@ import { typecheckTs, } from '../commands/leaf/index.ts'; import type { WorkspaceDiscovery } from './discovery.ts'; +import { typeCheckInclude } from './tsconfig-gen.ts'; import { aggregateTasks } from './turbo-aggregates.ts'; /** Turbo-only aggregate task names (no CLI handler). */ @@ -98,11 +99,23 @@ export const resolveToolFlags = (discovery: WorkspaceDiscovery): ToolFlags => { /** Creates a topological (cross-package) task dependency. */ const topo = (task: string): string => `^${task}`; +/** Script file extensions for turbo input globs. Sorted alphabetically. */ +const scriptExts = ['cjs', 'cts', 'js', 'jsx', 'mjs', 'mts', 'ts', 'tsx'] as const; + const isGlob = (pattern: string): boolean => /[*?\{]/v.test(pattern); -/** Converts a tsconfig `include` entry to a turbo input glob. */ -const toTurboGlob = (include: string): string => - isGlob(include) ? include : `${include}/**`; +/** + * Converts a tsconfig `include` entry to turbo input glob(s). + * Directories become recursive (`src` → `src/**`). + * Root-level globs (`*`, `.*`) expand to per-extension patterns + * since turbo globs are extension-agnostic unlike tsconfig. + */ +const toTurboGlobs = (include: string): readonly string[] => { + if (include === '*') return scriptExts.map(ext => `*.${ext}`); + if (include === '.*') return scriptExts.map(ext => `.*.${ext}`); + if (isGlob(include)) return [include]; + return [`${include}/**`]; +}; const typecheckTasks = (flags: ToolFlags): readonly ConditionalEntry[] => [ { @@ -112,7 +125,7 @@ const typecheckTasks = (flags: ToolFlags): readonly ConditionalEntry[ dependsOn: [...(flags.hasGenerate ? [Aggregate.generate] : [])], inputs: [ '$TURBO_ROOT$/tsconfig.base.json', - 'bin/**', 'src/**', 'test/**', 'e2e/**', 'scripts/**', + ...typeCheckInclude.flatMap(toTurboGlobs), 'tsconfig.json', 'tsconfig.*.json', ], outputs: [], @@ -128,7 +141,7 @@ const compileTasks = (flags: ToolFlags): readonly ConditionalEntry[] dependsOn: [topo(compileTs.name), ...(flags.hasGenerate ? [Aggregate.generate] : [])], inputs: [ '$TURBO_ROOT$/tsconfig.base.json', '$TURBO_ROOT$/tsconfig.build.json', - ...flags.compileIncludes.map(toTurboGlob), 'tsconfig.build.json', + ...flags.compileIncludes.flatMap(toTurboGlobs), 'tsconfig.build.json', ], outputs: ['dist/source/**'], }, @@ -172,7 +185,10 @@ const lintTasks = (flags: ToolFlags): readonly ConditionalEntry[] => * vitest configs are executable TypeScript, not statically parseable. * Broadening beyond test directories is intentional: tests import source. */ -const testInputs = ['bin/**', 'src/**', 'test/**', 'scripts/**', 'vitest.config.ts']; +const testInputs = [ + 'bin/**', 'src/**', 'test/**', 'scripts/**', + 'vitest.config.*', '!vitest.config.e2e.*', +]; const testTasks = (flags: ToolFlags): readonly ConditionalEntry[] => { const deps = [ diff --git a/packages/cli/test/turbo-json.test.ts b/packages/cli/test/turbo-json.test.ts index ce76aeb..42bc453 100644 --- a/packages/cli/test/turbo-json.test.ts +++ b/packages/cli/test/turbo-json.test.ts @@ -91,6 +91,19 @@ describe.concurrent(generateTurboJson, () => { expect(result.tasks).toHaveProperty('typecheck:ts'); }); + it('typecheck:ts inputs include root-level script files', ({ expect }) => { + const discovery = makeDiscovery([ + makeCapabilities({ hasTypeScript: true }), + ]); + + const result = generateTurboJson(discovery); + const inputs = result.tasks['typecheck:ts']?.inputs ?? []; + + expect(inputs).toStrictEqual(expect.arrayContaining([ + '*.ts', '*.mjs', '.*.ts', '.*.mjs', + ])); + }); + it('excludes typecheck:ts when no package has TypeScript', ({ expect }) => { const discovery = makeDiscovery([makeCapabilities()]); @@ -283,7 +296,7 @@ describe.concurrent(generateTurboJson, () => { expect(inputs).not.toContain('scripts/**'); }); - it('test:vitest:fast inputs use explicit vitest config filename', ({ expect }) => { + it('test:vitest:fast inputs match all vitest configs except e2e', ({ expect }) => { const discovery = makeDiscovery([ makeCapabilities({ hasTest: true, hasVitest: true }), ]); @@ -291,11 +304,12 @@ describe.concurrent(generateTurboJson, () => { const result = generateTurboJson(discovery); const inputs = result.tasks['test:vitest:fast']?.inputs ?? []; - expect(inputs).toContain('vitest.config.ts'); - expect(inputs).not.toContain('vitest.config.*'); + expect(inputs).toStrictEqual(expect.arrayContaining([ + 'vitest.config.*', '!vitest.config.e2e.*', + ])); }); - it('test:vitest:slow inputs use explicit vitest config filename', ({ expect }) => { + it('test:vitest:slow inputs match all vitest configs except e2e', ({ expect }) => { const discovery = makeDiscovery([ makeCapabilities({ hasTest: true, hasVitest: true }), ]); @@ -303,7 +317,8 @@ describe.concurrent(generateTurboJson, () => { const result = generateTurboJson(discovery); const inputs = result.tasks['test:vitest:slow']?.inputs ?? []; - expect(inputs).toContain('vitest.config.ts'); - expect(inputs).not.toContain('vitest.config.*'); + expect(inputs).toStrictEqual(expect.arrayContaining([ + 'vitest.config.*', '!vitest.config.e2e.*', + ])); }); }); diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 8f5a8d6..f81db4a 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -9,6 +9,7 @@ "src", "test", "e2e", - "*.config.*" + "*", + ".*" ] } diff --git a/packages/eslint-config/src/files.ts b/packages/eslint-config/src/files.ts new file mode 100644 index 0000000..0a2325d --- /dev/null +++ b/packages/eslint-config/src/files.ts @@ -0,0 +1,23 @@ +/** + * All script file extensions supported by the shared ESLint configuration. + * Sorted alphabetically. Used to derive glob patterns for plugin configs. + */ +export const scriptFileExtensions = [ + 'cjs', 'cts', 'js', 'jsx', 'mjs', 'mts', 'ts', 'tsx', +] as const; + +/** Glob patterns matching all script files in any directory. */ +export const scriptFiles: string[] = scriptFileExtensions.map( + ext => `**/*.${ext}`, +); + +/** + * TypeScript-only file extensions. Used to scope rules that require + * TypeScript syntax (e.g. `export type`, `: ReturnType`, `import type`). + */ +export const tsOnlyExtensions = ['cts', 'mts', 'ts', 'tsx'] as const; + +/** Glob patterns matching only TypeScript files. */ +export const tsOnlyFiles: string[] = tsOnlyExtensions.map( + ext => `**/*.${ext}`, +); diff --git a/packages/eslint-config/src/index.ts b/packages/eslint-config/src/index.ts index b28d68a..e71a625 100644 --- a/packages/eslint-config/src/index.ts +++ b/packages/eslint-config/src/index.ts @@ -3,13 +3,22 @@ /// import type { Linter } from 'eslint'; import { defineConfig } from 'eslint/config'; +import { scriptFileExtensions } from './files.ts'; import { plugins } from './plugins/index.ts'; +export { + scriptFileExtensions, + scriptFiles, + tsOnlyExtensions, + tsOnlyFiles, +} from './files.ts'; + +const entryPointDirs = ['**/bin', '**/scripts'] as const; + /** Default file patterns for entry points (bin and scripts directories). */ -export const defaultEntryPoints: readonly string[] = [ - '**/bin/**/*.ts', - '**/scripts/**/*.ts', -]; +export const defaultEntryPoints: readonly string[] = entryPointDirs.flatMap( + dir => scriptFileExtensions.map(ext => `${dir}/**/*.${ext}`), +); /** Options for the shared ESLint configuration. */ export interface ESLintConfigureOptions { diff --git a/packages/eslint-config/src/plugins/core.ts b/packages/eslint-config/src/plugins/core.ts index 548b206..4c3083e 100644 --- a/packages/eslint-config/src/plugins/core.ts +++ b/packages/eslint-config/src/plugins/core.ts @@ -1,11 +1,12 @@ import type { Linter } from 'eslint'; +import { scriptFiles } from '../files.ts'; import type { PluginFactory } from '../index.ts'; // --- Core ESLint rules not covered by presets --- /** Core ESLint rules not covered by presets. */ const coreRuleConfig: Linter.Config = { - files: ['**/*.ts'], + files: [...scriptFiles], rules: { // Justification: Methods that don't use this should be static or extracted 'class-methods-use-this': 'warn', @@ -48,7 +49,7 @@ const browserConfigs = ( entryPoints: readonly string[], ): Linter.Config[] => [ { - files: ['**/*.ts'], + files: [...scriptFiles], rules: { // Justification: Browser code should use a logger, not console 'no-alert': 'warn', @@ -70,7 +71,7 @@ const plugin: PluginFactory = (options) => { return [ coreRuleConfig, { - files: ['**/*.ts'], + files: [...scriptFiles], rules: { // Justification: Prevents subtle unicode bugs; /v for server, /u for browser compat 'require-unicode-regexp': ['warn', { requireFlag: unicodeFlag }], diff --git a/packages/eslint-config/src/plugins/import-x.ts b/packages/eslint-config/src/plugins/import-x.ts index d4e31b9..120e0f4 100644 --- a/packages/eslint-config/src/plugins/import-x.ts +++ b/packages/eslint-config/src/plugins/import-x.ts @@ -1,12 +1,13 @@ import type { Linter } from 'eslint'; import importPlugin from 'eslint-plugin-import-x'; +import { scriptFiles } from '../files.ts'; import type { PluginFactory } from '../index.ts'; // --- import-x (ordering only) --- /** Import ordering rules via eslint-plugin-import-x. */ const importConfig: Linter.Config = { - files: ['**/*.ts'], + files: [...scriptFiles], plugins: { 'import-x': importPlugin }, rules: { // Justification: Case-insensitive alphabetical grouping by import type diff --git a/packages/eslint-config/src/plugins/jsdoc.ts b/packages/eslint-config/src/plugins/jsdoc.ts index e7550b3..281ecfc 100644 --- a/packages/eslint-config/src/plugins/jsdoc.ts +++ b/packages/eslint-config/src/plugins/jsdoc.ts @@ -1,5 +1,6 @@ import type { Linter } from 'eslint'; import jsdoc from 'eslint-plugin-jsdoc'; +import { tsOnlyFiles } from '../files.ts'; import type { PluginFactory } from '../index.ts'; // --- JSDoc --- @@ -14,7 +15,7 @@ const tsdocConfig = jsdoc.configs['flat/recommended-tsdoc'] as unknown as Linter const plugin: PluginFactory = () => [ { - files: ['**/*.ts'], + files: [...tsOnlyFiles], plugins: tsdocConfig.plugins ?? {}, rules: { ...tsdocConfig.rules, diff --git a/packages/eslint-config/src/plugins/node.ts b/packages/eslint-config/src/plugins/node.ts index 516a30f..0b36bd8 100644 --- a/packages/eslint-config/src/plugins/node.ts +++ b/packages/eslint-config/src/plugins/node.ts @@ -1,17 +1,16 @@ import { defineConfig } from 'eslint/config'; import nodePlugin from 'eslint-plugin-n'; import tseslint from 'typescript-eslint'; +import { scriptFiles } from '../files.ts'; import type { PluginFactory } from '../index.ts'; import { resolveParserOptions } from './typescript.ts'; // --- Node.js --- -const nodeFiles = ['**/*.ts', '**/*.mts', '**/*.cts']; - const plugin: PluginFactory = options => [ ...defineConfig({ extends: [nodePlugin.configs['flat/recommended-module']], - files: nodeFiles, + files: [...scriptFiles], languageOptions: { parser: tseslint.parser, parserOptions: resolveParserOptions(options), diff --git a/packages/eslint-config/src/plugins/regexp.ts b/packages/eslint-config/src/plugins/regexp.ts index 65299ae..e7c859f 100644 --- a/packages/eslint-config/src/plugins/regexp.ts +++ b/packages/eslint-config/src/plugins/regexp.ts @@ -1,4 +1,5 @@ import regexpPlugin from 'eslint-plugin-regexp'; +import { scriptFiles } from '../files.ts'; import type { PluginFactory } from '../index.ts'; // --- Regexp --- @@ -7,7 +8,7 @@ import type { PluginFactory } from '../index.ts'; const plugin: PluginFactory = () => [ { ...regexpPlugin.configs['flat/recommended'], - files: ['**/*.ts'], + files: [...scriptFiles], }, ]; diff --git a/packages/eslint-config/src/plugins/stylistic.ts b/packages/eslint-config/src/plugins/stylistic.ts index 1303d66..ae9c501 100644 --- a/packages/eslint-config/src/plugins/stylistic.ts +++ b/packages/eslint-config/src/plugins/stylistic.ts @@ -1,16 +1,17 @@ import stylistic from '@stylistic/eslint-plugin'; +import { scriptFiles } from '../files.ts'; import type { PluginFactory } from '../index.ts'; // --- Stylistic --- -/** Stylistic formatting rules for TypeScript files. */ +/** Stylistic formatting rules for script files. */ const plugin: PluginFactory = () => [ { ...stylistic.configs.customize({ braceStyle: '1tbs', semi: true, severity: 'warn' }), - files: ['**/*.ts'], + files: [...scriptFiles], }, { - files: ['**/*.ts'], + files: [...scriptFiles], rules: { /* * Justification: 100 is a compromise between full-screen monitors and diff --git a/packages/eslint-config/src/plugins/typescript.ts b/packages/eslint-config/src/plugins/typescript.ts index fb484ba..84b63bc 100644 --- a/packages/eslint-config/src/plugins/typescript.ts +++ b/packages/eslint-config/src/plugins/typescript.ts @@ -1,22 +1,14 @@ import type { Linter } from 'eslint'; import tseslint from 'typescript-eslint'; +import { scriptFiles, tsOnlyFiles } from '../files.ts'; import type { PluginFactory, ResolvedOptions } from '../index.ts'; // --- TypeScript rule overrides (on top of strictTypeChecked + stylisticTypeChecked) --- -/** TypeScript rule overrides on top of strictTypeChecked + stylisticTypeChecked. */ -const tsRuleOverrides: Linter.Config = { - files: ['**/*.ts'], +/** Rule overrides that apply to all script files (JS + TS). */ +const scriptRuleOverrides: Linter.Config = { + files: [...scriptFiles], rules: { - // Justification: Enforces export type for type-only re-exports - '@typescript-eslint/consistent-type-exports': 'warn', - // Justification: Stable return types; catches accidental API changes - '@typescript-eslint/explicit-function-return-type': ['warn', { - allowDirectConstAssertionInArrowFunctions: true, - allowExpressions: true, - allowHigherOrderFunctions: true, - allowTypedFunctionExpressions: true, - }], // Justification: Property syntax is contravariant (safe); method syntax is bivariant (unsafe) '@typescript-eslint/method-signature-style': 'warn', // Justification: Prevents snake_case drift from agents and API boundaries @@ -28,8 +20,6 @@ const tsRuleOverrides: Linter.Config = { trailingUnderscore: 'allow', }, ], - // Justification: Prevents runtime imports for type-only specifiers - '@typescript-eslint/no-import-type-side-effects': 'warn', // Justification: Common sentinel values that are universally understood '@typescript-eslint/no-magic-numbers': ['warn', { ignore: [-1, 0, 1, 100], @@ -53,6 +43,24 @@ const tsRuleOverrides: Linter.Config = { }, }; +/** Rule overrides that require TypeScript syntax (export type, : ReturnType, import type). */ +const tsOnlyRuleOverrides: Linter.Config = { + files: [...tsOnlyFiles], + rules: { + // Justification: Enforces export type for type-only re-exports + '@typescript-eslint/consistent-type-exports': 'warn', + // Justification: Stable return types; catches accidental API changes + '@typescript-eslint/explicit-function-return-type': ['warn', { + allowDirectConstAssertionInArrowFunctions: true, + allowExpressions: true, + allowHigherOrderFunctions: true, + allowTypedFunctionExpressions: true, + }], + // Justification: Prevents runtime imports for type-only specifiers + '@typescript-eslint/no-import-type-side-effects': 'warn', + }, +}; + /** Resolves TypeScript parser options from the shared config options. */ export const resolveParserOptions = ( options: ResolvedOptions, @@ -67,10 +75,11 @@ const plugin: PluginFactory = options => [ ...tseslint.configs.strictTypeChecked, ...tseslint.configs.stylisticTypeChecked, { languageOptions: { parserOptions: resolveParserOptions(options) } }, - tsRuleOverrides, + scriptRuleOverrides, + tsOnlyRuleOverrides, { files: ['**/*'], - ignores: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'], + ignores: [...scriptFiles], ...tseslint.configs.disableTypeChecked, }, ]; diff --git a/packages/eslint-config/src/plugins/unicorn.ts b/packages/eslint-config/src/plugins/unicorn.ts index 41a5f55..4ef4e15 100644 --- a/packages/eslint-config/src/plugins/unicorn.ts +++ b/packages/eslint-config/src/plugins/unicorn.ts @@ -1,14 +1,14 @@ import unicornPlugin from 'eslint-plugin-unicorn'; +import { scriptFiles } from '../files.ts'; import type { PluginFactory } from '../index.ts'; // --- Unicorn --- -/** Unicorn recommended preset (scoped to TS) with rule overrides. */ +/** Unicorn recommended preset with rule overrides (scoped to script files). */ const plugin: PluginFactory = () => [ - // Unicorn recommended (scoped to TS — unicorn crashes on JSON/YAML parsers) - { ...unicornPlugin.configs.recommended, files: ['**/*.ts'] }, + { ...unicornPlugin.configs.recommended, files: [...scriptFiles] }, { - files: ['**/*.ts'], + files: [...scriptFiles], rules: { // Justification: Cannot distinguish intentional from accidental arity matches 'unicorn/no-array-callback-reference': 'off', diff --git a/packages/eslint-config/src/plugins/vitest.ts b/packages/eslint-config/src/plugins/vitest.ts index 75ee5aa..8c3c079 100644 --- a/packages/eslint-config/src/plugins/vitest.ts +++ b/packages/eslint-config/src/plugins/vitest.ts @@ -1,10 +1,15 @@ import vitestPlugin from '@vitest/eslint-plugin'; +import { scriptFileExtensions } from '../files.ts'; import type { PluginFactory } from '../index.ts'; // --- Vitest --- +const testDirs = ['**/test', '**/e2e'] as const; + /** File patterns for test files. */ -const testFiles = ['**/test/**/*.ts', '**/e2e/**/*.ts']; +const testFiles = testDirs.flatMap( + dir => scriptFileExtensions.map(ext => `${dir}/**/*.${ext}`), +); /** Vitest plugin configs with rule overrides for test files. */ const plugin: PluginFactory = () => [ @@ -50,7 +55,7 @@ const plugin: PluginFactory = () => [ }, }, { - files: ['**/e2e/**/*.ts'], + files: scriptFileExtensions.map(ext => `**/e2e/**/*.${ext}`), rules: { // Justification: E2e tests need beforeAll/afterAll for fixture setup/teardown 'vitest/no-hooks': 'off', diff --git a/packages/eslint-config/test/configure.test.ts b/packages/eslint-config/test/configure.test.ts index e49d629..2c59465 100644 --- a/packages/eslint-config/test/configure.test.ts +++ b/packages/eslint-config/test/configure.test.ts @@ -1,5 +1,13 @@ import { describe, it, vi } from 'vitest'; -import { configure } from '#src/index.js'; +import { configure, defaultEntryPoints } from '#src/index.js'; + +/** + * All script file extensions that plugin configs should target. + * Defined inline so tests don't depend on implementation. + */ +const allScriptExtensions = ['cjs', 'cts', 'js', 'jsx', 'mjs', 'mts', 'ts', 'tsx']; +const jsOnlyExtensions = ['cjs', 'js', 'jsx', 'mjs']; +const tsOnlyExtensions = ['cts', 'mts', 'ts', 'tsx']; const onlyWarnImport = vi.fn<() => void>(); @@ -105,4 +113,97 @@ describe(configure, () => { '**/package.json', ])); }); + + it('type-checks all script file extensions', async ({ expect }) => { + const configs = await configure({ onlyWarn: false }); + const disableConfig = configs.find(cfg => + cfg.files?.includes('**/*') && cfg.ignores?.includes('**/*.ts'), + ); + + for (const ext of allScriptExtensions) { + expect(disableConfig?.ignores).toContain(`**/*.${ext}`); + } + }); + + it('applies core rules to all script file extensions', async ({ expect }) => { + const configs = await configure({ onlyWarn: false }); + const coreConfig = configs.find( + cfg => cfg.rules?.['class-methods-use-this'] !== undefined, + ); + + for (const ext of allScriptExtensions) { + expect(coreConfig?.files).toContain(`**/*.${ext}`); + } + }); + + it('applies import ordering to all script file extensions', async ({ expect }) => { + const configs = await configure({ onlyWarn: false }); + const importConfig = configs.find( + cfg => cfg.rules?.['import-x/order'] !== undefined, + ); + + for (const ext of allScriptExtensions) { + expect(importConfig?.files).toContain(`**/*.${ext}`); + } + }); + + it('applies node rules to all script file extensions', async ({ expect }) => { + const configs = await configure({ onlyWarn: false }); + const nodeConfig = configs.find( + cfg => cfg.rules?.['n/no-extraneous-import'] === 'off', + ); + + for (const ext of allScriptExtensions) { + expect(nodeConfig?.files).toContain(`**/*.${ext}`); + } + }); + + it('applies vitest rules to test files with all script extensions', async ({ expect }) => { + const configs = await configure({ onlyWarn: false }); + const vitestConfig = configs.find( + cfg => cfg.rules?.['vitest/prefer-lowercase-title'] !== undefined, + ); + + for (const dir of ['**/test', '**/e2e']) { + for (const ext of allScriptExtensions) { + expect(vitestConfig?.files).toContain(`${dir}/**/*.${ext}`); + } + } + }); + + it('covers all script extensions in default entry points', ({ expect }) => { + for (const dir of ['**/bin', '**/scripts']) { + for (const ext of allScriptExtensions) { + expect(defaultEntryPoints).toContain(`${dir}/**/*.${ext}`); + } + } + }); + + it('scopes TS-syntax rules to TypeScript files only', async ({ expect }) => { + const configs = await configure({ onlyWarn: false }); + const tsOnlyConfig = configs.find( + cfg => cfg.rules?.['@typescript-eslint/consistent-type-exports'] !== undefined, + ); + + for (const ext of tsOnlyExtensions) { + expect(tsOnlyConfig?.files).toContain(`**/*.${ext}`); + } + for (const ext of jsOnlyExtensions) { + expect(tsOnlyConfig?.files).not.toContain(`**/*.${ext}`); + } + }); + + it('scopes jsdoc/tsdoc rules to TypeScript files only', async ({ expect }) => { + const configs = await configure({ onlyWarn: false }); + const jsdocConfig = configs.find( + cfg => cfg.rules?.['jsdoc/check-tag-names'] !== undefined, + ); + + for (const ext of tsOnlyExtensions) { + expect(jsdocConfig?.files).toContain(`**/*.${ext}`); + } + for (const ext of jsOnlyExtensions) { + expect(jsdocConfig?.files).not.toContain(`**/*.${ext}`); + } + }); }); diff --git a/packages/eslint-config/tsconfig.json b/packages/eslint-config/tsconfig.json index 8f5a8d6..f81db4a 100644 --- a/packages/eslint-config/tsconfig.json +++ b/packages/eslint-config/tsconfig.json @@ -9,6 +9,7 @@ "src", "test", "e2e", - "*.config.*" + "*", + ".*" ] } diff --git a/packages/markdownlint-config/tsconfig.json b/packages/markdownlint-config/tsconfig.json index 8f5a8d6..f81db4a 100644 --- a/packages/markdownlint-config/tsconfig.json +++ b/packages/markdownlint-config/tsconfig.json @@ -9,6 +9,7 @@ "src", "test", "e2e", - "*.config.*" + "*", + ".*" ] } diff --git a/packages/test-utils/tsconfig.json b/packages/test-utils/tsconfig.json index 8f5a8d6..f81db4a 100644 --- a/packages/test-utils/tsconfig.json +++ b/packages/test-utils/tsconfig.json @@ -9,6 +9,7 @@ "src", "test", "e2e", - "*.config.*" + "*", + ".*" ] } diff --git a/packages/tsconfig/tsconfig.json b/packages/tsconfig/tsconfig.json index bb26596..507d5c7 100644 --- a/packages/tsconfig/tsconfig.json +++ b/packages/tsconfig/tsconfig.json @@ -10,6 +10,7 @@ "src", "test", "e2e", - "*.config.*" + "*", + ".*" ] } diff --git a/packages/vitest-config/src/configure-e2e.ts b/packages/vitest-config/src/configure-e2e.ts index 2bec69b..3ffb597 100644 --- a/packages/vitest-config/src/configure-e2e.ts +++ b/packages/vitest-config/src/configure-e2e.ts @@ -13,6 +13,7 @@ import { findConfigFile, resolveProjectDirs, } from './configure.ts'; +import { scriptFileExtensions } from './files.ts'; /** Options for combined e2e configuration ({@link configureEndToEnd}). */ export interface VitestEndToEndConfigureOptions extends VitestConfigureOptions { @@ -32,7 +33,9 @@ export interface VitestEndToEndConfigureGlobalOptions extends VitestConfigureGlo readonly testTimeout?: number; } -const e2eTestInclude = ['e2e/**/*.test.ts'] as const; +const e2eTestInclude = scriptFileExtensions.map( + ext => `e2e/**/*.test.${ext}`, +); const e2eConfigPrefix = 'vitest.config.e2e'; diff --git a/packages/vitest-config/src/configure.ts b/packages/vitest-config/src/configure.ts index 5513936..09311f8 100644 --- a/packages/vitest-config/src/configure.ts +++ b/packages/vitest-config/src/configure.ts @@ -9,6 +9,7 @@ import { defineConfig, mergeConfig, } from 'vitest/config'; +import { scriptFileExtensions } from './files.ts'; /** Shared options for all Vitest configuration layers. */ export interface VitestConfigureOptions { @@ -61,7 +62,7 @@ export const excludeDefault = [ const packageName = '@gtbuchanan/vitest-config'; -const coverageExtensions = '*.{cjs,cts,js,mjs,mts,ts,tsx}'; +const coverageExtensions = `*.{${scriptFileExtensions.join(',')}}`; /** Default directories included in coverage reports. */ export const defaultCoverageDirs = [ @@ -263,7 +264,9 @@ export const buildGlobalConfig = ( }, }); -const unitTestInclude = ['test/**/*.test.ts'] as const; +const unitTestInclude = scriptFileExtensions.map( + ext => `test/**/*.test.${ext}`, +); const defaultSlowTimeout = 300_000; diff --git a/packages/vitest-config/src/files.ts b/packages/vitest-config/src/files.ts new file mode 100644 index 0000000..6dc2d49 --- /dev/null +++ b/packages/vitest-config/src/files.ts @@ -0,0 +1,7 @@ +/** + * Script file extensions for test and coverage include patterns. + * Sorted alphabetically. + */ +export const scriptFileExtensions = [ + 'cjs', 'cts', 'js', 'jsx', 'mjs', 'mts', 'ts', 'tsx', +] as const; diff --git a/packages/vitest-config/test/configure-e2e.test.ts b/packages/vitest-config/test/configure-e2e.test.ts index 7fb16ba..95c6f0e 100644 --- a/packages/vitest-config/test/configure-e2e.test.ts +++ b/packages/vitest-config/test/configure-e2e.test.ts @@ -7,6 +7,11 @@ import { } from '#src/configure-e2e.js'; import { excludeDefault } from '#src/configure.js'; +const allScriptExtensions = ['cjs', 'cts', 'js', 'jsx', 'mjs', 'mts', 'ts', 'tsx']; +const expectedE2eTestInclude = allScriptExtensions.map( + ext => `e2e/**/*.test.${ext}`, +); + const packageName = '@gtbuchanan/vitest-config'; describe(configureEndToEndProject, () => { @@ -19,7 +24,7 @@ describe(configureEndToEndProject, () => { it('includes e2e test pattern', ({ expect }) => { const config = configureEndToEndProject(); - expect(config.test?.include).toStrictEqual(['e2e/**/*.test.ts']); + expect(config.test?.include).toStrictEqual(expectedE2eTestInclude); }); it('excludes defaults', ({ expect }) => { @@ -116,7 +121,7 @@ describe(configureEndToEndPackage, () => { it('includes e2e test patterns', ({ expect }) => { const config = configureEndToEndPackage(); - expect(config.test?.include).toStrictEqual(['e2e/**/*.test.ts']); + expect(config.test?.include).toStrictEqual(expectedE2eTestInclude); }); it('includes global settings (setupFiles, mockReset)', ({ expect }) => { diff --git a/packages/vitest-config/test/configure.test.ts b/packages/vitest-config/test/configure.test.ts index dbbb333..6b850bd 100644 --- a/packages/vitest-config/test/configure.test.ts +++ b/packages/vitest-config/test/configure.test.ts @@ -11,6 +11,11 @@ import { resolveCoverageInclude, } from '#src/index.js'; +const allScriptExtensions = ['cjs', 'cts', 'js', 'jsx', 'mjs', 'mts', 'ts', 'tsx']; +const expectedUnitTestInclude = allScriptExtensions.map( + ext => `test/**/*.test.${ext}`, +); + const packageName = '@gtbuchanan/vitest-config'; describe(configure, () => { @@ -226,7 +231,7 @@ describe(resolveCoverageInclude, () => { it('respects custom coverage dirs', ({ expect }) => { const result = resolveCoverageInclude(undefined, ['src']); - expect(result).toStrictEqual(['src/**/*.{cjs,cts,js,mjs,mts,ts,tsx}']); + expect(result).toStrictEqual(['src/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}']); }); it('includes all default dirs', ({ expect }) => { @@ -256,7 +261,7 @@ describe(buildWorkspaceEntry, () => { it('preserves includes from configure function', ({ expect }) => { const entry = buildWorkspaceEntry('/path/to/my-package', configureProject); - expect(entry.test?.include).toStrictEqual(['test/**/*.test.ts']); + expect(entry.test?.include).toStrictEqual(expectedUnitTestInclude); }); it('preserves excludes from configure function', ({ expect }) => { @@ -290,7 +295,7 @@ describe(configurePackage, () => { it('includes test patterns', ({ expect }) => { const config = configurePackage(); - expect(config.test?.include).toStrictEqual(['test/**/*.test.ts']); + expect(config.test?.include).toStrictEqual(expectedUnitTestInclude); }); it('includes global settings (coverage, setupFiles)', ({ expect }) => { diff --git a/packages/vitest-config/tsconfig.json b/packages/vitest-config/tsconfig.json index 8f5a8d6..f81db4a 100644 --- a/packages/vitest-config/tsconfig.json +++ b/packages/vitest-config/tsconfig.json @@ -9,6 +9,7 @@ "src", "test", "e2e", - "*.config.*" + "*", + ".*" ] } diff --git a/tsconfig.json b/tsconfig.json index 4dab6c5..053c3ba 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "src", "test", "e2e", - "*.config.*" + "*", + ".*" ] } diff --git a/turbo.json b/turbo.json index 77d17cb..979d495 100644 --- a/turbo.json +++ b/turbo.json @@ -152,7 +152,8 @@ "src/**", "test/**", "scripts/**", - "vitest.config.ts" + "vitest.config.*", + "!vitest.config.e2e.*" ], "outputs": [ "dist/coverage/vitest/fast/**", @@ -171,7 +172,8 @@ "src/**", "test/**", "scripts/**", - "vitest.config.ts" + "vitest.config.*", + "!vitest.config.e2e.*" ], "outputs": [ "dist/coverage/vitest/slow/**", @@ -188,10 +190,26 @@ "inputs": [ "$TURBO_ROOT$/tsconfig.base.json", "bin/**", + "scripts/**", "src/**", "test/**", "e2e/**", - "scripts/**", + "*.cjs", + "*.cts", + "*.js", + "*.jsx", + "*.mjs", + "*.mts", + "*.ts", + "*.tsx", + ".*.cjs", + ".*.cts", + ".*.js", + ".*.jsx", + ".*.mjs", + ".*.mts", + ".*.ts", + ".*.tsx", "tsconfig.json", "tsconfig.*.json" ],