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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Disable no-undef for Svelte overrides

For Svelte components that use valid store auto-subscriptions such as $count (where count is a Svelte store), enumerating only the built-in rune names still leaves the migrated top-level no-undef rule active and Oxlint will continue reporting those $... references as undefined. This means migrated/generated Svelte projects with stores can still fail vp check; the Svelte override needs to turn off no-undef (or otherwise account for dynamic store subscriptions) rather than only adding a fixed globals list.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disabling no-undef weakens our checks for undefined variables, so I think the best quick fix is to register Runes as globals.

I wanted to avoid turning off no-undef for all .svelte files, so I chose to register each Rune individually. Disabling it globally runs the risk of missing real errors. We already agreed on this approach in #1967.

So, this PR focuses on properly supporting Runes like $props(). Supporting custom user store variables is out of scope for now.

...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