Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default [
{
rules: {
'no-undef': 'error',
},
},
{
files: ['*.svelte', '**/*.svelte'],
rules: {
'no-inner-declarations': 'off',
'no-self-assign': 'off',
},
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "migration-eslint-svelte-runes",
"scripts": {
"lint": "eslint ."
},
"devDependencies": {
"eslint": "^9.0.0",
"vite": "^7.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -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 },
]
Original file line number Diff line number Diff line change
@@ -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+ <version>
• Node <version> pnpm <version>
• 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 <duration> on 1 file with <n> rules using <n> threads.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
let { name = 'world' } = $props();
</script>

<h1>Hello {name}</h1>
102 changes: 102 additions & 0 deletions packages/cli/src/migration/__tests__/migrator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ const {
injectCreateDefaultTemplate,
injectFmtDefaults,
injectLintTypeCheckDefaults,
ensureSvelteRuneGlobals,
mergeViteConfigFiles,
rewriteEslintPackageJson,
collectInstalledPackageNames,
sanitizeMigratedOxlintConfig,
Expand Down Expand Up @@ -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));
Expand Down
30 changes: 30 additions & 0 deletions packages/cli/src/migration/migrator/vite-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<string, 'readonly'>;

// 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 {
Expand Down Expand Up @@ -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.
Expand Down
Loading