diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_svelte_runes/eslint.config.mjs b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_svelte_runes/eslint.config.mjs new file mode 100644 index 0000000000..294e79a1c7 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_svelte_runes/eslint.config.mjs @@ -0,0 +1,14 @@ +export default [ + { + rules: { + 'no-undef': 'error', + }, + }, + { + files: ['*.svelte', '**/*.svelte'], + rules: { + 'no-inner-declarations': 'off', + 'no-self-assign': 'off', + }, + }, +]; diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_svelte_runes/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_svelte_runes/package.json new file mode 100644 index 0000000000..e2f8ccbecd --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_svelte_runes/package.json @@ -0,0 +1,10 @@ +{ + "name": "migration-eslint-svelte-runes", + "scripts": { + "lint": "eslint ." + }, + "devDependencies": { + "eslint": "^9.0.0", + "vite": "^7.0.0" + } +} diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_svelte_runes/snapshots.toml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_svelte_runes/snapshots.toml new file mode 100644 index 0000000000..aac256e57d --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_svelte_runes/snapshots.toml @@ -0,0 +1,8 @@ +[[case]] +name = "migration_eslint_svelte_runes" +vp = "global" +steps = [ + { argv = ["vp", "migrate", "--no-interactive"], comment = "migration should add Svelte rune globals to the lint override", continue-on-failure = true }, + { argv = ["vpt", "print-file", "vite.config.ts"], comment = "Svelte override includes every built-in rune as a readonly global", continue-on-failure = true }, + { argv = ["vp", "lint", "src/App.svelte"], comment = "valid Svelte rune usage should pass no-undef", continue-on-failure = true }, +] diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_svelte_runes/snapshots/migration_eslint_svelte_runes.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_svelte_runes/snapshots/migration_eslint_svelte_runes.md new file mode 100644 index 0000000000..5453ad229c --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_svelte_runes/snapshots/migration_eslint_svelte_runes.md @@ -0,0 +1,89 @@ +# migration_eslint_svelte_runes + +## `vp migrate --no-interactive` + +migration should add Svelte rune globals to the lint override + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Migrated . to Vite+ +• Node pnpm +• 4 config updates applied +• ESLint rules migrated to Oxlint +``` + +## `vpt print-file vite.config.ts` + +Svelte override includes every built-in rune as a readonly global + +``` +import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + staged: { + "*": "vp check --fix" + }, + fmt: {}, + lint: { + "plugins": [ + "oxc", + "typescript", + "unicorn", + "react" + ], + "categories": { + "correctness": "warn" + }, + "env": { + "builtin": true + }, + "rules": { + "no-undef": "error", + "vite-plus/prefer-vite-plus-imports": "error" + }, + "overrides": [ + { + "files": [ + "*.svelte", + "**/*.svelte" + ], + "rules": { + "no-inner-declarations": "off", + "no-self-assign": "off" + }, + "globals": { + "$state": "readonly", + "$derived": "readonly", + "$effect": "readonly", + "$props": "readonly", + "$bindable": "readonly", + "$inspect": "readonly", + "$host": "readonly" + } + } + ], + "options": { + "typeAware": true, + "typeCheck": true + }, + "jsPlugins": [ + { + "name": "vite-plus", + "specifier": "vite-plus/oxlint-plugin" + } + ] + }, +}); +``` + +## `vp lint src/App.svelte` + +valid Svelte rune usage should pass no-undef + +``` +VITE+ - The Unified Toolchain for the Web + +Found 0 warnings and 0 errors. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_svelte_runes/src/App.svelte b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_svelte_runes/src/App.svelte new file mode 100644 index 0000000000..ae4e6388ba --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_svelte_runes/src/App.svelte @@ -0,0 +1,5 @@ + + +

Hello {name}

