oxlint-plugin-vize 0.57.0 → 0.59.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/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +249 -0
- package/dist/index.d.mts +28 -0
- package/dist/index.mjs +484 -0
- package/dist/workaround-57-10EZz.mjs +151 -0
- package/package.json +10 -10
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 {};
|
package/dist/index.d.mts
ADDED
|
@@ -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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxlint-plugin-vize",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.59.0",
|
|
4
4
|
"description": "Oxlint JS plugin bridge for Vize Patina",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lint",
|
|
@@ -40,21 +40,21 @@
|
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@tsdown/css": "0.21.9",
|
|
42
42
|
"@types/node": "25.6.0",
|
|
43
|
-
"@vizejs/native": "0.
|
|
43
|
+
"@vizejs/native": "0.59.0",
|
|
44
44
|
"tsdown": "0.21.9",
|
|
45
45
|
"typescript": "6.0.3",
|
|
46
46
|
"vite": "npm:@voidzero-dev/vite-plus-core@0.1.19",
|
|
47
47
|
"vite-plus": "0.1.19"
|
|
48
48
|
},
|
|
49
49
|
"optionalDependencies": {
|
|
50
|
-
"@vizejs/native-darwin-arm64": "0.
|
|
51
|
-
"@vizejs/native-darwin-x64": "0.
|
|
52
|
-
"@vizejs/native-linux-arm64-gnu": "0.
|
|
53
|
-
"@vizejs/native-linux-arm64-musl": "0.
|
|
54
|
-
"@vizejs/native-linux-x64-gnu": "0.
|
|
55
|
-
"@vizejs/native-linux-x64-musl": "0.
|
|
56
|
-
"@vizejs/native-win32-arm64-msvc": "0.
|
|
57
|
-
"@vizejs/native-win32-x64-msvc": "0.
|
|
50
|
+
"@vizejs/native-darwin-arm64": "0.59.0",
|
|
51
|
+
"@vizejs/native-darwin-x64": "0.59.0",
|
|
52
|
+
"@vizejs/native-linux-arm64-gnu": "0.59.0",
|
|
53
|
+
"@vizejs/native-linux-arm64-musl": "0.59.0",
|
|
54
|
+
"@vizejs/native-linux-x64-gnu": "0.59.0",
|
|
55
|
+
"@vizejs/native-linux-x64-musl": "0.59.0",
|
|
56
|
+
"@vizejs/native-win32-arm64-msvc": "0.59.0",
|
|
57
|
+
"@vizejs/native-win32-x64-msvc": "0.59.0"
|
|
58
58
|
},
|
|
59
59
|
"engines": {
|
|
60
60
|
"node": ">=24"
|