@vizejs/nuxt 0.312.0 → 0.318.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.
@@ -0,0 +1,1201 @@
1
+ import { createRequire } from "node:module";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { glob, lstat, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
6
+ import { Worker, isMainThread, parentPort } from "node:worker_threads";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+ import { randomBytes, randomUUID } from "node:crypto";
9
+ import { buildNuxtLintPlan, collectNuxtLintDirs, resolveNuxtLintFeatures } from "@vizejs/nuxt-lint-config";
10
+ import { createServer } from "node:http";
11
+ //#region src/lint/checker/options.ts
12
+ /**
13
+ * Resolve the engine-neutral part of `@nuxt/eslint`'s checker contract.
14
+ *
15
+ * `configType` and `eslintPath` intentionally do not exist: they choose an
16
+ * ESLint implementation, while this checker always executes oxlint + Patina.
17
+ */
18
+ function resolveNuxtLintCheckerOptions(checker, project) {
19
+ if (checker !== true && (checker === false || checker == null)) return false;
20
+ const overrides = typeof checker === "object" ? checker : {};
21
+ return {
22
+ cache: overrides.cache ?? true,
23
+ include: [...overrides.include ?? [`${project.srcDir}/**/*.{js,jsx,ts,tsx,vue}`]],
24
+ exclude: [...overrides.exclude ?? ["**/node_modules/**", project.buildDir]],
25
+ formatter: overrides.formatter ?? "stylish",
26
+ lintOnStart: overrides.lintOnStart ?? true,
27
+ emitWarning: overrides.emitWarning ?? true,
28
+ emitError: overrides.emitError ?? true,
29
+ fix: overrides.fix ?? false
30
+ };
31
+ }
32
+ //#endregion
33
+ //#region src/lint/checker/filter.ts
34
+ const GLOB_TOKEN = /[*?[\]{}()]/u;
35
+ function normalize(value) {
36
+ const normalized = value.replaceAll("\\", "/").replace(/\/{2,}/gu, "/");
37
+ if (normalized === "/" || /^[A-Za-z]:\/$/u.test(normalized)) return normalized;
38
+ return normalized.replace(/\/$/u, "");
39
+ }
40
+ function relativeToRoot(file, rootDir) {
41
+ const root = normalize(rootDir);
42
+ const absolute = normalize(file);
43
+ const prefix = `${root}/`;
44
+ if (absolute.startsWith(prefix)) return absolute.slice(prefix.length);
45
+ const normalized = normalize(path.relative(rootDir, file));
46
+ return normalized === ".." || normalized.startsWith("../") ? void 0 : normalized;
47
+ }
48
+ function matchesDirectory(candidate, pattern) {
49
+ if (GLOB_TOKEN.test(pattern)) return false;
50
+ return candidate === pattern || candidate.startsWith(`${pattern}/`);
51
+ }
52
+ function matchesPattern(absolute, relative, rawPattern) {
53
+ const pattern = normalize(rawPattern);
54
+ const candidates = path.isAbsolute(pattern) || /^[A-Za-z]:\//u.test(pattern) ? [absolute] : relative === void 0 ? [] : [relative];
55
+ for (const candidate of candidates) if (matchesDirectory(candidate, pattern) || path.matchesGlob(candidate, pattern)) return true;
56
+ return false;
57
+ }
58
+ /** Whether one watcher path belongs to the checker include/exclude contract. */
59
+ function matchesNuxtLintCheckerFile(file, rootDir, options) {
60
+ const absolute = normalize(file);
61
+ const relative = relativeToRoot(file, rootDir);
62
+ if (options.exclude.some((pattern) => matchesPattern(absolute, relative, pattern))) return false;
63
+ return options.include.some((pattern) => matchesPattern(absolute, relative, pattern));
64
+ }
65
+ //#endregion
66
+ //#region src/lint/checker/worker.ts
67
+ function resolveOxlintEntrypoint(cwd) {
68
+ try {
69
+ const manifest = createRequire(path.join(cwd, "package.json")).resolve("oxlint/package.json");
70
+ return path.join(path.dirname(manifest), "bin", "oxlint");
71
+ } catch (error) {
72
+ throw new Error(`Unable to resolve oxlint from ${cwd}. Install oxlint before enabling the Nuxt lint checker.`, { cause: error });
73
+ }
74
+ }
75
+ function projectPattern(pattern, cwd) {
76
+ if (!path.isAbsolute(pattern)) return pattern;
77
+ const relative = path.relative(cwd, pattern);
78
+ if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`)) return pattern;
79
+ return relative.split(path.sep).join("/") || ".";
80
+ }
81
+ function expandBraces(pattern) {
82
+ const match = /\{([^{}]+)\}/u.exec(pattern);
83
+ if (!match || match.index === void 0) return [pattern];
84
+ const before = pattern.slice(0, match.index);
85
+ const after = pattern.slice(match.index + match[0].length);
86
+ return match[1].split(",").flatMap((part) => expandBraces(`${before}${part}${after}`));
87
+ }
88
+ function oxlintPatterns(pattern, cwd) {
89
+ const patterns = expandBraces(projectPattern(pattern, cwd)).flatMap((expanded) => {
90
+ const shallow = expanded.replace(/(^|\/)\*\*\//gu, "$1");
91
+ return shallow === expanded ? [expanded] : [expanded, shallow];
92
+ });
93
+ return [...new Set(patterns)];
94
+ }
95
+ async function oxlintTargets(pattern, cwd) {
96
+ const rebased = projectPattern(pattern, cwd);
97
+ if (rebased.endsWith("/**/*.{js,jsx,ts,tsx,vue}")) return [rebased.slice(0, -25) || "."];
98
+ if (!/[*?[\]{}()]/u.test(rebased)) return [rebased];
99
+ const matches = await Array.fromAsync(glob(rebased, { cwd }));
100
+ return matches.length > 0 ? matches : oxlintPatterns(pattern, cwd);
101
+ }
102
+ async function checkerArgs(task) {
103
+ const args = [
104
+ "--config",
105
+ task.configFile,
106
+ "--format",
107
+ "json",
108
+ "--no-error-on-unmatched-pattern"
109
+ ];
110
+ if (task.fix) args.push("--fix");
111
+ for (const pattern of task.exclude) for (const exclude of oxlintPatterns(pattern, task.cwd)) args.push("--ignore-pattern", exclude);
112
+ for (const target of task.targets) args.push(...await oxlintTargets(target, task.cwd));
113
+ return args;
114
+ }
115
+ function spawnOxlint(node, entrypoint, args, cwd) {
116
+ return new Promise((resolve, reject) => {
117
+ const child = spawn(node, [entrypoint, ...args], {
118
+ cwd,
119
+ stdio: [
120
+ "ignore",
121
+ "pipe",
122
+ "pipe"
123
+ ]
124
+ });
125
+ const stdout = [];
126
+ const stderr = [];
127
+ child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
128
+ child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
129
+ child.on("error", (error) => {
130
+ reject(new Error(`Failed to start oxlint at ${entrypoint}: ${error.message}`, { cause: error }));
131
+ });
132
+ child.on("close", (status) => {
133
+ const output = Buffer.concat(stdout).toString("utf8");
134
+ if (output.trim()) {
135
+ resolve(output);
136
+ return;
137
+ }
138
+ const details = Buffer.concat(stderr).toString("utf8").trim();
139
+ if (status === 0) {
140
+ resolve("{\"diagnostics\":[]}\n");
141
+ return;
142
+ }
143
+ reject(/* @__PURE__ */ new Error(`oxlint exited with ${String(status)} without JSON output${details ? `: ${details}` : ""}`));
144
+ });
145
+ });
146
+ }
147
+ async function executeOxlint(entrypoint, args, cwd) {
148
+ if (!existsSync(entrypoint)) throw new Error(`Failed to start oxlint: entrypoint does not exist: ${entrypoint}`);
149
+ const node = existsSync(process.execPath) ? process.execPath : "node";
150
+ try {
151
+ return await spawnOxlint(node, entrypoint, args, cwd);
152
+ } catch (error) {
153
+ const cause = error.cause;
154
+ if (node !== "node" && cause?.code === "ENOENT") return spawnOxlint("node", entrypoint, args, cwd);
155
+ throw error;
156
+ }
157
+ }
158
+ function diagnosticKind(diagnostic) {
159
+ if (diagnostic.severity === 2 || diagnostic.severity === "error") return "error";
160
+ if (diagnostic.severity === 1 || diagnostic.severity === "warn" || diagnostic.severity === "warning") return "warning";
161
+ }
162
+ function readableDiagnostics(diagnostics, formatter) {
163
+ const unix = formatter === "unix";
164
+ const lines = diagnostics.map((diagnostic) => {
165
+ const span = diagnostic.labels?.find((label) => label.span)?.span;
166
+ const file = diagnostic.filename ?? diagnostic.filePath ?? "<unknown>";
167
+ const line = span?.line ?? 1;
168
+ const column = span?.column ?? 1;
169
+ const severity = diagnosticKind(diagnostic) ?? "warning";
170
+ const message = diagnostic.message ?? "lint diagnostic";
171
+ const code = diagnostic.code ? ` (${diagnostic.code})` : "";
172
+ return unix ? `${file}:${line}:${column}: ${message} [${severity}${code}]` : `${file}:${line}:${column} ${severity} ${message}${code}`;
173
+ });
174
+ return lines.length > 0 ? `${lines.join("\n")}\n` : "";
175
+ }
176
+ function resultFromPayload(payload, task) {
177
+ const diagnostics = payload.diagnostics.filter((diagnostic) => {
178
+ const kind = diagnosticKind(diagnostic);
179
+ return kind === "error" && task.emitError || kind === "warning" && task.emitWarning;
180
+ });
181
+ const hasErrors = diagnostics.some((diagnostic) => diagnosticKind(diagnostic) === "error");
182
+ const hasWarnings = diagnostics.some((diagnostic) => diagnosticKind(diagnostic) === "warning");
183
+ const output = task.formatter === "json" ? diagnostics.length > 0 ? `${JSON.stringify({
184
+ ...payload,
185
+ diagnostics
186
+ })}\n` : "" : readableDiagnostics(diagnostics, task.formatter);
187
+ return {
188
+ diagnosticCount: diagnostics.length,
189
+ hasErrors,
190
+ hasWarnings,
191
+ output
192
+ };
193
+ }
194
+ /** Run one checker pass. This function executes inside the long-lived worker. */
195
+ async function runNuxtLintCheckerTask(task) {
196
+ if (!task.emitError && !task.emitWarning && !task.fix) return {
197
+ diagnosticCount: 0,
198
+ hasErrors: false,
199
+ hasWarnings: false,
200
+ output: ""
201
+ };
202
+ const stdout = await executeOxlint(task.oxlintEntrypoint ?? resolveOxlintEntrypoint(task.cwd), await checkerArgs(task), task.cwd);
203
+ let payload;
204
+ try {
205
+ payload = JSON.parse(stdout);
206
+ } catch (error) {
207
+ throw new Error(`oxlint returned invalid JSON: ${stdout.slice(0, 240)}`, { cause: error });
208
+ }
209
+ if (!Array.isArray(payload.diagnostics)) throw new Error("oxlint JSON has no diagnostics array");
210
+ return resultFromPayload(payload, task);
211
+ }
212
+ function serializeError(error) {
213
+ if (error instanceof Error) return {
214
+ message: error.message,
215
+ stack: error.stack
216
+ };
217
+ return { message: String(error) };
218
+ }
219
+ if (!isMainThread && parentPort) parentPort.on("message", async ({ id, task }) => {
220
+ try {
221
+ parentPort?.postMessage({
222
+ id,
223
+ result: await runNuxtLintCheckerTask(task)
224
+ });
225
+ } catch (error) {
226
+ parentPort?.postMessage({
227
+ error: serializeError(error),
228
+ id
229
+ });
230
+ }
231
+ });
232
+ /** Persistent worker-thread client used by the Vite dev plugin. */
233
+ var NuxtLintCheckerWorker = class {
234
+ worker = new Worker(new URL(import.meta.url));
235
+ pending = /* @__PURE__ */ new Map();
236
+ nextId = 0;
237
+ closed = false;
238
+ constructor() {
239
+ this.worker.on("message", (response) => {
240
+ const pending = this.pending.get(response.id);
241
+ if (!pending) return;
242
+ this.pending.delete(response.id);
243
+ if ("error" in response) {
244
+ const error = new Error(response.error.message);
245
+ error.stack = response.error.stack ?? error.stack;
246
+ pending.reject(error);
247
+ } else pending.resolve(response.result);
248
+ });
249
+ this.worker.on("error", (error) => this.rejectAll(error));
250
+ this.worker.on("exit", (code) => {
251
+ if (!this.closed) this.rejectAll(/* @__PURE__ */ new Error(`Nuxt lint worker exited with ${code}`));
252
+ });
253
+ }
254
+ run(task) {
255
+ if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("Nuxt lint worker is closed"));
256
+ const id = ++this.nextId;
257
+ return new Promise((resolve, reject) => {
258
+ this.pending.set(id, {
259
+ reject,
260
+ resolve
261
+ });
262
+ this.worker.postMessage({
263
+ id,
264
+ task
265
+ });
266
+ });
267
+ }
268
+ async close() {
269
+ if (this.closed) return;
270
+ this.closed = true;
271
+ this.rejectAll(/* @__PURE__ */ new Error("Nuxt lint worker closed"));
272
+ await this.worker.terminate();
273
+ }
274
+ rejectAll(error) {
275
+ for (const pending of this.pending.values()) pending.reject(error);
276
+ this.pending.clear();
277
+ }
278
+ };
279
+ //#endregion
280
+ //#region src/lint/checker/vite.ts
281
+ const PLUGIN_NAME$1 = "vize:nuxt-lint-checker";
282
+ function overlayError(output) {
283
+ return {
284
+ type: "error",
285
+ err: {
286
+ message: "Vize lint checker found diagnostics",
287
+ stack: output,
288
+ plugin: PLUGIN_NAME$1
289
+ }
290
+ };
291
+ }
292
+ var NuxtLintChangeCollector = class {
293
+ pendingFiles = /* @__PURE__ */ new Set();
294
+ pendingFull = false;
295
+ draining = false;
296
+ closed = false;
297
+ overlayVisible = false;
298
+ config;
299
+ runner;
300
+ server;
301
+ oxlintEntrypoint;
302
+ constructor(config, runner, server, oxlintEntrypoint) {
303
+ this.config = config;
304
+ this.runner = runner;
305
+ this.server = server;
306
+ this.oxlintEntrypoint = oxlintEntrypoint;
307
+ }
308
+ full() {
309
+ if (this.closed) return;
310
+ this.pendingFull = true;
311
+ this.pendingFiles.clear();
312
+ this.schedule();
313
+ }
314
+ file(file) {
315
+ if (this.closed) return;
316
+ if (path.resolve(file) === path.resolve(this.config.configFile)) {
317
+ this.full();
318
+ return;
319
+ }
320
+ if (!matchesNuxtLintCheckerFile(file, this.config.rootDir, this.config.options)) return;
321
+ if (!this.config.options.cache) {
322
+ this.full();
323
+ return;
324
+ }
325
+ if (!this.pendingFull) this.pendingFiles.add(path.resolve(file));
326
+ this.schedule();
327
+ }
328
+ async close() {
329
+ if (this.closed) return;
330
+ this.closed = true;
331
+ this.pendingFiles.clear();
332
+ await this.runner.close();
333
+ }
334
+ schedule() {
335
+ if (this.draining) return;
336
+ this.draining = true;
337
+ queueMicrotask(() => void this.drain());
338
+ }
339
+ async drain() {
340
+ while (!this.closed && (this.pendingFull || this.pendingFiles.size > 0)) {
341
+ const targets = this.pendingFull ? [...this.config.options.include] : [...this.pendingFiles].sort();
342
+ this.pendingFull = false;
343
+ this.pendingFiles.clear();
344
+ try {
345
+ const result = await this.runner.run(this.task(targets));
346
+ if (!this.closed) this.report(result);
347
+ } catch (error) {
348
+ if (!this.closed) this.reportFailure(error);
349
+ }
350
+ }
351
+ this.draining = false;
352
+ if (!this.closed && (this.pendingFull || this.pendingFiles.size > 0)) this.schedule();
353
+ }
354
+ task(targets) {
355
+ const options = this.config.options;
356
+ return {
357
+ configFile: this.config.configFile,
358
+ cwd: this.config.rootDir,
359
+ emitError: options.emitError,
360
+ emitWarning: options.emitWarning,
361
+ exclude: [...options.exclude],
362
+ fix: options.fix,
363
+ formatter: options.formatter,
364
+ oxlintEntrypoint: this.oxlintEntrypoint,
365
+ targets
366
+ };
367
+ }
368
+ report(result) {
369
+ if (result.output) {
370
+ if (result.hasErrors) this.server.config.logger.error(result.output);
371
+ else if (result.hasWarnings) this.server.config.logger.warn(result.output);
372
+ }
373
+ if (result.diagnosticCount > 0) {
374
+ this.server.ws.send(overlayError(result.output));
375
+ this.overlayVisible = true;
376
+ } else if (this.overlayVisible) {
377
+ this.server.ws.send({
378
+ type: "update",
379
+ updates: []
380
+ });
381
+ this.overlayVisible = false;
382
+ }
383
+ }
384
+ reportFailure(error) {
385
+ const message = `Nuxt lint checker failed: ${error instanceof Error ? error.message : String(error)}`;
386
+ this.server.config.logger.error(message);
387
+ this.server.ws.send(overlayError(message));
388
+ this.overlayVisible = true;
389
+ }
390
+ };
391
+ /** Create the client-side Vite dev plugin; it has no transform/render hooks. */
392
+ function createNuxtLintCheckerVitePlugin(config, dependencies = {}) {
393
+ return {
394
+ name: PLUGIN_NAME$1,
395
+ apply: "serve",
396
+ enforce: "post",
397
+ configureServer(server) {
398
+ const collector = new NuxtLintChangeCollector(config, dependencies.createRunner?.() ?? new NuxtLintCheckerWorker(), server, dependencies.oxlintEntrypoint);
399
+ server.watcher.add(config.configFile);
400
+ const changed = (file) => collector.file(file);
401
+ server.watcher.on("add", changed);
402
+ server.watcher.on("change", changed);
403
+ server.watcher.on("unlink", changed);
404
+ server.httpServer?.once("close", () => void collector.close());
405
+ if (config.options.lintOnStart) collector.full();
406
+ }
407
+ };
408
+ }
409
+ //#endregion
410
+ //#region src/lint/checker/webpack.ts
411
+ const PLUGIN_NAME = "VizeNuxtLintChecker";
412
+ function checkerError(message) {
413
+ const error = new Error(message);
414
+ error.name = PLUGIN_NAME;
415
+ return error;
416
+ }
417
+ function reportResult(compilation, result) {
418
+ if (result.hasErrors) compilation.errors.push(checkerError(result.output));
419
+ else if (result.hasWarnings) compilation.warnings.push(checkerError(result.output));
420
+ }
421
+ function task(config, targets, oxlintEntrypoint) {
422
+ const options = config.options;
423
+ return {
424
+ configFile: config.configFile,
425
+ cwd: config.rootDir,
426
+ emitError: options.emitError,
427
+ emitWarning: options.emitWarning,
428
+ exclude: [...options.exclude],
429
+ fix: options.fix,
430
+ formatter: options.formatter,
431
+ oxlintEntrypoint,
432
+ targets
433
+ };
434
+ }
435
+ function changedTargets(compiler, config, initial) {
436
+ const modified = compiler.modifiedFiles;
437
+ if (initial && config.options.lintOnStart) return [...config.options.include];
438
+ if (!modified || modified.size === 0) return void 0;
439
+ if (!config.options.cache) return [...config.options.include];
440
+ const configFile = path.resolve(config.configFile);
441
+ if ([...modified].some((file) => path.resolve(file) === configFile)) return [...config.options.include];
442
+ const files = [...modified].filter((file) => matchesNuxtLintCheckerFile(file, config.rootDir, config.options)).map((file) => path.resolve(file)).sort();
443
+ return files.length > 0 ? files : void 0;
444
+ }
445
+ /**
446
+ * Create Nuxt 2's webpack adapter.
447
+ *
448
+ * The worker starts at `watchRun`, concurrently with compilation; only the
449
+ * final diagnostic handoff happens at `afterCompile`, where webpack can feed
450
+ * the result to its existing terminal and browser overlays.
451
+ */
452
+ function createNuxtLintCheckerWebpackPlugin(config, dependencies = {}) {
453
+ return { apply(compiler) {
454
+ const runner = dependencies.createRunner?.() ?? new NuxtLintCheckerWorker();
455
+ let initial = true;
456
+ let pending;
457
+ compiler.hooks.watchRun.tap(PLUGIN_NAME, (nextCompiler) => {
458
+ const targets = changedTargets(nextCompiler, config, initial);
459
+ initial = false;
460
+ pending = targets ? runner.run(task(config, targets, dependencies.oxlintEntrypoint)) : void 0;
461
+ });
462
+ compiler.hooks.afterCompile.tapPromise(PLUGIN_NAME, async (compilation) => {
463
+ compilation.fileDependencies.add(config.configFile);
464
+ if (!pending) return;
465
+ const current = pending;
466
+ pending = void 0;
467
+ try {
468
+ reportResult(compilation, await current);
469
+ } catch (error) {
470
+ compilation.errors.push(checkerError(`Nuxt lint checker failed: ${error instanceof Error ? error.message : String(error)}`));
471
+ }
472
+ });
473
+ compiler.hooks.watchClose.tap(PLUGIN_NAME, () => void runner.close());
474
+ } };
475
+ }
476
+ //#endregion
477
+ //#region src/lint/checker/setup.ts
478
+ async function addVitePlugin(plugin) {
479
+ (await import("@nuxt/kit")).addVitePlugin(plugin, { server: false });
480
+ }
481
+ async function addWebpackPlugin(plugin) {
482
+ (await import("@nuxt/kit")).addWebpackPlugin(plugin, { server: false });
483
+ }
484
+ function builderKind(builder) {
485
+ if (typeof builder !== "string") return "unsupported";
486
+ if (builder === "vite" || builder.includes("vite-builder")) return "vite";
487
+ if (builder === "webpack" || builder.includes("webpack-builder")) return "webpack";
488
+ return "unsupported";
489
+ }
490
+ /** Register the dev-only adapter over Phase 3's generated config artifact. */
491
+ async function setupNuxtLintChecker(checker, nuxt, generation, dependencies = {}) {
492
+ if (checker !== true && (checker === false || checker == null)) return void 0;
493
+ if (nuxt.options.dev !== true) return void 0;
494
+ if (!generation) throw new Error("The Nuxt lint checker requires lint config generation; enable `vize.lint`.");
495
+ const options = resolveNuxtLintCheckerOptions(checker, {
496
+ buildDir: nuxt.options.buildDir,
497
+ srcDir: nuxt.options.srcDir ?? nuxt.options.rootDir
498
+ });
499
+ if (options === false) return void 0;
500
+ const rootDir = path.resolve(nuxt.options.rootDir);
501
+ const config = {
502
+ configFile: generation.configFile,
503
+ options,
504
+ rootDir
505
+ };
506
+ const builder = builderKind(nuxt.options.builder);
507
+ if (builder === "vite") await (dependencies.addVitePlugin ?? addVitePlugin)(createNuxtLintCheckerVitePlugin(config));
508
+ else if (builder === "webpack") await (dependencies.addWebpackPlugin ?? addWebpackPlugin)(createNuxtLintCheckerWebpackPlugin(config));
509
+ else {
510
+ const label = typeof nuxt.options.builder === "string" ? nuxt.options.builder : "unknown";
511
+ (dependencies.warn ?? console.warn)(`Unsupported Nuxt builder ${label}; Vize lint checker is disabled.`);
512
+ }
513
+ return {
514
+ builder,
515
+ configFile: generation.configFile,
516
+ options
517
+ };
518
+ }
519
+ //#endregion
520
+ //#region src/lint/addons.ts
521
+ /** Hook other Nuxt modules use to extend Vize's generated lint config. */
522
+ const VIZE_NUXT_LINT_CONFIG_ADDONS_HOOK = "vize:lint:config:addons";
523
+ function asImportContext(value) {
524
+ if (value == null || typeof value !== "object" || !("getImports" in value)) return;
525
+ return typeof value.getImports === "function" ? value : void 0;
526
+ }
527
+ function nitroImportContext(value) {
528
+ if (value == null || typeof value !== "object" || !("unimport" in value)) return;
529
+ return asImportContext(value.unimport);
530
+ }
531
+ /**
532
+ * Capture Nuxt's client and server auto-import registries as readonly globals.
533
+ *
534
+ * Nuxt publishes both contexts after modules have begun setting up. Keeping
535
+ * their latest values in the addon means every config regeneration observes
536
+ * the current registry rather than a module-setup-time snapshot.
537
+ */
538
+ function createNuxtImportGlobalsAddon(nuxt) {
539
+ let unimport;
540
+ let nitroUnimport;
541
+ nuxt.hook("imports:context", (context) => {
542
+ unimport = asImportContext(context);
543
+ });
544
+ nuxt.hook("nitro:init", (nitro) => {
545
+ nitroUnimport = nitroImportContext(nitro);
546
+ });
547
+ return {
548
+ name: "vize:lint:import-globals",
549
+ async getConfigs() {
550
+ const imports = [...await unimport?.getImports() ?? [], ...await nitroUnimport?.getImports() ?? []].sort((left, right) => left.from.localeCompare(right.from) || left.name.localeCompare(right.name));
551
+ return [{
552
+ name: "nuxt/import-globals",
553
+ globals: Object.fromEntries(imports.map((imported) => [imported.as ?? imported.name, "readonly"]))
554
+ }];
555
+ }
556
+ };
557
+ }
558
+ /**
559
+ * Register the built-in auto-import addon and return the generation-time
560
+ * resolver consumed by the Nuxt lint config writer.
561
+ *
562
+ * The addon array is rebuilt for every generation. This mirrors Nuxt's addon
563
+ * hook without retaining contributions across `builder:generateApp` runs.
564
+ * The Vize-namespaced hook carries engine-neutral config items instead of raw
565
+ * ESLint source, so contributors remain compatible with the oxlint emitter.
566
+ */
567
+ function setupNuxtLintConfigAddons(nuxt) {
568
+ const defaults = [createNuxtImportGlobalsAddon(nuxt)];
569
+ return async () => {
570
+ const addons = [...defaults];
571
+ await nuxt.callHook(VIZE_NUXT_LINT_CONFIG_ADDONS_HOOK, addons);
572
+ const configs = [];
573
+ for (const addon of addons) {
574
+ const contributed = await addon.getConfigs();
575
+ if (contributed) configs.push(...contributed);
576
+ }
577
+ return configs;
578
+ };
579
+ }
580
+ //#endregion
581
+ //#region src/lint/emitter.ts
582
+ const PATINA_RULE_NAMESPACES = new Set([
583
+ "a11y",
584
+ "css",
585
+ "ecosystem",
586
+ "html",
587
+ "musea",
588
+ "nuxt",
589
+ "petite-vue",
590
+ "script",
591
+ "ssr",
592
+ "type",
593
+ "vapor",
594
+ "vize",
595
+ "vue"
596
+ ]);
597
+ function toOxlintRuleId(ruleId) {
598
+ const separator = ruleId.indexOf("/");
599
+ const namespace = separator < 0 ? ruleId : ruleId.slice(0, separator);
600
+ return PATINA_RULE_NAMESPACES.has(namespace) ? `vize/${ruleId}` : ruleId;
601
+ }
602
+ function prefixVizeRules(rules) {
603
+ if (!rules) return void 0;
604
+ return Object.fromEntries(Object.entries(rules).map(([ruleId, severity]) => [toOxlintRuleId(ruleId), severity]));
605
+ }
606
+ function mergeGlobals(target, source) {
607
+ if (!source) return;
608
+ for (const [name, access] of Object.entries(source)) Object.defineProperty(target, name, {
609
+ configurable: true,
610
+ enumerable: true,
611
+ value: access,
612
+ writable: true
613
+ });
614
+ }
615
+ function mergeRules(target, source) {
616
+ Object.assign(target, prefixVizeRules(source));
617
+ }
618
+ /** Render the complete generated oxlint config, including its trailing newline. */
619
+ function renderNuxtOxlintConfig(items, pluginSpecifier) {
620
+ const ignorePatterns = [];
621
+ const globals = {};
622
+ const rules = {};
623
+ const overrides = [];
624
+ for (const item of items) {
625
+ if (item.files) {
626
+ const override = { files: [...item.files] };
627
+ if (item.ignores) override.excludeFiles = [...item.ignores];
628
+ if (item.globals) override.globals = { ...item.globals };
629
+ if (item.rules) override.rules = prefixVizeRules(item.rules);
630
+ overrides.push(override);
631
+ continue;
632
+ }
633
+ if (item.ignores) ignorePatterns.push(...item.ignores);
634
+ mergeGlobals(globals, item.globals);
635
+ mergeRules(rules, item.rules);
636
+ }
637
+ const config = {
638
+ plugins: ["vue"],
639
+ jsPlugins: [{
640
+ name: "vize",
641
+ specifier: pluginSpecifier
642
+ }],
643
+ settings: { vize: { preset: "incremental" } }
644
+ };
645
+ if (ignorePatterns.length > 0) config.ignorePatterns = ignorePatterns;
646
+ if (Object.keys(globals).length > 0) config.globals = globals;
647
+ if (Object.keys(rules).length > 0) config.rules = rules;
648
+ if (overrides.length > 0) config.overrides = overrides;
649
+ return `${JSON.stringify(config, null, 2)}\n`;
650
+ }
651
+ //#endregion
652
+ //#region src/lint/nuxt-state.ts
653
+ /**
654
+ * Reduce a Nuxt instance to the project state lint config generation needs.
655
+ *
656
+ * Nuxt 2 has no `_layers`, so the project is treated as a single layer rooted
657
+ * at `srcDir` (falling back to `rootDir`). That keeps the Nuxt 2, 3 and 4 paths
658
+ * on one code path rather than branching on the detected major version.
659
+ */
660
+ function toNuxtLintProjectState(options, overrides = {}) {
661
+ const rootDir = overrides.rootDir || options.rootDir;
662
+ const layers = (options._layers ?? []).map((layer) => layer.config).filter((config) => Boolean(config)).map((config) => ({
663
+ ...config,
664
+ srcDir: config.srcDir || options.srcDir || options.rootDir
665
+ }));
666
+ return {
667
+ rootDir,
668
+ dir: options.dir ?? {},
669
+ layers: layers.length > 0 ? layers : [{ srcDir: options.srcDir || options.rootDir }]
670
+ };
671
+ }
672
+ //#endregion
673
+ //#region src/lint/inspector-view.ts
674
+ /** Render the dependency-free UI served by the in-process lint-plan inspector. */
675
+ function renderNuxtLintInspectorHtml(nonce) {
676
+ return `<!doctype html>
677
+ <html lang="en">
678
+ <head>
679
+ <meta charset="utf-8">
680
+ <meta name="viewport" content="width=device-width,initial-scale=1">
681
+ <title>Vize Nuxt Lint Plan</title>
682
+ <style nonce="${nonce}">
683
+ :root { color-scheme: light dark; font: 14px/1.5 ui-sans-serif, system-ui, sans-serif; }
684
+ * { box-sizing: border-box; }
685
+ body { margin: 0; background: #0d1117; color: #e6edf3; }
686
+ header { position: sticky; top: 0; z-index: 1; padding: 16px 20px; background: #161b22; border-bottom: 1px solid #30363d; }
687
+ h1 { margin: 0 0 12px; font-size: 18px; }
688
+ h2 { margin: 24px 0 10px; font-size: 16px; }
689
+ form { display: flex; gap: 8px; flex-wrap: wrap; }
690
+ input { flex: 1 1 320px; min-width: 180px; padding: 8px 10px; color: inherit; background: #0d1117; border: 1px solid #484f58; border-radius: 6px; }
691
+ button { padding: 8px 12px; color: #fff; background: #238636; border: 0; border-radius: 6px; cursor: pointer; }
692
+ button.secondary { background: #30363d; }
693
+ button:disabled { opacity: .6; cursor: wait; }
694
+ main { max-width: 1100px; margin: 0 auto; padding: 0 20px 32px; }
695
+ #status { min-height: 22px; margin-top: 8px; color: #8c959f; }
696
+ #status.error { color: #ff7b72; }
697
+ .card { margin: 8px 0; padding: 12px; background: #161b22; border: 1px solid #30363d; border-radius: 8px; }
698
+ .meta { color: #8c959f; overflow-wrap: anywhere; }
699
+ .badge { display: inline-block; margin: 2px 4px 2px 0; padding: 1px 6px; border-radius: 10px; background: #30363d; }
700
+ .severity-error { color: #ff7b72; } .severity-warn { color: #d29922; } .severity-off { color: #8c959f; }
701
+ table { width: 100%; border-collapse: collapse; }
702
+ th, td { padding: 7px 8px; text-align: left; border-bottom: 1px solid #30363d; overflow-wrap: anywhere; }
703
+ th { color: #8c959f; font-weight: 600; }
704
+ code { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }
705
+ </style>
706
+ </head>
707
+ <body>
708
+ <header>
709
+ <h1>Vize Nuxt Lint Plan</h1>
710
+ <form id="inspect-form">
711
+ <input id="file" name="file" autocomplete="off" placeholder="app/pages/index.vue" aria-label="Project-relative file">
712
+ <button type="submit">Inspect file</button>
713
+ <button id="refresh" class="secondary" type="button">Refresh plan</button>
714
+ </form>
715
+ <div id="status" role="status" aria-live="polite"></div>
716
+ </header>
717
+ <main>
718
+ <section><h2>Effective rules</h2><div id="effective"></div></section>
719
+ <section><h2>Ordered config items</h2><div id="items"></div></section>
720
+ </main>
721
+ <script nonce="${nonce}">
722
+ const fileInput = document.getElementById("file");
723
+ const form = document.getElementById("inspect-form");
724
+ const refresh = document.getElementById("refresh");
725
+ const status = document.getElementById("status");
726
+ const effective = document.getElementById("effective");
727
+ const items = document.getElementById("items");
728
+ const buttons = Array.from(document.querySelectorAll("button"));
729
+
730
+ function node(tag, text, className) {
731
+ const value = document.createElement(tag);
732
+ if (text !== undefined) value.textContent = String(text);
733
+ if (className) value.className = className;
734
+ return value;
735
+ }
736
+
737
+ function badges(values) {
738
+ const wrapper = node("div");
739
+ for (const value of values || []) wrapper.append(node("span", value, "badge"));
740
+ return wrapper;
741
+ }
742
+
743
+ function renderItems(payload) {
744
+ items.replaceChildren();
745
+ for (const item of payload.items || []) {
746
+ const card = node("article", undefined, "card");
747
+ card.append(node("strong", item.name || "unnamed"));
748
+ if (item.globalIgnore) card.append(node("span", " global ignore", "meta"));
749
+ if (item.basePath) card.append(node("div", "base: " + item.basePath, "meta"));
750
+ if (item.files) card.append(node("div", "files", "meta"), badges(item.files));
751
+ if (item.ignores && item.ignores.length) card.append(node("div", "ignores", "meta"), badges(item.ignores));
752
+ const rules = Object.entries(item.rules || {});
753
+ if (rules.length) card.append(badges(rules.map(([name, severity]) => name + ": " + severity)));
754
+ items.append(card);
755
+ }
756
+ }
757
+
758
+ function renderEffective(payload) {
759
+ effective.replaceChildren();
760
+ const file = payload.files && payload.files[0];
761
+ if (!file) {
762
+ effective.append(node("p", "Enter a project-relative file to explain its effective rules.", "meta"));
763
+ return;
764
+ }
765
+ const summary = node("div", undefined, "card");
766
+ summary.append(node("strong", file.path));
767
+ if (file.ignored) summary.append(node("div", "Ignored by: " + (file.ignoredBy || []).join(", "), "severity-warn"));
768
+ else summary.append(node("div", "Matched: " + (file.matchedItems || []).join(" → "), "meta"));
769
+ effective.append(summary);
770
+ if (!file.rules || !file.rules.length) return;
771
+ const table = node("table");
772
+ const head = node("tr");
773
+ for (const label of ["Rule", "Severity", "Set by"]) head.append(node("th", label));
774
+ const thead = node("thead"); thead.append(head); table.append(thead);
775
+ const tbody = node("tbody");
776
+ for (const rule of file.rules) {
777
+ const row = node("tr");
778
+ row.append(node("td", rule.name), node("td", rule.severity, "severity-" + rule.severity), node("td", rule.setBy));
779
+ tbody.append(row);
780
+ }
781
+ table.append(tbody); effective.append(table);
782
+ }
783
+
784
+ async function load(fresh) {
785
+ const url = new URL("api", location.href);
786
+ const file = fileInput.value.trim();
787
+ if (file) url.searchParams.set("file", file);
788
+ if (fresh) url.searchParams.set("fresh", "1");
789
+ buttons.forEach(button => { button.disabled = true; });
790
+ status.className = ""; status.textContent = fresh ? "Refreshing…" : "Loading…";
791
+ try {
792
+ const response = await fetch(url, { headers: { accept: "application/json" } });
793
+ const payload = await response.json();
794
+ if (!response.ok) throw new Error(payload.error || "Inspector request failed");
795
+ renderEffective(payload); renderItems(payload);
796
+ status.textContent = "Resolved " + (payload.items || []).length + " config items";
797
+ } catch (error) {
798
+ status.className = "error";
799
+ status.textContent = error instanceof Error ? error.message : String(error);
800
+ } finally {
801
+ buttons.forEach(button => { button.disabled = false; });
802
+ }
803
+ }
804
+
805
+ form.addEventListener("submit", event => { event.preventDefault(); void load(false); });
806
+ refresh.addEventListener("click", () => { void load(true); });
807
+ void load(false);
808
+ <\/script>
809
+ </body>
810
+ </html>`;
811
+ }
812
+ //#endregion
813
+ //#region src/lint/inspector-devtools.ts
814
+ const MAX_URL_BYTES = 8 * 1024;
815
+ const MAX_FILES = 128;
816
+ const MAX_FILE_BYTES = 4 * 1024;
817
+ const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
818
+ function resolveNuxtLintDevtoolsOptions(options) {
819
+ const enabled = options?.enabled ?? "lazy";
820
+ const port = options?.port;
821
+ if (port !== void 0 && (!Number.isInteger(port) || port < 1 || port > 65535)) throw new RangeError("Nuxt lint inspector port must be an integer from 1 to 65535");
822
+ return {
823
+ enabled,
824
+ port
825
+ };
826
+ }
827
+ async function setupNuxtLintDevtools(options, nuxt, provider) {
828
+ const resolved = resolveNuxtLintDevtoolsOptions(options);
829
+ if (resolved.enabled === false) return void 0;
830
+ const controller = createController(resolved.port, provider, nuxt);
831
+ nuxt.hook("devtools:customTabs", (...args) => {
832
+ args[0].push(controller.tab());
833
+ });
834
+ nuxt.hook("close", () => controller.close());
835
+ if (resolved.enabled === true) try {
836
+ await controller.start();
837
+ } catch (error) {
838
+ console.warn(`[vize] Nuxt lint inspector failed to start: ${error instanceof Error ? error.message : String(error)}`);
839
+ }
840
+ return controller;
841
+ }
842
+ function createController(requestedPort, provider, nuxt) {
843
+ const token = randomBytes(18).toString("base64url");
844
+ const nonce = randomBytes(18).toString("base64url");
845
+ const server = createServer((request, response) => {
846
+ handleRequest(request, response, provider, token, nonce, server);
847
+ });
848
+ let closed = false;
849
+ let startPromise;
850
+ let viewerUrl;
851
+ const start = async () => {
852
+ if (closed) throw new Error("Nuxt lint inspector is closed");
853
+ if (viewerUrl) return;
854
+ startPromise ||= listen(server, requestedPort).then(async (port) => {
855
+ viewerUrl = `http://127.0.0.1:${port}/${token}/`;
856
+ await nuxt.callHook?.("devtools:customTabs:refresh");
857
+ });
858
+ try {
859
+ await startPromise;
860
+ } catch (error) {
861
+ startPromise = void 0;
862
+ throw error;
863
+ }
864
+ };
865
+ return {
866
+ async close() {
867
+ closed = true;
868
+ try {
869
+ await startPromise;
870
+ } catch {}
871
+ if (!server.listening) return;
872
+ await new Promise((resolve, reject) => {
873
+ server.close((error) => error ? reject(error) : resolve());
874
+ server.closeAllConnections();
875
+ });
876
+ viewerUrl = void 0;
877
+ startPromise = void 0;
878
+ },
879
+ start,
880
+ tab() {
881
+ return {
882
+ name: "vize-nuxt-lint-plan",
883
+ title: "Vize Nuxt Lint Plan",
884
+ icon: "carbon:rule",
885
+ requireAuth: true,
886
+ view: viewerUrl ? {
887
+ type: "iframe",
888
+ src: viewerUrl,
889
+ persistent: true
890
+ } : {
891
+ type: "launch",
892
+ description: "Inspect the effective Vize lint rules for any Nuxt source file.",
893
+ actions: [{
894
+ label: "Launch",
895
+ pending: startPromise !== void 0,
896
+ handle: start
897
+ }]
898
+ }
899
+ };
900
+ },
901
+ url: () => viewerUrl
902
+ };
903
+ }
904
+ function listen(server, port) {
905
+ return new Promise((resolve, reject) => {
906
+ const onError = (error) => {
907
+ server.off("listening", onListening);
908
+ reject(error);
909
+ };
910
+ const onListening = () => {
911
+ server.off("error", onError);
912
+ resolve(server.address().port);
913
+ };
914
+ server.once("error", onError);
915
+ server.once("listening", onListening);
916
+ server.listen({
917
+ host: "127.0.0.1",
918
+ port: port ?? 0,
919
+ exclusive: true
920
+ });
921
+ });
922
+ }
923
+ async function handleRequest(request, response, provider, token, nonce, server) {
924
+ if (Buffer.byteLength(request.url ?? "") > MAX_URL_BYTES) {
925
+ sendJson(response, 414, { error: "request_uri_too_long" });
926
+ return;
927
+ }
928
+ const port = server.address()?.port;
929
+ if (!port || ![`127.0.0.1:${port}`, `localhost:${port}`].includes(request.headers.host ?? "")) {
930
+ sendJson(response, 421, { error: "misdirected_request" });
931
+ return;
932
+ }
933
+ if (request.method !== "GET" && request.method !== "HEAD") {
934
+ response.setHeader("allow", "GET, HEAD");
935
+ sendJson(response, 405, { error: "method_not_allowed" });
936
+ return;
937
+ }
938
+ let url;
939
+ try {
940
+ url = new URL(request.url ?? "", "http://localhost");
941
+ } catch {
942
+ sendJson(response, 400, { error: "invalid_url" }, request.method === "HEAD");
943
+ return;
944
+ }
945
+ const rootPath = `/${token}/`;
946
+ if (url.pathname === rootPath && url.search === "") {
947
+ sendHtml(response, renderNuxtLintInspectorHtml(nonce), nonce, request.method === "HEAD");
948
+ return;
949
+ }
950
+ if (url.pathname !== `${rootPath}api`) {
951
+ sendJson(response, 404, { error: "not_found" }, request.method === "HEAD");
952
+ return;
953
+ }
954
+ const parsed = parseApiRequest(url);
955
+ if ("error" in parsed) {
956
+ sendJson(response, parsed.status, { error: parsed.error }, request.method === "HEAD");
957
+ return;
958
+ }
959
+ try {
960
+ sendJson(response, 200, await provider(parsed.request), request.method === "HEAD");
961
+ } catch {
962
+ sendJson(response, 500, { error: "inspector_lint_plan_failed" }, request.method === "HEAD");
963
+ }
964
+ }
965
+ function parseApiRequest(url) {
966
+ for (const key of url.searchParams.keys()) if (key !== "file" && key !== "fresh") return {
967
+ status: 400,
968
+ error: "invalid_query"
969
+ };
970
+ const fresh = url.searchParams.getAll("fresh");
971
+ if (fresh.length > 1 || fresh[0] !== void 0 && fresh[0] !== "1") return {
972
+ status: 400,
973
+ error: "invalid_fresh"
974
+ };
975
+ const requested = url.searchParams.getAll("file");
976
+ if (requested.length > MAX_FILES) return {
977
+ status: 413,
978
+ error: "too_many_files"
979
+ };
980
+ const files = [...new Set(requested)];
981
+ if (files.some((file) => !isSafeFile(file))) return {
982
+ status: 400,
983
+ error: "invalid_file"
984
+ };
985
+ return { request: {
986
+ files,
987
+ fresh: fresh[0] === "1"
988
+ } };
989
+ }
990
+ function isSafeFile(file) {
991
+ return file.length > 0 && Buffer.byteLength(file) <= MAX_FILE_BYTES && !file.includes("\0") && !file.includes("\\") && !file.startsWith("/") && !/^[A-Za-z]:/u.test(file) && !file.split("/").some((part) => part.length === 0 || part === "..");
992
+ }
993
+ function setCommonHeaders(response, resourcePolicy) {
994
+ response.setHeader("cache-control", "no-store");
995
+ response.setHeader("cross-origin-resource-policy", resourcePolicy);
996
+ response.setHeader("referrer-policy", "no-referrer");
997
+ response.setHeader("x-content-type-options", "nosniff");
998
+ }
999
+ function sendHtml(response, body, nonce, headOnly) {
1000
+ response.statusCode = 200;
1001
+ setCommonHeaders(response, "cross-origin");
1002
+ response.setHeader("content-security-policy", `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; frame-ancestors http://localhost:* http://127.0.0.1:*`);
1003
+ response.setHeader("content-type", "text/html; charset=utf-8");
1004
+ response.setHeader("content-length", Buffer.byteLength(body));
1005
+ response.end(headOnly ? void 0 : body);
1006
+ }
1007
+ function sendJson(response, status, payload, headOnly = false) {
1008
+ let body = JSON.stringify(payload);
1009
+ if (body === void 0) {
1010
+ status = 500;
1011
+ body = JSON.stringify({ error: "inspector_lint_plan_failed" });
1012
+ }
1013
+ if (Buffer.byteLength(body) > MAX_RESPONSE_BYTES) {
1014
+ status = 413;
1015
+ body = JSON.stringify({ error: "inspector_response_too_large" });
1016
+ }
1017
+ response.statusCode = status;
1018
+ setCommonHeaders(response, "same-origin");
1019
+ response.setHeader("content-security-policy", "default-src 'none'");
1020
+ response.setHeader("content-type", "application/json; charset=utf-8");
1021
+ response.setHeader("content-length", Buffer.byteLength(body));
1022
+ response.end(headOnly ? void 0 : body);
1023
+ }
1024
+ //#endregion
1025
+ //#region src/lint/inspector.ts
1026
+ function createNuxtLintInspectorProvider(generation, dependencies = {}) {
1027
+ const inspect = dependencies.inspectLintPlan ?? inspectWithNative;
1028
+ return async (request) => {
1029
+ const items = await generation.resolvePlan(request.fresh);
1030
+ const serialized = await inspect(JSON.stringify({ items }), generation.root, request.files);
1031
+ const payload = JSON.parse(serialized);
1032
+ if (!isLintPlanPayload(payload)) throw new Error("Native lint-plan inspector returned an invalid payload");
1033
+ return payload;
1034
+ };
1035
+ }
1036
+ async function setupLintInspector(lint, nuxt, compiler, generation, enabled) {
1037
+ if (enabled === false || !generation) return;
1038
+ const provider = createNuxtLintInspectorProvider(generation);
1039
+ if (compiler !== false) {
1040
+ compiler.inspector ||= {};
1041
+ compiler.inspector.lintPlan ||= provider;
1042
+ }
1043
+ await setupNuxtLintDevtools(typeof lint === "object" && lint !== null ? lint.devtools : void 0, nuxt, compiler === false ? provider : compiler.inspector.lintPlan);
1044
+ }
1045
+ async function inspectWithNative(plan, root, files) {
1046
+ const { inspectLintPlan } = await import("@vizejs/native");
1047
+ return inspectLintPlan(plan, root, files);
1048
+ }
1049
+ function isLintPlanPayload(value) {
1050
+ return value !== null && typeof value === "object" && !Array.isArray(value) && value.schema === "vize.inspector.lint-plan" && value.version === 1 && typeof value.root === "string" && Array.isArray(value.items) && Array.isArray(value.files);
1051
+ }
1052
+ //#endregion
1053
+ //#region src/lint/generation.ts
1054
+ /** Nuxt module wiring for generated oxlint configuration. */
1055
+ const GENERATED_CONFIG_NAME = "oxlint.config.json";
1056
+ /** Root config names supported by oxlint's config discovery. */
1057
+ const ROOT_OXLINT_CONFIG_NAMES = [
1058
+ ".oxlintrc.json",
1059
+ ".oxlintrc.jsonc",
1060
+ "oxlint.config.ts",
1061
+ "oxlint.config.mts"
1062
+ ];
1063
+ function isNotFound(error) {
1064
+ return error.code === "ENOENT";
1065
+ }
1066
+ /**
1067
+ * Write bytes only when they differ from the current regular file.
1068
+ *
1069
+ * The length fast path mirrors Canon's materializer. A full byte comparison is
1070
+ * still required before skipping so equal-size edits are never mistaken for a
1071
+ * cache hit. Symlinks are rejected instead of followed.
1072
+ */
1073
+ async function writeFileIfChanged(file, content) {
1074
+ await mkdir(path.dirname(file), { recursive: true });
1075
+ try {
1076
+ const metadata = await lstat(file);
1077
+ if (!metadata.isFile() || metadata.isSymbolicLink()) throw new Error(`Generated oxlint config must be a regular file: ${file}`);
1078
+ if (metadata.size === Buffer.byteLength(content)) {
1079
+ if ((await readFile(file)).equals(Buffer.from(content))) return false;
1080
+ }
1081
+ } catch (error) {
1082
+ if (!isNotFound(error)) throw error;
1083
+ }
1084
+ const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
1085
+ try {
1086
+ await writeFile(temporary, content, { flag: "wx" });
1087
+ await rename(temporary, file);
1088
+ } finally {
1089
+ await unlink(temporary).catch((error) => {
1090
+ if (!isNotFound(error)) throw error;
1091
+ });
1092
+ }
1093
+ return true;
1094
+ }
1095
+ function hasTypeScript(rootDir) {
1096
+ try {
1097
+ createRequire(path.join(rootDir, "package.json")).resolve("typescript");
1098
+ return true;
1099
+ } catch {
1100
+ return false;
1101
+ }
1102
+ }
1103
+ function relativeSpecifier(from, target) {
1104
+ const relative = path.relative(from, target);
1105
+ if (path.isAbsolute(relative)) return pathToFileURL(target).href;
1106
+ const normalized = relative.split(path.sep).join("/");
1107
+ return normalized.startsWith("./") || normalized.startsWith("../") ? normalized : `./${normalized}`;
1108
+ }
1109
+ function resolveVizePluginSpecifier(configDir) {
1110
+ return relativeSpecifier(configDir, fileURLToPath(import.meta.resolve("oxlint-plugin-vize")));
1111
+ }
1112
+ async function findRootOxlintConfig(rootDir) {
1113
+ let directory = path.resolve(rootDir);
1114
+ while (true) {
1115
+ for (const name of ROOT_OXLINT_CONFIG_NAMES) {
1116
+ const candidate = path.join(directory, name);
1117
+ try {
1118
+ const metadata = await lstat(candidate);
1119
+ if (metadata.isFile() || metadata.isSymbolicLink()) return candidate;
1120
+ } catch (error) {
1121
+ if (!isNotFound(error)) throw error;
1122
+ }
1123
+ }
1124
+ const parent = path.dirname(directory);
1125
+ if (parent === directory) return void 0;
1126
+ directory = parent;
1127
+ }
1128
+ }
1129
+ function renderRootOxlintConfig(rootDir, generatedConfig) {
1130
+ const specifier = relativeSpecifier(rootDir, generatedConfig);
1131
+ return [
1132
+ "// Generated by @vizejs/nuxt.",
1133
+ "import { readFileSync } from \"node:fs\";",
1134
+ "",
1135
+ "type OxlintJsPlugin = string | { name?: string; specifier: string };",
1136
+ "",
1137
+ `const generatedUrl = new URL(${JSON.stringify(specifier)}, import.meta.url);`,
1138
+ "const config = JSON.parse(readFileSync(generatedUrl, \"utf8\"));",
1139
+ "config.jsPlugins = config.jsPlugins.map((plugin: OxlintJsPlugin) =>",
1140
+ " typeof plugin === \"string\"",
1141
+ " ? new URL(plugin, generatedUrl).href",
1142
+ " : { ...plugin, specifier: new URL(plugin.specifier, generatedUrl).href },",
1143
+ ");",
1144
+ "",
1145
+ "export default config;",
1146
+ ""
1147
+ ].join("\n");
1148
+ }
1149
+ async function initRootOxlintConfig(rootDir, generatedConfig) {
1150
+ if (await findRootOxlintConfig(rootDir)) return;
1151
+ const handle = await open(path.join(rootDir, "oxlint.config.mts"), "wx").catch((error) => {
1152
+ if (error.code === "EEXIST") return void 0;
1153
+ throw error;
1154
+ });
1155
+ if (!handle) return;
1156
+ try {
1157
+ await handle.writeFile(renderRootOxlintConfig(rootDir, generatedConfig), "utf8");
1158
+ } finally {
1159
+ await handle.close();
1160
+ }
1161
+ }
1162
+ /**
1163
+ * Generate the initial config and register the Nuxt regeneration hook.
1164
+ *
1165
+ * Addons are resolved inside `regenerate`, not at setup time, so Nuxt's import
1166
+ * registry and third-party module hooks can change between generateApp passes.
1167
+ */
1168
+ async function setupNuxtLintConfigGeneration(lint, nuxt, dependencies = {}) {
1169
+ if (lint === false) return void 0;
1170
+ const { autoInit = true, configFile: configuredFile, rootDir: configuredRoot, ...featureOptions } = typeof lint === "object" && lint !== null ? lint : {};
1171
+ const nuxtRoot = path.resolve(nuxt.options.rootDir);
1172
+ const planRoot = configuredRoot ? path.resolve(nuxtRoot, configuredRoot) : nuxtRoot;
1173
+ const configFile = path.resolve(nuxtRoot, configuredFile ?? path.join(nuxt.options.buildDir, GENERATED_CONFIG_NAME));
1174
+ const hasTypeScriptProbe = dependencies.hasTypeScript ?? hasTypeScript;
1175
+ const resolvePluginSpecifier = dependencies.resolvePluginSpecifier ?? resolveVizePluginSpecifier;
1176
+ const resolveAddons = dependencies.resolveAddons ?? ("callHook" in nuxt ? setupNuxtLintConfigAddons(nuxt) : void 0);
1177
+ let currentPlan = [];
1178
+ const regenerate = async () => {
1179
+ const plan = buildNuxtLintPlan(resolveNuxtLintFeatures(featureOptions, () => hasTypeScriptProbe(planRoot)), collectNuxtLintDirs(toNuxtLintProjectState(nuxt.options, { rootDir: planRoot })));
1180
+ const addons = await resolveAddons?.() ?? [];
1181
+ const nextPlan = [...plan, ...addons];
1182
+ const changed = await writeFileIfChanged(configFile, renderNuxtOxlintConfig(nextPlan, resolvePluginSpecifier(path.dirname(configFile))));
1183
+ currentPlan = nextPlan;
1184
+ return changed;
1185
+ };
1186
+ const resolvePlan = async (fresh = false) => {
1187
+ if (fresh) await regenerate();
1188
+ return currentPlan;
1189
+ };
1190
+ await regenerate();
1191
+ nuxt.hook("builder:generateApp", regenerate);
1192
+ if (autoInit) await initRootOxlintConfig(nuxtRoot, configFile);
1193
+ return {
1194
+ configFile,
1195
+ root: planRoot,
1196
+ regenerate,
1197
+ resolvePlan
1198
+ };
1199
+ }
1200
+ //#endregion
1201
+ export { toNuxtLintProjectState as a, setupNuxtLintConfigAddons as c, createNuxtLintCheckerVitePlugin as d, resolveNuxtLintCheckerOptions as f, setupLintInspector as i, setupNuxtLintChecker as l, setupNuxtLintConfigGeneration as n, renderNuxtOxlintConfig as o, writeFileIfChanged as r, VIZE_NUXT_LINT_CONFIG_ADDONS_HOOK as s, ROOT_OXLINT_CONFIG_NAMES as t, createNuxtLintCheckerWebpackPlugin as u };