From 11f9843d4f62e05ec470a1e5a56e300458cb98ee Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 12 Oct 2026 16:30:05 +0200 Subject: [PATCH 1/9] test: cover check without native TypeScript and svelte/internal in .ts Both tests fail on master. svelte-check --tsgo exits 0 with zero diagnostics when the native TypeScript executable cannot start, and Oxlint applies the svelte/internal guard only to .svelte.ts files. --- tests/tooling/svelteCheck.test.ts | 38 ++++++++++++++++++++++++++++++ tests/tooling/sveltePolicy.test.ts | 6 ++++- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/tooling/svelteCheck.test.ts diff --git a/tests/tooling/svelteCheck.test.ts b/tests/tooling/svelteCheck.test.ts new file mode 100644 index 000000000..24995fe9d --- /dev/null +++ b/tests/tooling/svelteCheck.test.ts @@ -0,0 +1,38 @@ +import { spawnSync } from "node:child_process"; +import { copyFileSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { expect, it } from "vitest"; + +it("fails Svelte checking on a type error when the native TypeScript executable cannot run", () => { + const root = fileURLToPath(new URL("../../", import.meta.url)); + const workspace = mkdtempSync(join(tmpdir(), "quickadd-svelte-check-native-")); + try { + symlinkSync(join(root, "node_modules"), join(workspace, "node_modules"), process.platform === "win32" ? "junction" : "dir"); + copyFileSync(join(root, "svelte.config.mjs"), join(workspace, "svelte.config.mjs")); + writeFileSync(join(workspace, "tsconfig.json"), JSON.stringify({ compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "bundler", lib: ["DOM", "ES2022"], strict: true, skipLibCheck: true }, include: ["*.ts"] })); + writeFileSync(join(workspace, "entry.ts"), 'export { default as Probe } from "./Probe.svelte";'); + writeFileSync(join(workspace, "Probe.svelte"), '

{n}

'); + // The native compiler's launcher resolves its platform package at startup, so this + // reproduces an install without that package in every Node process the check starts. + writeFileSync(join(workspace, "no-native-typescript.mjs"), [ + 'import { registerHooks } from "node:module";', + 'registerHooks({ resolve: (specifier, context, next) => { if (specifier.startsWith("@typescript/typescript-")) throw new Error(`Cannot find package ${specifier}`); return next(specifier, context); } });', + ].join("\n")); + const env = { ...process.env, NODE_OPTIONS: `--import ${pathToFileURL(join(workspace, "no-native-typescript.mjs")).href}` }; + + const nativeTsc = spawnSync(process.execPath, [join(root, "node_modules/@typescript/native/bin/tsc"), "--version"], { cwd: workspace, encoding: "utf8", env, timeout: 30_000 }); + expect(nativeTsc.status).not.toBe(0); + expect(nativeTsc.stderr).toContain("Unable to resolve @typescript/typescript-"); + + const manifest: { scripts: { check: string } } = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); + const check = spawnSync(process.execPath, [join(root, "node_modules/svelte-check/bin/svelte-check"), ...manifest.scripts.check.split(" ").slice(1), "--output", "machine-verbose"], { cwd: workspace, encoding: "utf8", env, timeout: 30_000 }); + expect(check.error).toBeUndefined(); + expect(check.status, check.stdout + check.stderr).toBe(1); + expect(check.stdout).toContain('"Probe.svelte"'); + expect(check.stdout).toContain('"code":2322'); + } finally { + rmSync(workspace, { recursive: true, force: true }); + } +}, 30_000); diff --git a/tests/tooling/sveltePolicy.test.ts b/tests/tooling/sveltePolicy.test.ts index f41362796..32a35ab44 100644 --- a/tests/tooling/sveltePolicy.test.ts +++ b/tests/tooling/sveltePolicy.test.ts @@ -57,6 +57,9 @@ describe("custom syntax guards without ESLint", () => { writeFileSync(join(workspace, "src/excluded.test.ts"), 'document.createElement("div");'); writeFileSync(join(workspace, "src/state.svelte.ts"), 'export { writable } from "svelte/store";'); writeFileSync(join(workspace, "src/debug.svelte.ts"), 'const value = 1; $inspect(value);'); + writeFileSync(join(workspace, "src/internal.ts"), 'import "svelte/internal/disclose-version";'); + writeFileSync(join(workspace, "src/internal.svelte.ts"), 'export * from "svelte/internal/client";'); + writeFileSync(join(workspace, "src/internal.test.ts"), 'export { mount } from "svelte/internal/client";'); const result = spawnSync(process.execPath, [join(root, "node_modules/oxlint/bin/oxlint"), "-c", ".oxlintrc.syntax.json", "src", "--format", "json"], { cwd: workspace, encoding: "utf8", timeout: 30_000 }); expect(result.error).toBeUndefined(); expect(result.status, result.stderr).toBe(1); @@ -68,8 +71,9 @@ describe("custom syntax guards without ESLint", () => { expect(diagnostic).toBeDefined(); expect(diagnostic?.message).toBe(checkSvelte(contexts.template(statement))[0].message); } - expect(output.diagnostics.filter(item => item.code === "quickadd(svelte-runes)")).toHaveLength(1); + expect(output.diagnostics.filter(item => item.code === "quickadd(svelte-runes)").map(item => item.filename)).toEqual(["src/state.svelte.ts"]); expect(output.diagnostics.filter(item => item.code === "quickadd(svelte-debug)")).toHaveLength(1); + expect(output.diagnostics.filter(item => item.code === "quickadd(svelte-internal)").map(item => item.filename).sort()).toEqual(["src/internal.svelte.ts", "src/internal.test.ts", "src/internal.ts"]); expect(output.diagnostics.filter(item => item.filename.includes("valid-") || item.filename.includes("excluded.test.ts"))).toEqual([]); } finally { rmSync(workspace, { recursive: true, force: true }); From 0118c3b36e5a9496fa58ab084fbfcf22c1ac3094 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 12 Oct 2026 16:33:23 +0200 Subject: [PATCH 2/9] fix(build): run svelte-check on the TypeScript 6 path svelte-check 4.7.6 under --tsgo ignores the native compiler's exit status, so pnpm run check and pnpm run lint passed a component type error when the native executable could not start. The TypeScript 6 path reports it. tsc keeps the native compiler, which fails on a type error and on a missing executable. --- package.json | 2 +- tooling/README.md | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 650079992..943868c8e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "lint": "pnpm run lint:native && pnpm run lint:svelte && pnpm run check", "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "build-with-lint": "tsc -noEmit -skipLibCheck && pnpm lint && node esbuild.config.mjs production", - "check": "svelte-check --tsgo --fail-on-warnings", + "check": "svelte-check --fail-on-warnings", "packages:build": "node docs/packages/build.mjs", "obsidian:e2e": "obsidian-e2e run", "provision:e2e-vault": "obsidian-e2e provision", diff --git a/tooling/README.md b/tooling/README.md index fc8a30df9..58fea5fec 100644 --- a/tooling/README.md +++ b/tooling/README.md @@ -1,7 +1,7 @@ # Tooling checks `pnpm run lint` runs Oxlint, `svelte-policy.mjs`, and -`svelte-check --tsgo --fail-on-warnings`. `pnpm run build-with-lint` runs the +`svelte-check --fail-on-warnings`. `pnpm run build-with-lint` runs the same checks before producing the application bundle. CI also runs the policy tests through `pnpm run test`. @@ -13,8 +13,9 @@ matches these selectors without an ESLint runtime. Oxlint's JavaScript plugin API is alpha and does not have a semver guarantee. The dependency is pinned. The selectors execute in JavaScript, while built-in -Oxlint rules execute natively. The TypeScript 6 API package remains for tools -that need it, while compilation and Svelte checking use the native compiler. +Oxlint rules execute natively. `tsc` uses the native compiler. svelte-check +uses the TypeScript 6 API package, because svelte-check 4.7.6 exits 0 under +`--tsgo` when the native compiler cannot start. All current components use Svelte 5 runes. Svelte checking and production compilation require runes mode. The policy checker rejects `$:` statements @@ -85,10 +86,12 @@ through Oxlint, with the existing test and user-script exceptions. TypeScript, component scripts, and inline template handlers. It also checks valid examples, the test-file exclusion, matching diagnostics between Oxlint and the component checker, and template policy failures. Temporary fixtures -are private and removed after the test. +are private and removed after the test. `tests/tooling/svelteCheck.test.ts` +runs `pnpm run check`'s command with the native compiler unable to start and +requires it to fail on a component type error. ```sh -pnpm exec vitest run tests/tooling/sveltePolicy.test.ts +pnpm exec vitest run tests/tooling/sveltePolicy.test.ts tests/tooling/svelteCheck.test.ts pnpm run build-with-lint pnpm run test ``` @@ -105,7 +108,7 @@ These refinements follow the tooling migration: - Review the retired CSS checks and decide which merit separate enforcement. - Move checks to upstream Oxlint or Svelte implementations when they cover template expressions, reducing the code maintained here. -- Remove the TypeScript 6 compatibility package when its consumers support - the native compiler's APIs. +- Remove the TypeScript 6 compatibility package when svelte-check fails on a + native compiler that cannot start. - Measure the final verification pipeline. Earlier evaluation timings predate the custom-rule port and the removal of ESLint. From 91dcee7f527c797c2dc0c4edbd4cb1fd516fb673 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 12 Oct 2026 16:33:23 +0200 Subject: [PATCH 3/9] build: drop the .svelte-check ignores Only svelte-check --tsgo and --incremental write .svelte-check, and check uses neither. TypeScript's ** pattern already skips dot-directories, and Oxlint lints only src and tests. --- .gitignore | 1 - .oxlintrc.syntax.json | 3 +-- tsconfig.json | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index d72db0a30..922aaee73 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,3 @@ coverage/ # Obsidian application bundle (only ever extracted transiently for inspection) /app.js -.svelte-check/ diff --git a/.oxlintrc.syntax.json b/.oxlintrc.syntax.json index 14540652d..10a94065a 100644 --- a/.oxlintrc.syntax.json +++ b/.oxlintrc.syntax.json @@ -17,8 +17,7 @@ ".audit/**", ".obsidian-e2e-vaults/**", "main.js", - "**/*.d.ts", - ".svelte-check/**" + "**/*.d.ts" ], "overrides": [ { diff --git a/tsconfig.json b/tsconfig.json index 79f4c9c64..d13370a8f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,5 +22,5 @@ "verbatimModuleSyntax": true }, "include": ["**/*.ts"], - "exclude": ["node_modules", "docs", ".svelte-check"] + "exclude": ["node_modules", "docs"] } From 573a6f4cf0203527e5834b7f0a4067a357504bb9 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 12 Oct 2026 16:33:23 +0200 Subject: [PATCH 4/9] fix(lint): apply the svelte/internal guard to all TypeScript files ESLint's svelte/no-svelte-internal covered every linted .ts file. The Oxlint port enabled the selector only through quickadd/svelte-runes on .svelte.ts files. It is now its own rule on **/*.ts, and svelte-runes keeps only the store and dispatcher guards. --- .oxlintrc.syntax.json | 8 ++++++++ tooling/README.md | 2 +- tooling/oxlint-plugin.mjs | 15 +++++++++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.oxlintrc.syntax.json b/.oxlintrc.syntax.json index 10a94065a..6afbef6f0 100644 --- a/.oxlintrc.syntax.json +++ b/.oxlintrc.syntax.json @@ -222,6 +222,14 @@ "quickadd/restricted-syntax": "off" } }, + { + "files": [ + "**/*.ts" + ], + "rules": { + "quickadd/svelte-internal": "error" + } + }, { "files": [ "**/*.svelte.ts" diff --git a/tooling/README.md b/tooling/README.md index 58fea5fec..378170e5c 100644 --- a/tooling/README.md +++ b/tooling/README.md @@ -56,7 +56,7 @@ listed as retired below now depend on review. | `no-not-function-handler` | Svelte type checking verifies event-handler types. | | `no-object-in-text-mustaches` | Compiler-AST policy rejects object and function literals in text interpolation. | | `no-raw-special-elements` | Compiler-AST policy requires the `svelte:` prefix for special elements. | -| `no-svelte-internal` | Shared selector rejects imports and re-exports from Svelte internals. | +| `no-svelte-internal` | Shared selector rejects imports and re-exports from Svelte internals in components and all TypeScript files. | | `no-unused-svelte-ignore` | Compiler-AST policy compares suppressed warnings with compiler diagnostics for the target element. | | `no-inner-declarations` | Removed. Svelte scripts are modules, and block-scoped functions were already allowed. | | `no-unused-props` | Removed. This rule required TypeScript parser services that the remaining ESLint fallback did not enable. | diff --git a/tooling/oxlint-plugin.mjs b/tooling/oxlint-plugin.mjs index fb051c33e..fb9d3a574 100644 --- a/tooling/oxlint-plugin.mjs +++ b/tooling/oxlint-plugin.mjs @@ -41,11 +41,14 @@ export const guards = [ } ]; -export const svelteImportGuards = [ +export const svelteInternalGuards = [ { selector: ':matches(ImportDeclaration, ImportExpression, ExportNamedDeclaration, ExportAllDeclaration)[source.value=/^svelte\\/internal(\\/|$)/]', message: "Use Svelte's public API instead of svelte/internal.", }, +]; + +export const svelteRunesGuards = [ { selector: ':matches(ImportDeclaration, ImportExpression, ExportNamedDeclaration, ExportAllDeclaration)[source.value="svelte/store"]', message: "Use Svelte runes for component state instead of legacy stores.", @@ -56,6 +59,8 @@ export const svelteImportGuards = [ }, ]; +export const svelteImportGuards = [...svelteInternalGuards, ...svelteRunesGuards]; + export const svelteDebugGuards = [ { selector: 'CallExpression:matches([callee.name="$inspect"], [callee.object.name="$inspect"])', @@ -81,10 +86,16 @@ export default { return createGuards(context, guards); }, }, + "svelte-internal": { + meta: { type: "problem", schema: [] }, + create(context) { + return createGuards(context, svelteInternalGuards); + }, + }, "svelte-runes": { meta: { type: "problem", schema: [] }, create(context) { - return createGuards(context, svelteImportGuards); + return createGuards(context, svelteRunesGuards); }, }, "svelte-debug": { From c92bb5d4db92ee6d5802eed56fda1fa82be759d0 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 12 Oct 2026 17:11:54 +0200 Subject: [PATCH 5/9] test: skip the native-compiler check test before Node 22.15 and compare basenames The preload uses module.registerHooks, which Node 22.15 added, and package.json allows Node 22.12. Oxlint prints file:// URLs in JetBrains terminals, so the guard assertions compare file basenames. --- tests/tooling/svelteCheck.test.ts | 4 +++- tests/tooling/sveltePolicy.test.ts | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/tooling/svelteCheck.test.ts b/tests/tooling/svelteCheck.test.ts index 24995fe9d..f61bb9f7e 100644 --- a/tests/tooling/svelteCheck.test.ts +++ b/tests/tooling/svelteCheck.test.ts @@ -1,11 +1,13 @@ import { spawnSync } from "node:child_process"; import { copyFileSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import module from "node:module"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { expect, it } from "vitest"; -it("fails Svelte checking on a type error when the native TypeScript executable cannot run", () => { +// module.registerHooks, which the preload below uses, arrived in Node 22.15. +it.skipIf(typeof module.registerHooks !== "function")("fails Svelte checking on a type error when the native TypeScript executable cannot run", () => { const root = fileURLToPath(new URL("../../", import.meta.url)); const workspace = mkdtempSync(join(tmpdir(), "quickadd-svelte-check-native-")); try { diff --git a/tests/tooling/sveltePolicy.test.ts b/tests/tooling/sveltePolicy.test.ts index 32a35ab44..cd3cda5ad 100644 --- a/tests/tooling/sveltePolicy.test.ts +++ b/tests/tooling/sveltePolicy.test.ts @@ -1,7 +1,7 @@ import { spawnSync } from "node:child_process"; import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { basename, join } from "node:path"; import { fileURLToPath } from "node:url"; import { compile } from "svelte/compiler"; import { describe, expect, it } from "vitest"; @@ -71,9 +71,9 @@ describe("custom syntax guards without ESLint", () => { expect(diagnostic).toBeDefined(); expect(diagnostic?.message).toBe(checkSvelte(contexts.template(statement))[0].message); } - expect(output.diagnostics.filter(item => item.code === "quickadd(svelte-runes)").map(item => item.filename)).toEqual(["src/state.svelte.ts"]); + expect(output.diagnostics.filter(item => item.code === "quickadd(svelte-runes)").map(item => basename(item.filename))).toEqual(["state.svelte.ts"]); expect(output.diagnostics.filter(item => item.code === "quickadd(svelte-debug)")).toHaveLength(1); - expect(output.diagnostics.filter(item => item.code === "quickadd(svelte-internal)").map(item => item.filename).sort()).toEqual(["src/internal.svelte.ts", "src/internal.test.ts", "src/internal.ts"]); + expect(output.diagnostics.filter(item => item.code === "quickadd(svelte-internal)").map(item => basename(item.filename)).sort()).toEqual(["internal.svelte.ts", "internal.test.ts", "internal.ts"]); expect(output.diagnostics.filter(item => item.filename.includes("valid-") || item.filename.includes("excluded.test.ts"))).toEqual([]); } finally { rmSync(workspace, { recursive: true, force: true }); From cb20e970cbeb68bff534b2f1d452cba94a9cfe0a Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 12 Oct 2026 17:53:53 +0200 Subject: [PATCH 6/9] build: keep ignoring .svelte-check overlays svelte-check --tsgo and --incremental leave the directory behind, and checkouts that ran check after #2224 already have one. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 922aaee73..d72db0a30 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ coverage/ # Obsidian application bundle (only ever extracted transiently for inspection) /app.js +.svelte-check/ From a756b615fa21c99242191cf0a08fa28cab3c28df Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 12 Oct 2026 17:53:53 +0200 Subject: [PATCH 7/9] test: run both svelte-check gate tests through one workspace helper The strict-check test moves next to the native-compiler test. Both read the check command from package.json in one place, and the preload extends NODE_OPTIONS instead of replacing it. --- tests/tooling/svelteCheck.test.ts | 69 +++++++++++++++++++++++------- tests/tooling/sveltePolicy.test.ts | 26 +---------- tooling/README.md | 4 +- 3 files changed, 56 insertions(+), 43 deletions(-) diff --git a/tests/tooling/svelteCheck.test.ts b/tests/tooling/svelteCheck.test.ts index f61bb9f7e..d634f45b6 100644 --- a/tests/tooling/svelteCheck.test.ts +++ b/tests/tooling/svelteCheck.test.ts @@ -6,35 +6,72 @@ import { join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { expect, it } from "vitest"; -// module.registerHooks, which the preload below uses, arrived in Node 22.15. -it.skipIf(typeof module.registerHooks !== "function")("fails Svelte checking on a type error when the native TypeScript executable cannot run", () => { - const root = fileURLToPath(new URL("../../", import.meta.url)); - const workspace = mkdtempSync(join(tmpdir(), "quickadd-svelte-check-native-")); +const root = fileURLToPath(new URL("../../", import.meta.url)); + +function tsconfig(include: string[]): string { + return JSON.stringify({ compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "bundler", lib: ["DOM", "ES2022"], strict: true, skipLibCheck: true }, include }); +} + +function withWorkspace(files: Record, run: (workspace: string) => void): void { + const workspace = mkdtempSync(join(tmpdir(), "quickadd-svelte-check-")); try { symlinkSync(join(root, "node_modules"), join(workspace, "node_modules"), process.platform === "win32" ? "junction" : "dir"); copyFileSync(join(root, "svelte.config.mjs"), join(workspace, "svelte.config.mjs")); - writeFileSync(join(workspace, "tsconfig.json"), JSON.stringify({ compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "bundler", lib: ["DOM", "ES2022"], strict: true, skipLibCheck: true }, include: ["*.ts"] })); - writeFileSync(join(workspace, "entry.ts"), 'export { default as Probe } from "./Probe.svelte";'); - writeFileSync(join(workspace, "Probe.svelte"), '

{n}

'); + for (const [name, text] of Object.entries(files)) writeFileSync(join(workspace, name), text); + run(workspace); + } finally { + rmSync(workspace, { recursive: true, force: true }); + } +} + +function runCheck(workspace: string, env: NodeJS.ProcessEnv = process.env) { + const { scripts }: { scripts: { check: string } } = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); + const result = spawnSync(process.execPath, [join(root, "node_modules/svelte-check/bin/svelte-check"), ...scripts.check.split(" ").slice(1), "--output", "machine-verbose"], { cwd: workspace, encoding: "utf8", env, timeout: 30_000 }); + expect(result.error).toBeUndefined(); + return result; +} + +it("fails strict Svelte checking on warnings, invalid handler types, and legacy reactivity", () => { + withWorkspace({ + "tsconfig.json": tsconfig(["*.svelte"]), + "accessibility.svelte": "
{}}>X
", + "unused-css.svelte": "

X

", + "handler.svelte": '', + "legacy.svelte": "{n}", + }, workspace => { + const result = runCheck(workspace); + expect(result.status, result.stderr).toBe(1); + for (const filename of ["accessibility.svelte", "unused-css.svelte", "handler.svelte", "legacy.svelte"]) expect(result.stdout).toContain(filename); + expect(result.stdout).toContain("a11y_click_events_have_key_events"); + expect(result.stdout).toContain("css_unused_selector"); + expect(result.stdout).toContain("legacy_reactive_statement_invalid"); + expect(result.stdout).toContain("2322"); + }); +}, 30_000); + +// module.registerHooks, which the preload below uses, arrived in Node 22.15. +it.skipIf(typeof module.registerHooks !== "function")("fails Svelte checking on a type error when the native TypeScript executable cannot run", () => { + withWorkspace({ + "tsconfig.json": tsconfig(["*.ts"]), + "entry.ts": 'export { default as Probe } from "./Probe.svelte";', + "Probe.svelte": '

{n}

', // The native compiler's launcher resolves its platform package at startup, so this // reproduces an install without that package in every Node process the check starts. - writeFileSync(join(workspace, "no-native-typescript.mjs"), [ + "no-native-typescript.mjs": [ 'import { registerHooks } from "node:module";', 'registerHooks({ resolve: (specifier, context, next) => { if (specifier.startsWith("@typescript/typescript-")) throw new Error(`Cannot find package ${specifier}`); return next(specifier, context); } });', - ].join("\n")); - const env = { ...process.env, NODE_OPTIONS: `--import ${pathToFileURL(join(workspace, "no-native-typescript.mjs")).href}` }; + ].join("\n"), + }, workspace => { + const preload = `--import ${pathToFileURL(join(workspace, "no-native-typescript.mjs")).href}`; + const env = { ...process.env, NODE_OPTIONS: [process.env.NODE_OPTIONS, preload].filter(Boolean).join(" ") }; const nativeTsc = spawnSync(process.execPath, [join(root, "node_modules/@typescript/native/bin/tsc"), "--version"], { cwd: workspace, encoding: "utf8", env, timeout: 30_000 }); expect(nativeTsc.status).not.toBe(0); expect(nativeTsc.stderr).toContain("Unable to resolve @typescript/typescript-"); - const manifest: { scripts: { check: string } } = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); - const check = spawnSync(process.execPath, [join(root, "node_modules/svelte-check/bin/svelte-check"), ...manifest.scripts.check.split(" ").slice(1), "--output", "machine-verbose"], { cwd: workspace, encoding: "utf8", env, timeout: 30_000 }); - expect(check.error).toBeUndefined(); + const check = runCheck(workspace, env); expect(check.status, check.stdout + check.stderr).toBe(1); expect(check.stdout).toContain('"Probe.svelte"'); expect(check.stdout).toContain('"code":2322'); - } finally { - rmSync(workspace, { recursive: true, force: true }); - } + }); }, 30_000); diff --git a/tests/tooling/sveltePolicy.test.ts b/tests/tooling/sveltePolicy.test.ts index cd3cda5ad..464102a2c 100644 --- a/tests/tooling/sveltePolicy.test.ts +++ b/tests/tooling/sveltePolicy.test.ts @@ -1,5 +1,5 @@ import { spawnSync } from "node:child_process"; -import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { copyFileSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { basename, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -244,28 +244,4 @@ describe("Svelte template policies", () => { const unused = compile("

X

", { generate: false, runes: true }); expect(unused.warnings.map(item => item.code)).toContain("css_unused_selector"); }); - it("fails strict Svelte checking on warnings, invalid handler types, and legacy reactivity", () => { - const root = fileURLToPath(new URL("../../", import.meta.url)); - const workspace = mkdtempSync(join(tmpdir(), "quickadd-svelte-check-")); - try { - symlinkSync(join(root, "node_modules"), join(workspace, "node_modules"), process.platform === "win32" ? "junction" : "dir"); - copyFileSync(join(root, "svelte.config.mjs"), join(workspace, "svelte.config.mjs")); - writeFileSync(join(workspace, "tsconfig.json"), JSON.stringify({ compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "bundler", lib: ["DOM", "ES2022"], strict: true, skipLibCheck: true }, include: ["*.svelte"] })); - writeFileSync(join(workspace, "accessibility.svelte"), "
{}}>X
"); - writeFileSync(join(workspace, "unused-css.svelte"), "

X

"); - writeFileSync(join(workspace, "handler.svelte"), ''); - writeFileSync(join(workspace, "legacy.svelte"), "{n}"); - const manifest: { scripts: { check: string } } = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); - const result = spawnSync(process.execPath, [join(root, "node_modules/svelte-check/bin/svelte-check"), ...manifest.scripts.check.split(" ").slice(1), "--output", "machine-verbose"], { cwd: workspace, encoding: "utf8", timeout: 30_000 }); - expect(result.error).toBeUndefined(); - expect(result.status, result.stderr).toBe(1); - for (const filename of ["accessibility.svelte", "unused-css.svelte", "handler.svelte", "legacy.svelte"]) expect(result.stdout).toContain(filename); - expect(result.stdout).toContain("a11y_click_events_have_key_events"); - expect(result.stdout).toContain("css_unused_selector"); - expect(result.stdout).toContain("legacy_reactive_statement_invalid"); - expect(result.stdout).toContain("2322"); - } finally { - rmSync(workspace, { recursive: true, force: true }); - } - }, 30_000); }); diff --git a/tooling/README.md b/tooling/README.md index 378170e5c..f94b430f0 100644 --- a/tooling/README.md +++ b/tooling/README.md @@ -87,8 +87,8 @@ TypeScript, component scripts, and inline template handlers. It also checks valid examples, the test-file exclusion, matching diagnostics between Oxlint and the component checker, and template policy failures. Temporary fixtures are private and removed after the test. `tests/tooling/svelteCheck.test.ts` -runs `pnpm run check`'s command with the native compiler unable to start and -requires it to fail on a component type error. +requires `pnpm run check`'s command to fail on compiler warnings, legacy +reactivity, and type errors, including when the native compiler cannot start. ```sh pnpm exec vitest run tests/tooling/sveltePolicy.test.ts tests/tooling/svelteCheck.test.ts From d655ef0faee0b65c1029111711c80b5525054f1f Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 12 Oct 2026 17:53:53 +0200 Subject: [PATCH 8/9] ci: drop the duplicate check from release verification pnpm run lint already ends with pnpm run check. --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e4251641..049229437 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,7 +42,6 @@ jobs: release-assets: '["main.js","manifest.json","styles.css"]' verify-commands: | pnpm run lint - pnpm run check pnpm run test notify-name: QuickAdd secrets: From 3be3032d3d99428ac13317cca9ee225a853b4790 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 12 Oct 2026 18:06:01 +0200 Subject: [PATCH 9/9] test: assert the check script runs svelte-check and fails in one helper Also label the .gitignore entry and restore the README condition that svelte-check must run on the native compiler's API before the TypeScript 6 package can go. --- .gitignore | 2 ++ tests/tooling/svelteCheck.test.ts | 29 +++++++++++++++-------------- tooling/README.md | 4 ++-- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index d72db0a30..63aad240e 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,6 @@ coverage/ # Obsidian application bundle (only ever extracted transiently for inspection) /app.js + +# Overlays from svelte-check --tsgo or --incremental runs .svelte-check/ diff --git a/tests/tooling/svelteCheck.test.ts b/tests/tooling/svelteCheck.test.ts index d634f45b6..15be3ce5f 100644 --- a/tests/tooling/svelteCheck.test.ts +++ b/tests/tooling/svelteCheck.test.ts @@ -24,11 +24,14 @@ function withWorkspace(files: Record, run: (workspace: string) = } } -function runCheck(workspace: string, env: NodeJS.ProcessEnv = process.env) { +function expectCheckFails(workspace: string, env: NodeJS.ProcessEnv = process.env): string { const { scripts }: { scripts: { check: string } } = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); - const result = spawnSync(process.execPath, [join(root, "node_modules/svelte-check/bin/svelte-check"), ...scripts.check.split(" ").slice(1), "--output", "machine-verbose"], { cwd: workspace, encoding: "utf8", env, timeout: 30_000 }); + const [bin, ...args] = scripts.check.split(" "); + expect(bin).toBe("svelte-check"); + const result = spawnSync(process.execPath, [join(root, "node_modules/svelte-check/bin/svelte-check"), ...args, "--output", "machine-verbose"], { cwd: workspace, encoding: "utf8", env, timeout: 30_000 }); expect(result.error).toBeUndefined(); - return result; + expect(result.status, result.stdout + result.stderr).toBe(1); + return result.stdout; } it("fails strict Svelte checking on warnings, invalid handler types, and legacy reactivity", () => { @@ -39,13 +42,12 @@ it("fails strict Svelte checking on warnings, invalid handler types, and legacy "handler.svelte": '', "legacy.svelte": "{n}", }, workspace => { - const result = runCheck(workspace); - expect(result.status, result.stderr).toBe(1); - for (const filename of ["accessibility.svelte", "unused-css.svelte", "handler.svelte", "legacy.svelte"]) expect(result.stdout).toContain(filename); - expect(result.stdout).toContain("a11y_click_events_have_key_events"); - expect(result.stdout).toContain("css_unused_selector"); - expect(result.stdout).toContain("legacy_reactive_statement_invalid"); - expect(result.stdout).toContain("2322"); + const output = expectCheckFails(workspace); + for (const filename of ["accessibility.svelte", "unused-css.svelte", "handler.svelte", "legacy.svelte"]) expect(output).toContain(filename); + expect(output).toContain("a11y_click_events_have_key_events"); + expect(output).toContain("css_unused_selector"); + expect(output).toContain("legacy_reactive_statement_invalid"); + expect(output).toContain("2322"); }); }, 30_000); @@ -69,9 +71,8 @@ it.skipIf(typeof module.registerHooks !== "function")("fails Svelte checking on expect(nativeTsc.status).not.toBe(0); expect(nativeTsc.stderr).toContain("Unable to resolve @typescript/typescript-"); - const check = runCheck(workspace, env); - expect(check.status, check.stdout + check.stderr).toBe(1); - expect(check.stdout).toContain('"Probe.svelte"'); - expect(check.stdout).toContain('"code":2322'); + const output = expectCheckFails(workspace, env); + expect(output).toContain('"Probe.svelte"'); + expect(output).toContain('"code":2322'); }); }, 30_000); diff --git a/tooling/README.md b/tooling/README.md index f94b430f0..477713121 100644 --- a/tooling/README.md +++ b/tooling/README.md @@ -108,7 +108,7 @@ These refinements follow the tooling migration: - Review the retired CSS checks and decide which merit separate enforcement. - Move checks to upstream Oxlint or Svelte implementations when they cover template expressions, reducing the code maintained here. -- Remove the TypeScript 6 compatibility package when svelte-check fails on a - native compiler that cannot start. +- Remove the TypeScript 6 compatibility package when svelte-check runs on the + native compiler's API and fails when that compiler cannot start. - Measure the final verification pipeline. Earlier evaluation timings predate the custom-rule port and the removal of ESLint.