oxlint-plugin-vize 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ubugeeei
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # oxlint-plugin-vize
2
+
3
+ Oxlint JS plugin bridge for Vize Patina.
4
+
5
+ This package lets Oxlint execute Patina through Vize's native binding while still using Oxlint's JS plugin model and rule configuration.
6
+
7
+ > [!IMPORTANT]
8
+ > `oxlint-plugin-vize` is a terminal-first Vue SFC linting package.
9
+ > Until upstream Vue support in Oxlint matures, prefer `oxlint-vize -f stylish` for day-to-day output and treat machine-readable / full-fidelity original-SFC reporting as best-effort.
10
+
11
+ ## Main Features
12
+
13
+ - Runs Vize Patina rules inside Oxlint as `vize/*` diagnostics, so Vue-specific findings can live beside Oxlint core rules in one command.
14
+ - Keeps Oxlint's existing rules and built-in `vue` plugin active. The bridge adds Vize rules; it does not replace `eqeqeq`, `no-console`, or your existing `vue/*` setup.
15
+ - Ships preset rule maps for JS/TS Oxlint configs: `configs.recommended`, `configs.essential`, `configs.opinionated`, `configs.nuxt`, `configs.all`, and type-aware opt-in variants.
16
+ - Supports runtime settings through `settings.vize`, including `locale`, `preset`, and `helpLevel`.
17
+ - Provides the `oxlint-vize` CLI wrapper, which runs Oxlint with a scriptless-SFC workaround and rewrites temporary paths back to the original `.vue` files.
18
+ - Resolves Vize native bindings through platform-specific optional dependencies, so published installs do not need a separate `@vizejs/native` package.
19
+ - Caches file contents and native rule results for the lifetime of the Oxlint process, reducing duplicate work when several Vize rules are enabled for the same file.
20
+
21
+ ## Performance
22
+
23
+ The bridge is optimized around Oxlint's per-rule execution model:
24
+
25
+ - The first enabled Patina rule on a file runs native linting for that rule only.
26
+ - If a second Patina rule is encountered on the same file, the bridge upgrades to one shared full-file Patina pass and reuses that result for the remaining Patina rules.
27
+ - File contents and rule results are cached per file and locale for the lifetime of the Oxlint process.
28
+
29
+ ## Installation
30
+
31
+ `oxlint-plugin-vize` targets Node 24+. In this repository, Vite+ reads `.node-version` for you, so the usual setup is:
32
+
33
+ ```bash
34
+ vp install
35
+ vp run --filter './npm/vize-native' build
36
+ vp run --filter './npm/oxlint-plugin-vize' build
37
+ ```
38
+
39
+ Install it from npm with:
40
+
41
+ ```bash
42
+ pnpm add -D oxlint oxlint-plugin-vize
43
+ ```
44
+
45
+ `oxlint-plugin-vize` pulls the appropriate Vize native binding for the current platform through optional dependencies, so no separate `@vizejs/native` install is required for published builds.
46
+
47
+ ## Usage
48
+
49
+ Enable Oxlint's built-in `vue` plugin as well as this JS plugin:
50
+
51
+ ```json
52
+ {
53
+ "plugins": ["vue"],
54
+ "jsPlugins": ["oxlint-plugin-vize"],
55
+ "rules": {
56
+ "eqeqeq": "error",
57
+ "no-console": "warn",
58
+ "vize/vue/require-v-for-key": "error",
59
+ "vize/vue/no-v-html": "warn"
60
+ }
61
+ }
62
+ ```
63
+
64
+ This bridge only adds the `vize/*` rules. Oxlint's existing core rules and built-in plugin rules still run as configured, so checks like `eqeqeq`, `no-console`, or your existing `vue/*` setup continue to report normally.
65
+
66
+ If you want a lower-config JS/TS Oxlint setup, the package also exports preset rule maps:
67
+
68
+ ```js
69
+ import { configs } from "oxlint-plugin-vize";
70
+
71
+ export default {
72
+ plugins: ["vue"],
73
+ jsPlugins: ["oxlint-plugin-vize"],
74
+ settings: {
75
+ vize: {
76
+ helpLevel: "short",
77
+ preset: "opinionated",
78
+ },
79
+ },
80
+ rules: configs.opinionated,
81
+ };
82
+ ```
83
+
84
+ `configs.recommended`, `configs.essential`, `configs.opinionated`, `configs.nuxt`, and `configs.all` intentionally skip Vize's unstable type-aware rules for now. If you explicitly want those experimental rules too, use `configs.recommendedWithTypeAware`, `configs.opinionatedWithTypeAware`, or `createVizeRuleConfig({ includeTypeAware: true, preset: ... })`.
85
+
86
+ You can pass Patina settings through `settings.vize`:
87
+
88
+ ```json
89
+ {
90
+ "settings": {
91
+ "vize": {
92
+ "locale": "ja",
93
+ "preset": "essential",
94
+ "helpLevel": "short"
95
+ }
96
+ }
97
+ }
98
+ ```
99
+
100
+ - `preset` accepts `"general-recommended"`, `"essential"`, `"incremental"`, `"opinionated"`, or `"nuxt"`.
101
+ - `preset` defaults to `"general-recommended"`.
102
+ - Bundle presets keep out-of-bundle rules quiet even if they are still listed in `rules`.
103
+ - `"incremental"` skips bundle gating and runs only the Vize rules you explicitly configure in Oxlint.
104
+ - `"opinionated"` is the preset that enables Vize's built-in script rules such as `vize/script/no-options-api`.
105
+ - Legacy aliases such as `"GeneralRecommended"`, `"Essential"`, `"Incremental"`, `"Opinionated"`, `"Nuxt"`, and `"happy-path"` are still accepted for compatibility.
106
+ - `helpLevel` accepts `"full"`, `"short"`, or `"none"`.
107
+ - `helpLevel: "full"` only expands the Patina remediation text. It does not restore original-SFC formatter anchors or machine-readable range fidelity.
108
+ - `showHelp` is still accepted for backward compatibility, but `helpLevel` is the preferred setting.
109
+
110
+ For example, this keeps Oxlint focused on correctness-only Vize diagnostics while still allowing your existing Oxlint rules to run unchanged:
111
+
112
+ ```json
113
+ {
114
+ "settings": {
115
+ "vize": {
116
+ "preset": "essential",
117
+ "helpLevel": "short"
118
+ }
119
+ },
120
+ "rules": {
121
+ "vize/vue/require-v-for-key": "error",
122
+ "vize/vue/require-scoped-style": "error"
123
+ }
124
+ }
125
+ ```
126
+
127
+ In that config, `vize/vue/require-v-for-key` can report, while `vize/vue/require-scoped-style` stays silent because it belongs to the broader `"general-recommended"` preset.
128
+
129
+ If you want to adopt Vize one rule at a time, use `"preset": "incremental"`. In that mode, preset membership no longer suppresses configured rules, so only the Vize rules you list under `rules` will run.
130
+
131
+ For day-to-day terminal runs, the recommended command today is:
132
+
133
+ ```bash
134
+ pnpm exec oxlint-vize -c .oxlintrc.json -f stylish src
135
+ ```
136
+
137
+ `oxlint-vize` is a thin wrapper around `oxlint`. Until upstream JS plugin coverage improves, it appends a temporary `<script setup>` block only for scriptless `.vue` files so Oxlint's JS plugin pipeline still invokes Vize, then rewrites reported paths back to the original files.
138
+ `stylish` is currently the most usable compromise for mixed Oxlint + Vize output because the Patina summary can inline the original SFC location even though Oxlint still anchors JS plugin diagnostics to the extracted script program.
139
+
140
+ ## Limitations
141
+
142
+ - Raw `oxlint` still misses files without `<script>` or `<script setup>`. The temporary `oxlint-vize` wrapper works around this by generating a transient script block for scriptless `.vue` files before invoking `oxlint`.
143
+ - Oxlint JS plugins only accept ranges inside the extracted Vue script program. For template diagnostics, Vize now inlines the original SFC block and `line:column` into the summary, while the formatter anchor still points at the script block.
144
+ - Formatter parity is not there yet. `stylish` is recommended for human-readable terminal output, while `json` and other machine-readable outputs are best treated as debugging aids for original template/style positions.
145
+ - Oxlint core rules that need JavaScript bindings extracted from Vue templates, such as template-aware unused-variable checks, still depend on upstream work in [Oxc's Better Vue Support](https://github.com/oxc-project/oxc/issues/15761).
146
+ - Vize's own SFC diagnostics can run through the plugin, but precise original-SFC ranges across all Oxlint formatters depend on the JS plugin reporting work tracked in [oxc-project/oxc#20465](https://github.com/oxc-project/oxc/issues/20465).
147
+ - Type-aware Vize rules are experimental and excluded from the default exported configs. Opt into them explicitly with `configs.recommendedWithTypeAware`, `configs.opinionatedWithTypeAware`, or `createVizeRuleConfig({ includeTypeAware: true, preset: ... })`.
148
+
149
+ ## Current expectations
150
+
151
+ - This release is meant for terminal-first workflows.
152
+ - It is not yet a promise of precise original-SFC spans across every Oxlint formatter.
153
+ - Once Oxlint can preserve original Vue positions for JS plugins reliably, Vize can improve formatter parity and machine-readable reporting without relying on summary fallbacks.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../dist/cli.mjs";
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.mjs ADDED
@@ -0,0 +1,249 @@
1
+ import { n as hasScriptLikeBlock, t as appendScriptlessWorkaround } from "./workaround-57-10EZz.mjs";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ //#region src/cli/args.ts
6
+ const OPTION_NAMES_WITH_VALUES = new Set([
7
+ "-A",
8
+ "-D",
9
+ "-W",
10
+ "-c",
11
+ "-f",
12
+ "--config",
13
+ "--cwd",
14
+ "--deny",
15
+ "--fix-suggestions",
16
+ "--format",
17
+ "--ignore-path",
18
+ "--ignore-pattern",
19
+ "--import-plugin",
20
+ "--jsx-a11y-plugin",
21
+ "--max-warnings",
22
+ "--nextjs-plugin",
23
+ "--node-plugin",
24
+ "--promise-plugin",
25
+ "--react-perf-plugin",
26
+ "--react-plugin",
27
+ "--threads",
28
+ "--tsconfig",
29
+ "--typescript-plugin",
30
+ "--unicorn-plugin",
31
+ "--vitest-plugin",
32
+ "--warn"
33
+ ]);
34
+ function getLintTargets(argv) {
35
+ const targets = [];
36
+ let collectEverything = false;
37
+ for (let index = 0; index < argv.length; index += 1) {
38
+ const arg = argv[index];
39
+ if (collectEverything) {
40
+ targets.push(arg);
41
+ continue;
42
+ }
43
+ if (arg === "--") {
44
+ collectEverything = true;
45
+ continue;
46
+ }
47
+ if (arg.startsWith("--") && arg.includes("=")) continue;
48
+ if (OPTION_NAMES_WITH_VALUES.has(arg)) {
49
+ index += 1;
50
+ continue;
51
+ }
52
+ if (arg.startsWith("-")) continue;
53
+ targets.push(arg);
54
+ }
55
+ return targets.length === 0 ? ["."] : targets;
56
+ }
57
+ //#endregion
58
+ //#region src/cli/files.ts
59
+ const GLOB_PATTERN = /[*?[\]{}]/u;
60
+ function collectVueFilesFromTargets(cwd, targets) {
61
+ const files = /* @__PURE__ */ new Set();
62
+ for (const target of targets) for (const file of collectVueFilesFromTarget(cwd, target)) files.add(file);
63
+ return [...files];
64
+ }
65
+ function collectVueFilesFromTarget(cwd, target) {
66
+ if (GLOB_PATTERN.test(target)) return fs.globSync(target, {
67
+ cwd,
68
+ withFileTypes: false,
69
+ exclude: ["**/node_modules/**", "**/.git/**"]
70
+ }).map((entry) => path.resolve(cwd, entry)).filter(isVueFile);
71
+ const absoluteTarget = path.resolve(cwd, target);
72
+ if (!fs.existsSync(absoluteTarget)) return [];
73
+ if (fs.statSync(absoluteTarget).isDirectory()) return fs.globSync("**/*.vue", {
74
+ cwd: absoluteTarget,
75
+ withFileTypes: false,
76
+ exclude: ["**/node_modules/**", "**/.git/**"]
77
+ }).map((entry) => path.resolve(absoluteTarget, entry));
78
+ return isVueFile(absoluteTarget) ? [absoluteTarget] : [];
79
+ }
80
+ function isVueFile(filename) {
81
+ return filename.endsWith(".vue");
82
+ }
83
+ //#endregion
84
+ //#region src/cli/oxlint.ts
85
+ const OXLINT_ENTRYPOINT_SEGMENTS = [
86
+ "node_modules",
87
+ "oxlint",
88
+ "bin",
89
+ "oxlint"
90
+ ];
91
+ const PNPM_OXLINT_ENTRYPOINT_PATTERN = "node_modules/.pnpm/oxlint@*/node_modules/oxlint/bin/oxlint";
92
+ function resolveOxlintCliEntrypoint(cwd) {
93
+ let currentDir = cwd;
94
+ for (;;) {
95
+ const candidate = path.join(currentDir, ...OXLINT_ENTRYPOINT_SEGMENTS);
96
+ if (fs.existsSync(candidate)) return candidate;
97
+ const pnpmCandidate = fs.globSync(PNPM_OXLINT_ENTRYPOINT_PATTERN, {
98
+ cwd: currentDir,
99
+ withFileTypes: false
100
+ })[0];
101
+ if (pnpmCandidate != null) return path.resolve(currentDir, pnpmCandidate);
102
+ const parentDir = path.dirname(currentDir);
103
+ if (parentDir === currentDir) throw new Error("Unable to locate oxlint. Install `oxlint` in the current workspace before using `oxlint-vize`.");
104
+ currentDir = parentDir;
105
+ }
106
+ }
107
+ //#endregion
108
+ //#region src/cli/output.ts
109
+ function rewriteReportedPaths(output, replacements) {
110
+ let rewritten = output;
111
+ const orderedReplacements = [...replacements].sort((left, right) => right[0].length - left[0].length);
112
+ for (const [from, to] of orderedReplacements) rewritten = rewritten.split(from).join(to);
113
+ return rewritten;
114
+ }
115
+ if (import.meta.vitest) {
116
+ const { describe, expect, it } = import.meta.vitest;
117
+ describe("rewriteReportedPaths", () => {
118
+ it("rewrites both absolute and relative temporary filenames", () => {
119
+ const replacements = new Map([["/repo/__oxlint_plugin_vize_temp__/100/0-Example.vue", "/repo/src/Example.vue"], ["__oxlint_plugin_vize_temp__/100/0-Example.vue", "/repo/src/Example.vue"]]);
120
+ expect(rewriteReportedPaths(["__oxlint_plugin_vize_temp__/100/0-Example.vue", "/repo/__oxlint_plugin_vize_temp__/100/0-Example.vue"].join("\n"), replacements)).toBe(["/repo/src/Example.vue", "/repo/src/Example.vue"].join("\n"));
121
+ });
122
+ });
123
+ }
124
+ //#endregion
125
+ //#region src/cli/workaround-files.ts
126
+ function prepareScriptlessWorkaroundFiles(cwd, filenames) {
127
+ const tempDir = path.join(cwd, "__oxlint_plugin_vize_temp__", String(process.pid));
128
+ const ignoreArgs = [];
129
+ const tempArgs = [];
130
+ const pathReplacements = /* @__PURE__ */ new Map();
131
+ let counter = 0;
132
+ for (const filename of filenames) {
133
+ const source = fs.readFileSync(filename, "utf8");
134
+ if (hasScriptLikeBlock(source)) continue;
135
+ const relativeFilename = path.relative(cwd, filename);
136
+ const tempFilename = path.join(tempDir, `${counter}-${path.basename(filename)}`);
137
+ counter += 1;
138
+ fs.mkdirSync(path.dirname(tempFilename), { recursive: true });
139
+ fs.writeFileSync(tempFilename, appendScriptlessWorkaround(source, filename));
140
+ ignoreArgs.push("--ignore-pattern", toCliPath(relativeFilename));
141
+ tempArgs.push(tempFilename);
142
+ registerPathReplacementVariants(pathReplacements, cwd, tempFilename, filename);
143
+ }
144
+ return {
145
+ appendedArgs: [...ignoreArgs, ...tempArgs],
146
+ cleanup() {
147
+ if (pathReplacements.size === 0) return;
148
+ fs.rmSync(tempDir, {
149
+ force: true,
150
+ recursive: true
151
+ });
152
+ },
153
+ pathReplacements,
154
+ usedScriptlessWorkaround: pathReplacements.size > 0
155
+ };
156
+ }
157
+ function toCliPath(filename) {
158
+ return filename.split(path.sep).join("/");
159
+ }
160
+ function registerPathReplacementVariants(replacements, cwd, tempFilename, originalFilename) {
161
+ const relativeTempFilename = path.relative(cwd, tempFilename);
162
+ const relativeOriginalFilename = getReportedOriginalFilename(cwd, originalFilename);
163
+ const variants = new Set([
164
+ [tempFilename, relativeOriginalFilename],
165
+ [toCliPath(tempFilename), toCliPath(relativeOriginalFilename)],
166
+ [relativeTempFilename, relativeOriginalFilename],
167
+ [toCliPath(relativeTempFilename), toCliPath(relativeOriginalFilename)]
168
+ ]);
169
+ for (const [from, to] of variants) {
170
+ if (!from || !to) continue;
171
+ replacements.set(from, to);
172
+ }
173
+ }
174
+ function getReportedOriginalFilename(cwd, filename) {
175
+ const relativeFilename = path.relative(cwd, filename);
176
+ if (relativeFilename && !relativeFilename.startsWith(`..${path.sep}`) && relativeFilename !== ".." && !path.isAbsolute(relativeFilename)) return relativeFilename;
177
+ return filename;
178
+ }
179
+ //#endregion
180
+ //#region src/cli.ts
181
+ async function main() {
182
+ const cwd = process.cwd();
183
+ const forwardedArgs = process.argv.slice(2);
184
+ const prepared = prepareScriptlessWorkaroundFiles(cwd, collectVueFilesFromTargets(cwd, getLintTargets(forwardedArgs)));
185
+ const args = [
186
+ resolveOxlintCliEntrypoint(cwd),
187
+ ...forwardedArgs,
188
+ ...prepared.appendedArgs
189
+ ];
190
+ try {
191
+ const result = await runOxlint(process.execPath, args, cwd);
192
+ const stdout = rewriteReportedPaths(result.stdout, prepared.pathReplacements);
193
+ const stderr = rewriteReportedPaths(result.stderr, prepared.pathReplacements);
194
+ if (stdout) await writeStream(process.stdout, stdout);
195
+ if (stderr) await writeStream(process.stderr, stderr);
196
+ if (prepared.usedScriptlessWorkaround && forwardedArgs.includes("--fix")) await writeStream(process.stderr, "\n[oxlint-plugin-vize] Scriptless SFC workaround is active; fixes are not applied back to original .vue files yet.\n");
197
+ process.exitCode = result.status ?? 1;
198
+ } finally {
199
+ prepared.cleanup();
200
+ }
201
+ }
202
+ main().catch((error) => {
203
+ process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
204
+ process.exitCode = 1;
205
+ });
206
+ function runOxlint(executable, args, cwd) {
207
+ return new Promise((resolve, reject) => {
208
+ const child = spawn(executable, args, {
209
+ cwd,
210
+ stdio: [
211
+ "ignore",
212
+ "pipe",
213
+ "pipe"
214
+ ]
215
+ });
216
+ const stdoutChunks = [];
217
+ const stderrChunks = [];
218
+ child.stdout.on("data", (chunk) => {
219
+ stdoutChunks.push(asBuffer(chunk));
220
+ });
221
+ child.stderr.on("data", (chunk) => {
222
+ stderrChunks.push(asBuffer(chunk));
223
+ });
224
+ child.on("error", reject);
225
+ child.on("close", (status) => {
226
+ resolve({
227
+ status,
228
+ stderr: Buffer.concat(stderrChunks).toString("utf8"),
229
+ stdout: Buffer.concat(stdoutChunks).toString("utf8")
230
+ });
231
+ });
232
+ });
233
+ }
234
+ function asBuffer(chunk) {
235
+ return typeof chunk === "string" ? Buffer.from(chunk) : chunk;
236
+ }
237
+ function writeStream(stream, text) {
238
+ return new Promise((resolve, reject) => {
239
+ stream.write(text, (error) => {
240
+ if (error) {
241
+ reject(error);
242
+ return;
243
+ }
244
+ resolve();
245
+ });
246
+ });
247
+ }
248
+ //#endregion
249
+ export {};
@@ -0,0 +1,28 @@
1
+ import * as _oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/plugin.d.ts
4
+ declare const _default: _oxlint_plugins0.Plugin;
5
+ //#endregion
6
+ //#region src/model.d.ts
7
+ type PatinaPreset = "general-recommended" | "essential" | "incremental" | "opinionated" | "nuxt";
8
+ //#endregion
9
+ //#region src/configs.d.ts
10
+ type OxlintRuleSeverity = "error" | "warn";
11
+ type OxlintRuleConfig = Record<string, OxlintRuleSeverity>;
12
+ type VizeRuleConfigPreset = Exclude<PatinaPreset, "incremental"> | "all";
13
+ interface VizeRuleConfigOptions {
14
+ includeTypeAware?: boolean;
15
+ preset?: VizeRuleConfigPreset;
16
+ }
17
+ declare function createVizeRuleConfig(options?: VizeRuleConfigOptions): OxlintRuleConfig;
18
+ declare const configs: {
19
+ readonly all: OxlintRuleConfig;
20
+ readonly essential: OxlintRuleConfig;
21
+ readonly nuxt: OxlintRuleConfig;
22
+ readonly opinionated: OxlintRuleConfig;
23
+ readonly opinionatedWithTypeAware: OxlintRuleConfig;
24
+ readonly recommended: OxlintRuleConfig;
25
+ readonly recommendedWithTypeAware: OxlintRuleConfig;
26
+ };
27
+ //#endregion
28
+ export { type OxlintRuleConfig, type OxlintRuleSeverity, type VizeRuleConfigOptions, configs, createVizeRuleConfig, _default as default };
package/dist/index.mjs ADDED
@@ -0,0 +1,484 @@
1
+ import { a as extractSfcBlocks, i as compareLineColumn, o as formatBlockLabel, r as resolveWorkaroundSource, s as getDiagnosticBlock } from "./workaround-57-10EZz.mjs";
2
+ import { createRequire } from "node:module";
3
+ import { definePlugin, defineRule } from "@oxlint/plugins";
4
+ import fs, { readFileSync } from "node:fs";
5
+ import path from "node:path";
6
+ //#region src/native.ts
7
+ const require = createRequire(import.meta.url);
8
+ const FALLBACK_BINDING_PACKAGE = "@vizejs/native";
9
+ let bindingCache = null;
10
+ function isMusl() {
11
+ const report = process.report?.getReport();
12
+ if (typeof report === "object" && report !== null && "header" in report) return !report.header.glibcVersionRuntime;
13
+ try {
14
+ return readFileSync(require("node:child_process").execSync("which ldd").toString().trim(), "utf8").includes("musl");
15
+ } catch {
16
+ return true;
17
+ }
18
+ }
19
+ function getBindingPackageName() {
20
+ const { arch, platform } = process;
21
+ switch (platform) {
22
+ case "darwin": switch (arch) {
23
+ case "arm64": return "@vizejs/native-darwin-arm64";
24
+ case "x64": return "@vizejs/native-darwin-x64";
25
+ default: throw new Error(`Unsupported architecture on macOS: ${arch}`);
26
+ }
27
+ case "linux": switch (arch) {
28
+ case "arm64": return isMusl() ? "@vizejs/native-linux-arm64-musl" : "@vizejs/native-linux-arm64-gnu";
29
+ case "x64": return isMusl() ? "@vizejs/native-linux-x64-musl" : "@vizejs/native-linux-x64-gnu";
30
+ default: throw new Error(`Unsupported architecture on Linux: ${arch}`);
31
+ }
32
+ case "win32": switch (arch) {
33
+ case "arm64": return "@vizejs/native-win32-arm64-msvc";
34
+ case "x64": return "@vizejs/native-win32-x64-msvc";
35
+ default: throw new Error(`Unsupported architecture on Windows: ${arch}`);
36
+ }
37
+ default: throw new Error(`Unsupported OS: ${platform}`);
38
+ }
39
+ }
40
+ function isPatinaBinding(binding) {
41
+ return typeof binding.lintPatinaSfc === "function" && typeof binding.getPatinaRules === "function";
42
+ }
43
+ function loadBinding() {
44
+ if (bindingCache) return bindingCache;
45
+ const attemptedPackages = getAttemptedPackages();
46
+ let lastError = null;
47
+ for (const packageName of attemptedPackages) try {
48
+ const binding = require(packageName);
49
+ if (!isPatinaBinding(binding)) throw new Error(`${packageName} does not expose the Patina Oxlint bridge.`);
50
+ bindingCache = binding;
51
+ return bindingCache;
52
+ } catch (error) {
53
+ lastError = error;
54
+ }
55
+ const message = lastError instanceof Error && lastError.message ? ` ${lastError.message}` : "";
56
+ throw new Error(`Failed to load the Vize native binding. Tried ${attemptedPackages.join(", ")}.${message}`);
57
+ }
58
+ function getAttemptedPackages() {
59
+ const platformBindingPackage = getBindingPackageName();
60
+ return shouldPreferWorkspaceBinding(resolveFallbackBindingPath()) ? [FALLBACK_BINDING_PACKAGE, platformBindingPackage] : [platformBindingPackage, FALLBACK_BINDING_PACKAGE];
61
+ }
62
+ function resolveFallbackBindingPath() {
63
+ try {
64
+ return require.resolve(FALLBACK_BINDING_PACKAGE);
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+ function shouldPreferWorkspaceBinding(resolvedPath) {
70
+ const override = process.env.VIZE_PREFER_WORKSPACE_BINDING;
71
+ if (override === "1" || override === "true") return true;
72
+ if (override === "0" || override === "false") return false;
73
+ if (resolvedPath == null) return false;
74
+ return resolvedPath.includes(`${path.sep}npm${path.sep}vize-native${path.sep}`);
75
+ }
76
+ if (import.meta.vitest) {
77
+ const { describe, expect, it } = import.meta.vitest;
78
+ describe("shouldPreferWorkspaceBinding", () => {
79
+ it("detects the local workspace native package", () => {
80
+ expect(shouldPreferWorkspaceBinding(`${path.sep}Users${path.sep}example${path.sep}repo${path.sep}npm${path.sep}vize-native${path.sep}index.js`)).toBe(true);
81
+ });
82
+ it("ignores published platform packages", () => {
83
+ expect(shouldPreferWorkspaceBinding(`${path.sep}repo${path.sep}node_modules${path.sep}.pnpm${path.sep}@vizejs+native-darwin-arm64${path.sep}node_modules${path.sep}@vizejs${path.sep}native-darwin-arm64${path.sep}index.js`)).toBe(false);
84
+ });
85
+ it("returns false when the fallback package cannot be resolved", () => {
86
+ expect(shouldPreferWorkspaceBinding(null)).toBe(false);
87
+ });
88
+ });
89
+ }
90
+ //#endregion
91
+ //#region src/binding.ts
92
+ function lintPatina(source, filename, settings, enabledRules) {
93
+ return loadBinding().lintPatinaSfc(source, {
94
+ filename,
95
+ locale: settings.locale,
96
+ helpLevel: settings.helpLevel,
97
+ preset: settings.preset,
98
+ enabledRules: enabledRules ? [...enabledRules] : void 0
99
+ });
100
+ }
101
+ function getPatinaRules() {
102
+ return loadBinding().getPatinaRules();
103
+ }
104
+ //#endregion
105
+ //#region src/script-map.ts
106
+ function createSingleScriptMap(source, extractedScript) {
107
+ if (!extractedScript) return null;
108
+ const blocks = extractSfcBlocks(source).filter((block) => block.kind === "script" || block.kind === "script-setup");
109
+ if (blocks.length !== 1) return null;
110
+ const [block] = blocks;
111
+ return block.content === extractedScript ? { block } : null;
112
+ }
113
+ function mapToScriptLoc(diagnostic, scriptMap) {
114
+ if (!scriptMap) return null;
115
+ const { block } = scriptMap;
116
+ if (compareLineColumn(diagnostic.location.start, block.contentStart) < 0 || compareLineColumn(diagnostic.location.end, block.contentEnd) > 0) return null;
117
+ return {
118
+ start: toScriptPosition(diagnostic.location.start, block.contentStart),
119
+ end: toScriptPosition(diagnostic.location.end, block.contentStart)
120
+ };
121
+ }
122
+ function toScriptPosition(position, contentStart) {
123
+ if (position.line === contentStart.line) return {
124
+ line: 1,
125
+ column: position.column - contentStart.column + 1
126
+ };
127
+ return {
128
+ line: position.line - contentStart.line + 1,
129
+ column: position.column
130
+ };
131
+ }
132
+ //#endregion
133
+ //#region src/settings.ts
134
+ const HELP_LEVELS = new Set([
135
+ "none",
136
+ "short",
137
+ "full"
138
+ ]);
139
+ const PRESET_ALIASES = new Map([
140
+ ["generalrecommended", "general-recommended"],
141
+ ["happypath", "general-recommended"],
142
+ ["happy", "general-recommended"],
143
+ ["default", "general-recommended"],
144
+ ["recommended", "general-recommended"],
145
+ ["essential", "essential"],
146
+ ["incremental", "incremental"],
147
+ ["opinionated", "opinionated"],
148
+ ["opnionated", "opinionated"],
149
+ ["strict", "opinionated"],
150
+ ["all", "opinionated"],
151
+ ["nuxt", "nuxt"]
152
+ ]);
153
+ function isVueLikeFile(filename) {
154
+ return filename.endsWith(".vue");
155
+ }
156
+ function getVizeSettings(context) {
157
+ const settings = context.settings;
158
+ return parseVizeSettings(settings.vize ?? settings.patina);
159
+ }
160
+ function parseVizeSettings(vize) {
161
+ if (typeof vize !== "object" || vize === null || Array.isArray(vize)) return {};
162
+ const vizeRecord = vize;
163
+ const locale = vizeRecord.locale;
164
+ const helpLevel = vizeRecord.helpLevel;
165
+ const preset = vizeRecord.preset;
166
+ const showHelp = vizeRecord.showHelp;
167
+ const resolved = {};
168
+ if (typeof locale === "string") resolved.locale = locale;
169
+ if (typeof preset === "string") {
170
+ const normalizedPreset = normalizePreset(preset);
171
+ if (normalizedPreset) resolved.preset = normalizedPreset;
172
+ }
173
+ if (typeof helpLevel === "string" && HELP_LEVELS.has(helpLevel)) {
174
+ resolved.helpLevel = helpLevel;
175
+ return resolved;
176
+ }
177
+ if (typeof showHelp === "boolean") resolved.helpLevel = showHelp ? "full" : "none";
178
+ return resolved;
179
+ }
180
+ function getActivePreset(settings) {
181
+ return settings.preset ?? "general-recommended";
182
+ }
183
+ function isIncrementalPreset(settings) {
184
+ return getActivePreset(settings) === "incremental";
185
+ }
186
+ function getCacheKey(filename, settings) {
187
+ return `${filename}::${settings.locale ?? ""}::${settings.helpLevel ?? ""}::${getActivePreset(settings)}`;
188
+ }
189
+ function normalizePreset(value) {
190
+ return PRESET_ALIASES.get(value.replaceAll(/[-_\s]/gu, "").toLowerCase());
191
+ }
192
+ if (import.meta.vitest) {
193
+ const { describe, expect, it } = import.meta.vitest;
194
+ describe("parseVizeSettings", () => {
195
+ it("reads locale, help level, and preset", () => {
196
+ expect(parseVizeSettings({
197
+ locale: "ja",
198
+ helpLevel: "short",
199
+ preset: "essential"
200
+ })).toEqual({
201
+ locale: "ja",
202
+ helpLevel: "short",
203
+ preset: "essential"
204
+ });
205
+ });
206
+ it("normalizes preset aliases", () => {
207
+ expect(parseVizeSettings({ preset: "recommended" })).toEqual({ preset: "general-recommended" });
208
+ expect(parseVizeSettings({ preset: "incremental" })).toEqual({ preset: "incremental" });
209
+ expect(parseVizeSettings({ preset: "strict" })).toEqual({ preset: "opinionated" });
210
+ expect(parseVizeSettings({ preset: "Opnionated" })).toEqual({ preset: "opinionated" });
211
+ });
212
+ it("falls back to showHelp for compatibility", () => {
213
+ expect(parseVizeSettings({
214
+ locale: "ja",
215
+ showHelp: false,
216
+ preset: "Nuxt"
217
+ })).toEqual({
218
+ locale: "ja",
219
+ helpLevel: "none",
220
+ preset: "nuxt"
221
+ });
222
+ });
223
+ it("ignores invalid values", () => {
224
+ expect(parseVizeSettings({
225
+ locale: 1,
226
+ showHelp: "no",
227
+ helpLevel: "verbose",
228
+ preset: "wide"
229
+ })).toEqual({});
230
+ });
231
+ it("ignores non-object vize settings", () => {
232
+ expect(parseVizeSettings(null)).toEqual({});
233
+ expect(parseVizeSettings("ja")).toEqual({});
234
+ });
235
+ });
236
+ }
237
+ //#endregion
238
+ //#region src/file-state.ts
239
+ const fileStateCache = /* @__PURE__ */ new Map();
240
+ const EMPTY_DIAGNOSTICS = [];
241
+ function getFileState(context) {
242
+ const settings = getVizeSettings(context);
243
+ const resolvedSource = resolveWorkaroundSource(fs.readFileSync(context.physicalFilename, "utf8"), context.physicalFilename);
244
+ const cacheKey = getCacheKey(resolvedSource.filename, settings);
245
+ const cached = fileStateCache.get(cacheKey);
246
+ if (cached) return cached;
247
+ const state = {
248
+ filename: resolvedSource.filename,
249
+ source: resolvedSource.source,
250
+ extractedScript: context.sourceCode.text,
251
+ usesOriginalLocations: resolvedSource.usesOriginalLocations,
252
+ reportedRules: /* @__PURE__ */ new Set(),
253
+ sfcBlocks: void 0,
254
+ scriptMap: void 0,
255
+ allDiagnosticsByRule: null,
256
+ partialDiagnosticsByRule: /* @__PURE__ */ new Map(),
257
+ requestedRules: /* @__PURE__ */ new Set()
258
+ };
259
+ fileStateCache.set(cacheKey, state);
260
+ return state;
261
+ }
262
+ function getDiagnosticsForRule(context, state, ruleName) {
263
+ if (state.allDiagnosticsByRule) return state.allDiagnosticsByRule.get(ruleName) ?? EMPTY_DIAGNOSTICS;
264
+ const cached = state.partialDiagnosticsByRule.get(ruleName);
265
+ if (cached) return cached;
266
+ const settings = getVizeSettings(context);
267
+ if (isIncrementalPreset(settings)) {
268
+ const diagnostics = lintPatina(state.source, state.filename, settings, [ruleName]).diagnostics;
269
+ const ruleDiagnostics = indexDiagnosticsByRule(diagnostics).get(ruleName) ?? EMPTY_DIAGNOSTICS;
270
+ state.partialDiagnosticsByRule.set(ruleName, ruleDiagnostics);
271
+ return ruleDiagnostics;
272
+ }
273
+ if (state.requestedRules.size === 0) {
274
+ state.requestedRules.add(ruleName);
275
+ const diagnostics = lintPatina(state.source, state.filename, settings, [ruleName]).diagnostics;
276
+ const ruleDiagnostics = indexDiagnosticsByRule(diagnostics).get(ruleName) ?? EMPTY_DIAGNOSTICS;
277
+ state.partialDiagnosticsByRule.set(ruleName, ruleDiagnostics);
278
+ return ruleDiagnostics;
279
+ }
280
+ state.requestedRules.add(ruleName);
281
+ const allDiagnostics = lintPatina(state.source, state.filename, settings).diagnostics;
282
+ state.allDiagnosticsByRule = indexDiagnosticsByRule(allDiagnostics);
283
+ state.partialDiagnosticsByRule.clear();
284
+ return state.allDiagnosticsByRule.get(ruleName) ?? EMPTY_DIAGNOSTICS;
285
+ }
286
+ function getScriptMap(state) {
287
+ if (state.scriptMap !== void 0) return state.scriptMap;
288
+ state.scriptMap = createSingleScriptMap(state.source, state.extractedScript);
289
+ return state.scriptMap;
290
+ }
291
+ function getSfcBlocks(state) {
292
+ if (state.sfcBlocks !== void 0) return state.sfcBlocks;
293
+ state.sfcBlocks = extractSfcBlocks(state.source);
294
+ return state.sfcBlocks;
295
+ }
296
+ function markRuleAsReported(state, ruleName) {
297
+ if (state.reportedRules.has(ruleName)) return false;
298
+ state.reportedRules.add(ruleName);
299
+ return true;
300
+ }
301
+ function indexDiagnosticsByRule(diagnostics) {
302
+ const grouped = /* @__PURE__ */ new Map();
303
+ for (const diagnostic of diagnostics) {
304
+ const existing = grouped.get(diagnostic.rule);
305
+ if (existing) {
306
+ existing.push(diagnostic);
307
+ continue;
308
+ }
309
+ grouped.set(diagnostic.rule, [diagnostic]);
310
+ }
311
+ return grouped;
312
+ }
313
+ //#endregion
314
+ //#region src/format.ts
315
+ const helpTextCache = /* @__PURE__ */ new Map();
316
+ function formatPatinaMessage(diagnostic, options) {
317
+ const sections = [];
318
+ const summaryLine = createSummaryLine(diagnostic.message);
319
+ const details = createDetailsBody(diagnostic.message, summaryLine);
320
+ const summary = options.hasMappedLocation ? summaryLine : `${summaryLine} (${formatInlineLocation(options.blockLabel, diagnostic)})`;
321
+ if (details) sections.push(formatSection("Details", details));
322
+ const helpText = resolveHelpText(diagnostic.help, options.helpLevel);
323
+ if (helpText) sections.push(formatSection("Help", helpText));
324
+ if (sections.length === 0) return summary;
325
+ return `${summary}\n${sections.join("\n")}`;
326
+ }
327
+ function createDetailsBody(message, summary) {
328
+ if (message === summary) return null;
329
+ if (message.startsWith(summary)) return message.slice(summary.length).trim() || null;
330
+ return message;
331
+ }
332
+ function formatSection(title, body) {
333
+ return ` ${title}:\n${indentBlock(body, " ")}`;
334
+ }
335
+ function formatInlineLocation(blockLabel, diagnostic) {
336
+ const { line, column } = diagnostic.location.start;
337
+ return `at ${blockLabel}:${line}:${column}`;
338
+ }
339
+ function resolveHelpText(help, helpLevel) {
340
+ if (help == null || helpLevel === "none") return null;
341
+ const plainText = formatHelpText(help);
342
+ if (helpLevel === "short") return firstMeaningfulLine(plainText);
343
+ return plainText;
344
+ }
345
+ function formatHelpText(help) {
346
+ const cached = helpTextCache.get(help);
347
+ if (cached) return cached;
348
+ const plainText = help.replace(/\r\n?/gu, "\n").replace(/^\s*```[^\n]*$/gmu, "").replace(/\*\*(.*?)\*\*/gu, "$1").replace(/__(.*?)__/gu, "$1").replace(/`([^`\n]+)`/gu, "$1").replace(/^\s*[-*]\s+/gmu, "- ").replace(/\n{3,}/gu, "\n\n").trim();
349
+ helpTextCache.set(help, plainText);
350
+ return plainText;
351
+ }
352
+ function firstMeaningfulLine(text) {
353
+ for (const line of text.split("\n")) {
354
+ const trimmed = line.trim();
355
+ if (trimmed) return trimmed;
356
+ }
357
+ return null;
358
+ }
359
+ function indentBlock(text, indent) {
360
+ return text.split("\n").map((line) => `${indent}${line}`).join("\n");
361
+ }
362
+ function createSummaryLine(message) {
363
+ const firstSentenceEnd = message.indexOf(". ");
364
+ if (firstSentenceEnd === -1) return message;
365
+ return message.slice(0, firstSentenceEnd + 1);
366
+ }
367
+ //#endregion
368
+ //#region src/plugin.ts
369
+ function createOxlintDiagnostic(diagnostic, state, helpLevel) {
370
+ const scriptMap = getScriptMap(state);
371
+ const loc = state.usesOriginalLocations ? createOriginalSfcLoc(diagnostic) : mapToScriptLoc(diagnostic, scriptMap);
372
+ const block = loc === null ? getDiagnosticBlock(diagnostic, getSfcBlocks(state)) : null;
373
+ return {
374
+ loc: loc ?? {
375
+ start: {
376
+ line: 1,
377
+ column: 1
378
+ },
379
+ end: {
380
+ line: 1,
381
+ column: 1
382
+ }
383
+ },
384
+ message: formatPatinaMessage(diagnostic, {
385
+ hasMappedLocation: loc !== null,
386
+ blockLabel: formatBlockLabel(block),
387
+ helpLevel
388
+ })
389
+ };
390
+ }
391
+ function createOriginalSfcLoc(diagnostic) {
392
+ return {
393
+ start: {
394
+ line: diagnostic.location.start.line,
395
+ column: Math.max(0, diagnostic.location.start.column - 1)
396
+ },
397
+ end: {
398
+ line: diagnostic.location.end.line,
399
+ column: Math.max(0, diagnostic.location.end.column - 1)
400
+ }
401
+ };
402
+ }
403
+ function createPatinaRule(ruleMeta) {
404
+ return defineRule({
405
+ meta: {
406
+ type: ruleMeta.defaultSeverity === "error" ? "problem" : "suggestion",
407
+ docs: { description: ruleMeta.description }
408
+ },
409
+ createOnce(context) {
410
+ return { Program() {
411
+ if (!isVueLikeFile(context.filename)) return;
412
+ const settings = getVizeSettings(context);
413
+ const activePreset = getActivePreset(settings);
414
+ if (!isIncrementalPreset(settings) && !ruleMeta.presets.includes(activePreset)) return;
415
+ const helpLevel = settings.helpLevel ?? "full";
416
+ const state = getFileState(context);
417
+ const diagnostics = getDiagnosticsForRule(context, state, ruleMeta.name);
418
+ if (diagnostics.length === 0) return;
419
+ if (!markRuleAsReported(state, ruleMeta.name)) return;
420
+ for (const diagnostic of diagnostics) context.report(createOxlintDiagnostic(diagnostic, state, helpLevel));
421
+ } };
422
+ }
423
+ });
424
+ }
425
+ var plugin_default = definePlugin({
426
+ meta: { name: "vize" },
427
+ rules: Object.fromEntries(getPatinaRules().map((ruleMeta) => [ruleMeta.name, createPatinaRule(ruleMeta)]))
428
+ });
429
+ //#endregion
430
+ //#region src/configs.ts
431
+ const TYPE_AWARE_RULE_PREFIX = "type/";
432
+ function createVizeRuleConfig(options = {}) {
433
+ const preset = options.preset ?? "general-recommended";
434
+ const includeTypeAware = options.includeTypeAware ?? false;
435
+ const rules = {};
436
+ for (const ruleMeta of getPatinaRules()) {
437
+ if (!matchesPreset(ruleMeta, preset)) continue;
438
+ if (!includeTypeAware && isTypeAwareRule(ruleMeta)) continue;
439
+ rules[`vize/${ruleMeta.name}`] = toOxlintSeverity(ruleMeta.defaultSeverity);
440
+ }
441
+ return rules;
442
+ }
443
+ const configs = {
444
+ all: createVizeRuleConfig({ preset: "all" }),
445
+ essential: createVizeRuleConfig({ preset: "essential" }),
446
+ nuxt: createVizeRuleConfig({ preset: "nuxt" }),
447
+ opinionated: createVizeRuleConfig({ preset: "opinionated" }),
448
+ opinionatedWithTypeAware: createVizeRuleConfig({
449
+ includeTypeAware: true,
450
+ preset: "opinionated"
451
+ }),
452
+ recommended: createVizeRuleConfig({ preset: "general-recommended" }),
453
+ recommendedWithTypeAware: createVizeRuleConfig({
454
+ includeTypeAware: true,
455
+ preset: "general-recommended"
456
+ })
457
+ };
458
+ function matchesPreset(ruleMeta, preset) {
459
+ return preset === "all" || ruleMeta.presets.includes(preset);
460
+ }
461
+ function isTypeAwareRule(ruleMeta) {
462
+ return ruleMeta.name.startsWith(TYPE_AWARE_RULE_PREFIX);
463
+ }
464
+ function toOxlintSeverity(severity) {
465
+ return severity === "warning" ? "warn" : severity;
466
+ }
467
+ if (import.meta.vitest) {
468
+ const { describe, expect, it } = import.meta.vitest;
469
+ describe("createVizeRuleConfig", () => {
470
+ it("normalizes Vize warning severity to Oxlint's warn", () => {
471
+ expect(configs.recommended["vize/vue/no-multi-spaces"]).toBe("warn");
472
+ });
473
+ it("filters rules by preset", () => {
474
+ expect(configs.essential["vize/vue/require-v-for-key"]).toBe("error");
475
+ expect(configs.essential["vize/vue/require-scoped-style"]).toBeUndefined();
476
+ });
477
+ it("skips unstable type-aware rules by default", () => {
478
+ expect(configs.opinionated["vize/type/require-typed-props"]).toBeUndefined();
479
+ expect(configs.opinionatedWithTypeAware["vize/type/require-typed-props"]).toBe("warn");
480
+ });
481
+ });
482
+ }
483
+ //#endregion
484
+ export { configs, createVizeRuleConfig, plugin_default as default };
@@ -0,0 +1,151 @@
1
+ //#region src/sfc-blocks.ts
2
+ const rootBlockPattern = /<([A-Za-z][\w-]*)\b[^>]*>/gu;
3
+ function extractSfcBlocks(source) {
4
+ const blocks = [];
5
+ const lineStarts = createLineStartOffsets(source);
6
+ let cursor = 0;
7
+ for (;;) {
8
+ rootBlockPattern.lastIndex = cursor;
9
+ const match = rootBlockPattern.exec(source);
10
+ if (match == null) return blocks;
11
+ const tagName = match[1];
12
+ const openTag = match[0];
13
+ const openTagEnd = match.index + openTag.length;
14
+ const closeTag = `</${tagName}>`;
15
+ const closeTagStart = source.indexOf(closeTag, openTagEnd);
16
+ if (closeTagStart === -1) {
17
+ cursor = openTagEnd;
18
+ continue;
19
+ }
20
+ blocks.push({
21
+ kind: resolveBlockKind(tagName, openTag),
22
+ name: tagName,
23
+ content: source.slice(openTagEnd, closeTagStart),
24
+ contentStart: offsetToLineColumn(lineStarts, openTagEnd),
25
+ contentEnd: offsetToLineColumn(lineStarts, closeTagStart)
26
+ });
27
+ cursor = closeTagStart + closeTag.length;
28
+ }
29
+ }
30
+ function getDiagnosticBlock(diagnostic, blocks) {
31
+ for (const block of blocks) if (compareLineColumn(diagnostic.location.start, block.contentStart) >= 0 && compareLineColumn(diagnostic.location.end, block.contentEnd) <= 0) return block;
32
+ return null;
33
+ }
34
+ function formatBlockLabel(block) {
35
+ if (block == null) return "SFC";
36
+ switch (block.kind) {
37
+ case "template": return "<template>";
38
+ case "script": return "<script>";
39
+ case "script-setup": return "<script setup>";
40
+ case "style": return "<style>";
41
+ case "custom": return `<${block.name}>`;
42
+ }
43
+ }
44
+ function compareLineColumn(left, right) {
45
+ if (left.line !== right.line) return left.line - right.line;
46
+ return left.column - right.column;
47
+ }
48
+ function createLineStartOffsets(source) {
49
+ const lineStarts = [0];
50
+ for (let index = 0; index < source.length; index += 1) if (source.charCodeAt(index) === 10) lineStarts.push(index + 1);
51
+ return lineStarts;
52
+ }
53
+ function offsetToLineColumn(lineStarts, offset) {
54
+ let low = 0;
55
+ let high = lineStarts.length - 1;
56
+ while (low <= high) {
57
+ const middle = low + high >> 1;
58
+ if (lineStarts[middle] <= offset) {
59
+ low = middle + 1;
60
+ continue;
61
+ }
62
+ high = middle - 1;
63
+ }
64
+ const lineIndex = Math.max(0, low - 1);
65
+ return {
66
+ line: lineIndex + 1,
67
+ column: offset - lineStarts[lineIndex] + 1
68
+ };
69
+ }
70
+ function resolveBlockKind(tagName, openTag) {
71
+ if (tagName === "template") return "template";
72
+ if (tagName === "script") return /\bsetup(?:\s|>|=)/u.test(openTag) ? "script-setup" : "script";
73
+ if (tagName === "style") return "style";
74
+ return "custom";
75
+ }
76
+ //#endregion
77
+ //#region src/workaround.ts
78
+ const SCRIPTLESS_WORKAROUND_OPEN_TAG_PREFIX = `<script setup lang="ts" data-oxlint-plugin-vize-scriptless="`;
79
+ const SCRIPTLESS_WORKAROUND_CLOSE_TAG = "<\/script>";
80
+ function hasScriptLikeBlock(source) {
81
+ return extractSfcBlocks(source).some((block) => block.kind === "script" || block.kind === "script-setup");
82
+ }
83
+ function appendScriptlessWorkaround(source, filename) {
84
+ return `${createWorkaroundScript(source, filename)}${source}`;
85
+ }
86
+ function resolveWorkaroundSource(source, fallbackFilename) {
87
+ if (!source.startsWith(SCRIPTLESS_WORKAROUND_OPEN_TAG_PREFIX)) return {
88
+ filename: fallbackFilename,
89
+ source,
90
+ usesOriginalLocations: false
91
+ };
92
+ const encodedFilenameStart = SCRIPTLESS_WORKAROUND_OPEN_TAG_PREFIX.length;
93
+ const encodedFilenameEnd = source.indexOf(`">`, encodedFilenameStart);
94
+ if (encodedFilenameEnd === -1) return {
95
+ filename: fallbackFilename,
96
+ source,
97
+ usesOriginalLocations: false
98
+ };
99
+ const closeTagStart = source.indexOf(SCRIPTLESS_WORKAROUND_CLOSE_TAG, encodedFilenameEnd + 2);
100
+ if (closeTagStart === -1) return {
101
+ filename: fallbackFilename,
102
+ source,
103
+ usesOriginalLocations: false
104
+ };
105
+ let strippedSourceStart = closeTagStart + 9;
106
+ if (source.charCodeAt(strippedSourceStart) === 13) strippedSourceStart += 1;
107
+ if (source.charCodeAt(strippedSourceStart) === 10) strippedSourceStart += 1;
108
+ return {
109
+ filename: decodeWorkaroundFilename(source.slice(encodedFilenameStart, encodedFilenameEnd)) ?? fallbackFilename,
110
+ source: source.slice(strippedSourceStart),
111
+ usesOriginalLocations: true
112
+ };
113
+ }
114
+ function createWorkaroundScript(source, filename) {
115
+ return `${SCRIPTLESS_WORKAROUND_OPEN_TAG_PREFIX}${encodeWorkaroundFilename(filename)}">${createWhitespaceMirror(source)}<\/script>\n`;
116
+ }
117
+ function createWhitespaceMirror(source) {
118
+ return source.replaceAll(/[^\r\n]/gu, " ");
119
+ }
120
+ function encodeWorkaroundFilename(filename) {
121
+ return Buffer.from(filename, "utf8").toString("base64url");
122
+ }
123
+ function decodeWorkaroundFilename(encoded) {
124
+ try {
125
+ return Buffer.from(encoded, "base64url").toString("utf8");
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+ if (import.meta.vitest) {
131
+ const { describe, expect, it } = import.meta.vitest;
132
+ describe("scriptless workaround helpers", () => {
133
+ it("detects script blocks", () => {
134
+ expect(hasScriptLikeBlock("<template><div /></template>")).toBe(false);
135
+ expect(hasScriptLikeBlock("<template><div /></template>\n<script setup>const x = 1<\/script>")).toBe(true);
136
+ });
137
+ it("appends and resolves the workaround payload", () => {
138
+ const source = "<template>\n <div>hello</div>\n</template>\n";
139
+ const filename = "/Users/example/Hello.vue";
140
+ const appended = appendScriptlessWorkaround(source, filename);
141
+ expect(appended).toMatch(/oxlint-plugin-vize-scriptless/u);
142
+ expect(resolveWorkaroundSource(appended, "/Users/example/fallback.vue")).toEqual({
143
+ filename,
144
+ source,
145
+ usesOriginalLocations: true
146
+ });
147
+ });
148
+ });
149
+ }
150
+ //#endregion
151
+ export { extractSfcBlocks as a, compareLineColumn as i, hasScriptLikeBlock as n, formatBlockLabel as o, resolveWorkaroundSource as r, getDiagnosticBlock as s, appendScriptlessWorkaround as t };
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "oxlint-plugin-vize",
3
+ "version": "0.33.0",
4
+ "description": "Oxlint JS plugin bridge for Vize Patina",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "bin": {
10
+ "oxlint-vize": "bin/oxlint-vize"
11
+ },
12
+ "main": "./dist/index.mjs",
13
+ "types": "./dist/index.d.mts",
14
+ "exports": {
15
+ ".": {
16
+ "import": "./dist/index.mjs",
17
+ "types": "./dist/index.d.mts"
18
+ }
19
+ },
20
+ "files": [
21
+ "bin",
22
+ "dist"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/ubugeeei/vize",
27
+ "directory": "npm/oxlint-plugin-vize"
28
+ },
29
+ "keywords": [
30
+ "oxlint",
31
+ "plugin",
32
+ "vue",
33
+ "vize",
34
+ "lint"
35
+ ],
36
+ "license": "MIT",
37
+ "engines": {
38
+ "node": ">=24"
39
+ },
40
+ "dependencies": {
41
+ "@oxlint/plugins": "^1.53.0"
42
+ },
43
+ "optionalDependencies": {
44
+ "@vizejs/native-darwin-arm64": "0.48.0",
45
+ "@vizejs/native-darwin-x64": "0.48.0",
46
+ "@vizejs/native-linux-arm64-gnu": "0.48.0",
47
+ "@vizejs/native-linux-arm64-musl": "0.48.0",
48
+ "@vizejs/native-linux-x64-gnu": "0.48.0",
49
+ "@vizejs/native-linux-x64-musl": "0.48.0",
50
+ "@vizejs/native-win32-arm64-msvc": "0.48.0",
51
+ "@vizejs/native-win32-x64-msvc": "0.48.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^22.0.0",
55
+ "typescript": "~5.6.0",
56
+ "vite-plus": "latest",
57
+ "@vizejs/native": "0.48.0"
58
+ },
59
+ "scripts": {
60
+ "build": "vp pack",
61
+ "test": "vp pack && node --experimental-strip-types src/test.ts",
62
+ "check": "vp check src vite.config.ts",
63
+ "check:fix": "vp check --fix src vite.config.ts",
64
+ "fmt": "vp fmt --write src vite.config.ts"
65
+ }
66
+ }