diff --git a/packages/cli/src/migration/__tests__/migrator.spec.ts b/packages/cli/src/migration/__tests__/migrator.spec.ts index bcefe8a9f5..f30e82b3c8 100644 --- a/packages/cli/src/migration/__tests__/migrator.spec.ts +++ b/packages/cli/src/migration/__tests__/migrator.spec.ts @@ -42,6 +42,8 @@ const { injectCreateDefaultTemplate, injectFmtDefaults, injectLintTypeCheckDefaults, + ensureSvelteRuneGlobals, + mergeViteConfigFiles, rewriteEslintPackageJson, collectInstalledPackageNames, sanitizeMigratedOxlintConfig, @@ -1172,6 +1174,106 @@ describe('collectInstalledPackageNames', () => { }); }); +describe('ensureSvelteRuneGlobals', () => { + it('adds all built-in runes to Svelte overrides', () => { + const config: import('oxlint').OxlintConfig = { + overrides: [{ files: ['*.svelte', '**/*.svelte.ts'] }], + }; + + ensureSvelteRuneGlobals(config); + + expect(config.overrides?.[0]?.globals).toEqual({ + $state: 'readonly', + $derived: 'readonly', + $effect: 'readonly', + $props: 'readonly', + $bindable: 'readonly', + $inspect: 'readonly', + $host: 'readonly', + }); + }); + + it('preserves existing globals and explicit rune values', () => { + const config: import('oxlint').OxlintConfig = { + overrides: [ + { + files: ['**/*.svelte'], + globals: { customGlobal: 'writable', $state: 'writable' }, + }, + ], + }; + + ensureSvelteRuneGlobals(config); + + expect(config.overrides?.[0]?.globals).toMatchObject({ + customGlobal: 'writable', + $state: 'writable', + $props: 'readonly', + }); + }); + + it('detects Svelte in brace-expanded override patterns', () => { + const config: import('oxlint').OxlintConfig = { + overrides: [{ files: ['**/*.{js,ts,svelte}'] }], + }; + + ensureSvelteRuneGlobals(config); + + expect(config.overrides?.[0]?.globals).toMatchObject({ + $props: 'readonly', + }); + }); + + it('does not modify unrelated or negated-only overrides', () => { + const config: import('oxlint').OxlintConfig = { + overrides: [{ files: ['**/*.ts'] }, { files: ['!**/*.svelte'] }], + }; + + ensureSvelteRuneGlobals(config); + + expect(config.overrides).toEqual([{ files: ['**/*.ts'] }, { files: ['!**/*.svelte'] }]); + }); +}); + +describe('mergeViteConfigFiles — Svelte rune globals', () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-test-svelte-runes-')); + fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({ name: 'test' })); + fs.writeFileSync( + path.join(tmpDir, 'vite.config.ts'), + "import { defineConfig } from 'vite-plus';\n\nexport default defineConfig({});\n", + ); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('merges built-in rune globals into the final lint config', () => { + fs.writeFileSync( + path.join(tmpDir, '.oxlintrc.json'), + JSON.stringify({ + overrides: [ + { + files: ['**/*.{js,ts,svelte}'], + globals: { customGlobal: 'writable' }, + }, + ], + }), + ); + + mergeViteConfigFiles(tmpDir, true); + + const viteConfig = fs.readFileSync(path.join(tmpDir, 'vite.config.ts'), 'utf8'); + expect(viteConfig).toMatch(/["']\$props["']\s*:\s*["']readonly["']/); + expect(viteConfig).toMatch(/["']\$host["']\s*:\s*["']readonly["']/); + expect(viteConfig).toMatch(/["']customGlobal["']\s*:\s*["']writable["']/); + expect(fs.existsSync(path.join(tmpDir, '.oxlintrc.json'))).toBe(false); + }); +}); + function writePkgAt(dir: string, pkg: object): void { fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(pkg)); diff --git a/packages/cli/src/migration/migrator/vite-config.ts b/packages/cli/src/migration/migrator/vite-config.ts index 3d3ac5036c..86b8e29c49 100644 --- a/packages/cli/src/migration/migrator/vite-config.ts +++ b/packages/cli/src/migration/migrator/vite-config.ts @@ -2,6 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import * as prompts from '@voidzero-dev/vite-plus-prompts'; +import { braceExpand } from 'minimatch'; import { type OxlintConfig } from 'oxlint'; import { @@ -35,6 +36,34 @@ import { warnMigration, } from './shared.ts'; +const SVELTE_RUNE_GLOBALS = { + $state: 'readonly', + $derived: 'readonly', + $effect: 'readonly', + $props: 'readonly', + $bindable: 'readonly', + $inspect: 'readonly', + $host: 'readonly', +} satisfies Record; + +// Add Svelte's built-in rune globals to migrated Svelte overrides. +// https://github.com/oxc-project/oxc/issues/20191 +export function ensureSvelteRuneGlobals(config: OxlintConfig): void { + for (const override of config.overrides ?? []) { + const targetsSvelte = override.files.some( + (file: string) => + !file.startsWith('!') && braceExpand(file).some((pattern) => pattern.includes('.svelte')), + ); + if (!targetsSvelte) { + continue; + } + override.globals = { + ...SVELTE_RUNE_GLOBALS, + ...override.globals, + }; + } +} + // Remove the "lint-staged" key from package.json after config has been // successfully merged into vite.config.ts. export function removeLintStagedFromPackageJson(packageJsonPath: string): void { @@ -231,6 +260,7 @@ export function mergeViteConfigFiles( collectInstalledPackageNames(workspaceRoot ?? projectPath, packages), report, ); + ensureSvelteRuneGlobals(oxlintJson); const normalizedOxlintConfig = ensureVitePlusImportRuleDefaults(oxlintJson); // writeJsonFile preserves the user file's existing indent/newline (and adds a // trailing newline) instead of forcing 2-space + no EOL.