nuxt-layerscope 0.1.3 → 0.1.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.4
4
+
5
+ - Packages that Nuxt hoists into the generated tsconfig `paths` (`ofetch`, `consola`, `h3`, `defu`,
6
+ `nitropack`) are no longer reported as unresolved imports.
7
+ - The DevTools tab serves JSON endpoints under `/__layerscope/api` (`state`, `report`, `rerun`),
8
+ with an `ETag` revision that only changes when the findings, layers or notes change.
9
+ - `eslint --cache` no longer keeps stale results: `configs.recommended` carries a digest of the
10
+ registry and the layer config, so ESLint discards cached results when either changes.
11
+ - The ESLint plugin no longer reuses a file's last result after the registry or config changed,
12
+ which showed stale findings in editors until the file's text changed.
13
+ - The DevTools tab re-analyzes only files that changed since the last run.
14
+ - The DevTools tab reads `layerscope-baseline.json`, so accepted findings are left out and counted
15
+ as "in baseline", like `layerscope check`.
16
+
3
17
  ## 0.1.3
4
18
 
5
19
  - Notes and the `--verbose` line are printed after the report, in every command, so they are the
@@ -1,104 +1,11 @@
1
- import { c as realPath, d as effectiveConfig, f as labelConfigError, g as ruleSeverity, h as isRuleName, s as readJson, v as LayerscopeError } from "./layer-factory-DFh7nkFH.mjs";
1
+ import { E as LayerscopeError, b as labelConfigError, f as collectFiles, v as realPath, w as ruleSeverity, y as effectiveConfig } from "./layer-factory-DVSf2Wj4.mjs";
2
2
  import { r as pascalCase, t as compareStrings } from "./strings-CrOjNZmB.mjs";
3
- import { t as globComponents } from "./component-files-DgOXNE6l.mjs";
4
- import { c as createOwnerLookup, d as createAnalysisEnv, f as readRegistry, h as boundaryFindings, m as loadSymbolTable, n as analyzeFile, p as tableFromRegistry, r as collectFiles, u as resolveLayers, v as loadConfig } from "./file-analysis-Bh7qcWUK.mjs";
3
+ import { o as applyBaselineFile, r as analyzeFiles, t as globComponents } from "./component-files-B_qvkKwv.mjs";
4
+ import { a as readRegistry, c as boundaryFindings, d as loadConfig, i as createAnalysisEnv, o as tableFromRegistry, r as resolveLayers, s as loadSymbolTable, t as createOwnerLookup } from "./owner-BA00V9VD.mjs";
5
5
  import { basename, extname, join, relative, resolve } from "pathe";
6
- import { existsSync, statSync, writeFileSync } from "node:fs";
6
+ import { existsSync, statSync } from "node:fs";
7
7
  import { escapePath, glob } from "tinyglobby";
8
8
  import { spawnSync } from "node:child_process";
9
- //#region src/baseline/index.ts
10
- const BASELINE_FILE = "layerscope-baseline.json";
11
- /** Bumped on breaking changes to the baseline file. */
12
- const BASELINE_VERSION = 1;
13
- /** Rule, file, symbol and target layer; lines are left out so unrelated edits keep the entry. */
14
- function keyOf(entry) {
15
- return [
16
- entry.rule,
17
- entry.file,
18
- entry.symbol,
19
- entry.toLayer ?? ""
20
- ].join("\0");
21
- }
22
- function toKeyed(finding, rootDir) {
23
- return {
24
- rule: finding.rule,
25
- file: relative(rootDir, finding.file),
26
- symbol: finding.symbol,
27
- toLayer: finding.toLayer
28
- };
29
- }
30
- function compareEntries(a, b) {
31
- return compareStrings(a.file, b.file) || compareStrings(a.rule, b.rule) || compareStrings(a.symbol, b.symbol) || compareStrings(a.toLayer ?? "", b.toLayer ?? "");
32
- }
33
- function createBaseline(findings, rootDir) {
34
- const entries = /* @__PURE__ */ new Map();
35
- for (const finding of findings) {
36
- const keyed = toKeyed(finding, rootDir);
37
- const key = keyOf(keyed);
38
- const entry = entries.get(key);
39
- if (entry === void 0) entries.set(key, keyed);
40
- else entry.count = (entry.count ?? 1) + 1;
41
- }
42
- return {
43
- version: 1,
44
- entries: [...entries.values()].toSorted(compareEntries)
45
- };
46
- }
47
- function writeBaseline(file, baseline) {
48
- writeFileSync(file, `${JSON.stringify(baseline, null, 2)}\n`);
49
- }
50
- function isEntry(value) {
51
- const entry = value;
52
- return typeof entry === "object" && entry !== null && typeof entry.rule === "string" && isRuleName(entry.rule) && typeof entry.file === "string" && typeof entry.symbol === "string" && (entry.toLayer === null || typeof entry.toLayer === "string");
53
- }
54
- function readBaseline(file) {
55
- if (!existsSync(file)) return null;
56
- const baseline = readJson(file);
57
- if (baseline?.version !== 1 || !Array.isArray(baseline.entries)) throw new LayerscopeError(`${file} is not a layerscope baseline (version 1). Recreate it with --update-baseline.`);
58
- const invalid = baseline.entries.findIndex((entry) => !isEntry(entry));
59
- if (invalid !== -1) throw new LayerscopeError(`${file}: entry ${invalid} is not a valid baseline entry`);
60
- return {
61
- version: 1,
62
- entries: baseline.entries
63
- };
64
- }
65
- /** Findings missing from the baseline stay; the rest are suppressed, up to each entry's count. */
66
- function applyBaseline(findings, baseline, file, rootDir) {
67
- const remaining = new Map(baseline.entries.map((entry) => [keyOf(entry), {
68
- entry,
69
- left: entry.count ?? 1
70
- }]));
71
- const fresh = [];
72
- const suppressed = [];
73
- for (const finding of findings) {
74
- const slot = remaining.get(keyOf(toKeyed(finding, rootDir)));
75
- if (slot === void 0 || slot.left === 0) fresh.push(finding);
76
- else {
77
- slot.left -= 1;
78
- suppressed.push(finding);
79
- }
80
- }
81
- return {
82
- findings: fresh,
83
- baseline: {
84
- file,
85
- suppressed,
86
- removable: [...remaining.values()].flatMap(({ entry, left }) => left === 0 ? [] : [left === (entry.count ?? 1) ? entry : {
87
- ...entry,
88
- count: left
89
- }])
90
- }
91
- };
92
- }
93
- /** The result with the baseline applied, or unchanged when the file does not exist. */
94
- function applyBaselineFile(result, file) {
95
- const baseline = readBaseline(file);
96
- return baseline === null ? result : {
97
- ...result,
98
- ...applyBaseline(result.findings, baseline, file, result.rootDir)
99
- };
100
- }
101
- //#endregion
102
9
  //#region src/rules/compare.ts
103
10
  /** Total order on position, then rule or kind, then symbol, for deterministic reports. */
104
11
  function compareByPosition(a, b) {
@@ -335,7 +242,7 @@ async function analyze(options = {}) {
335
242
  });
336
243
  const { layers, env, registry, config } = environment;
337
244
  const files = await collectFiles(layers, env.ownerOf, config.ignore ?? []);
338
- const analyses = [...files].map(([file, layer]) => analyzeFile(file, layer, env));
245
+ const analyses = analyzeFiles(files, env, options.cache, options.envKey);
339
246
  const edges = analyses.flatMap((analysis) => analysis.edges).toSorted(compareByPosition);
340
247
  const { findings, notes } = runRules({
341
248
  edges,
@@ -360,4 +267,4 @@ async function analyze(options = {}) {
360
267
  return options.baseline === void 0 ? result : applyBaselineFile(result, resolve(rootDir, options.baseline));
361
268
  }
362
269
  //#endregion
363
- export { writeBaseline as a, createBaseline as i, BASELINE_FILE as n, BASELINE_VERSION as r, analyze as t };
270
+ export { analyze as t };
@@ -0,0 +1,2 @@
1
+ import { t as analyze } from "./analyze-BwmKhroS.mjs";
2
+ export { analyze };
package/dist/api.d.mts CHANGED
@@ -1,6 +1,93 @@
1
- import { a as Context, c as Layer, d as Reference, f as ReferenceKind, g as SourceOption, h as Severity, i as BaselineResult, l as LayerRule, m as RuleName, n as AnalyzeResult, o as Edge, p as ResolutionSource, r as BaselineEntry, s as Finding, t as defineConfig, u as LayerscopeConfig } from "./define-DNRGLVG_.mjs";
2
- import { t as ProjectConfig } from "./effective-Dqg4EwH_.mjs";
1
+ import { _ as SymbolTable, a as Context, c as Layer, d as Reference, f as ReferenceKind, g as SourceOption, h as Severity, i as BaselineResult, l as LayerRule, m as RuleName, n as AnalyzeResult, o as Edge, p as ResolutionSource, r as BaselineEntry, s as Finding, t as defineConfig, u as LayerscopeConfig } from "./define-DDg4HQRH.mjs";
2
+ import { t as ProjectConfig } from "./effective-BPMNHe9A.mjs";
3
3
  import { styleText } from "node:util";
4
+ //#region src/nuxt/owner.d.ts
5
+ type OwnerLookup = (file: string) => Layer | null;
6
+ //#endregion
7
+ //#region src/scan/types.d.ts
8
+ /** References found in one source file. Offsets point into the original file. */
9
+ interface FileScan {
10
+ /** Identifiers without a local binding. */
11
+ free: Named[];
12
+ /** Identifiers a template reads from the component instance (`_ctx.x`). */
13
+ templateIdents: Named[];
14
+ components: Named[];
15
+ /** Offsets of `<component :is>` bound to a runtime value. */
16
+ dynamicComponents: number[];
17
+ imports: ImportRef[];
18
+ /** Set when the file could not be parsed; nothing else in it was analyzed. */
19
+ error: {
20
+ message: string;
21
+ offset: number;
22
+ } | null;
23
+ }
24
+ interface Named {
25
+ name: string;
26
+ offset: number;
27
+ }
28
+ interface ImportRef {
29
+ specifier: string;
30
+ /** Export names, with `default` and `*` for default and namespace imports. */
31
+ names: string[];
32
+ offset: number;
33
+ }
34
+ //#endregion
35
+ //#region src/nuxt/aliases.d.ts
36
+ /** Alias → absolute path or bare module, longest alias first. */
37
+ type AliasMap = [string, string][];
38
+ //#endregion
39
+ //#region src/analyze/import-refs.d.ts
40
+ interface ImportEnv {
41
+ table: SymbolTable;
42
+ aliases: AliasMap;
43
+ buildDir: string;
44
+ }
45
+ //#endregion
46
+ //#region src/analyze/file-analysis.d.ts
47
+ interface AnalysisEnv extends ImportEnv {
48
+ ownerOf: OwnerLookup;
49
+ identifiers: Set<string>;
50
+ components: Set<string>;
51
+ unresolvedSeverity: Severity;
52
+ }
53
+ /** Turns one file's scan into dependency edges and unresolved-reference findings. */
54
+ declare class FileAnalysis {
55
+ readonly edges: Edge[];
56
+ readonly unresolved: Finding[];
57
+ /** Renders a component chosen at runtime, so any component may be used. */
58
+ hasDynamicComponent: boolean;
59
+ private readonly seen;
60
+ readonly file: string;
61
+ private readonly source;
62
+ private readonly layer;
63
+ private readonly env;
64
+ private readonly context;
65
+ constructor(file: string, source: string, layer: Layer, env: AnalysisEnv);
66
+ run(scan: FileScan): this;
67
+ private addIdentifiers;
68
+ private addComponents;
69
+ private addImports;
70
+ private reference;
71
+ /** Only the first use of a symbol per file is recorded. */
72
+ private firstSeen;
73
+ private addEdge;
74
+ private addUnresolved;
75
+ }
76
+ //#endregion
77
+ //#region src/analyze/cache.d.ts
78
+ /**
79
+ * Keeps one file's analysis until its mtime or size changes. Analyses depend on the symbol table,
80
+ * so a changed `envKey` (registry, generated types, config) drops everything.
81
+ */
82
+ declare class AnalysisCache {
83
+ private envKey;
84
+ private readonly entries;
85
+ get size(): number;
86
+ reset(envKey: string): void;
87
+ get(file: string, compute: () => FileAnalysis): FileAnalysis;
88
+ retain(files: Iterable<string>): void;
89
+ }
90
+ //#endregion
4
91
  //#region src/analyze/index.d.ts
5
92
  interface AnalyzeOptions {
6
93
  /** Defaults to `process.cwd()`. */
@@ -18,6 +105,10 @@ interface AnalyzeOptions {
18
105
  * Ignored when the file does not exist.
19
106
  */
20
107
  baseline?: string;
108
+ /** Reuses per-file analyses across runs. Used by the DevTools tab; the CLI passes none. */
109
+ cache?: AnalysisCache;
110
+ /** Changes when the symbol table or config changes, which flushes `cache`. */
111
+ envKey?: string;
21
112
  }
22
113
  export declare function analyze(options?: AnalyzeOptions): Promise<AnalyzeResult>;
23
114
  //#endregion
package/dist/api.mjs CHANGED
@@ -1,6 +1,7 @@
1
- import { l as REGISTRY_FILE, u as REGISTRY_VERSION, v as LayerscopeError } from "./layer-factory-DFh7nkFH.mjs";
1
+ import { E as LayerscopeError, o as REGISTRY_FILE, s as REGISTRY_VERSION } from "./layer-factory-DVSf2Wj4.mjs";
2
+ import { a as BASELINE_VERSION, i as BASELINE_FILE, s as createBaseline } from "./component-files-B_qvkKwv.mjs";
2
3
  import { t as defineConfig } from "./define-l62B9fVk.mjs";
3
- import { i as createBaseline, n as BASELINE_FILE, r as BASELINE_VERSION, t as analyze } from "./analyze-QaYmBXyK.mjs";
4
- import { i as JSON_REPORT_VERSION, n as formatResult, t as OUTPUT_FORMATS } from "./report-DShTCB59.mjs";
5
- import { r as buildGraph, t as findUnused } from "./unused-BtsPSfxb.mjs";
4
+ import { t as analyze } from "./analyze-BwmKhroS.mjs";
5
+ import { i as JSON_REPORT_VERSION, n as formatResult, t as OUTPUT_FORMATS } from "./report-jEMtNXS8.mjs";
6
+ import { r as buildGraph, t as findUnused } from "./unused-DbvZ4gRV.mjs";
6
7
  export { BASELINE_FILE, BASELINE_VERSION, JSON_REPORT_VERSION, LayerscopeError, OUTPUT_FORMATS, REGISTRY_FILE, REGISTRY_VERSION, analyze, buildGraph, createBaseline, defineConfig, findUnused, formatResult };
package/dist/bin.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { v as LayerscopeError } from "./layer-factory-DFh7nkFH.mjs";
2
+ import { E as LayerscopeError } from "./layer-factory-DVSf2Wj4.mjs";
3
3
  import { i as plural, r as pascalCase } from "./strings-CrOjNZmB.mjs";
4
- import { a as writeBaseline, i as createBaseline, n as BASELINE_FILE, t as analyze } from "./analyze-QaYmBXyK.mjs";
5
- import { a as paintFor, n as formatResult, r as isOutputFormat, t as OUTPUT_FORMATS } from "./report-DShTCB59.mjs";
6
- import { i as edgeStatus, n as GRAPH_LEVELS, r as buildGraph, t as findUnused } from "./unused-BtsPSfxb.mjs";
4
+ import { c as writeBaseline, i as BASELINE_FILE, s as createBaseline } from "./component-files-B_qvkKwv.mjs";
5
+ import { t as analyze } from "./analyze-BwmKhroS.mjs";
6
+ import { a as paintFor, n as formatResult, r as isOutputFormat, t as OUTPUT_FORMATS } from "./report-jEMtNXS8.mjs";
7
+ import { i as edgeStatus, n as GRAPH_LEVELS, r as buildGraph, t as findUnused } from "./unused-DbvZ4gRV.mjs";
7
8
  import { relative, resolve } from "pathe";
8
9
  import { cac } from "cac";
9
10
  //#region src/commands/shared.ts
@@ -0,0 +1,153 @@
1
+ import { C as isRuleName, E as LayerscopeError, _ as readJson, l as analyzeFile } from "./layer-factory-DVSf2Wj4.mjs";
2
+ import { t as compareStrings } from "./strings-CrOjNZmB.mjs";
3
+ import { relative } from "pathe";
4
+ import { existsSync, statSync, writeFileSync } from "node:fs";
5
+ import { glob } from "tinyglobby";
6
+ //#region src/baseline/index.ts
7
+ const BASELINE_FILE = "layerscope-baseline.json";
8
+ /** Bumped on breaking changes to the baseline file. */
9
+ const BASELINE_VERSION = 1;
10
+ /** Rule, file, symbol and target layer; lines are left out so unrelated edits keep the entry. */
11
+ function keyOf(entry) {
12
+ return [
13
+ entry.rule,
14
+ entry.file,
15
+ entry.symbol,
16
+ entry.toLayer ?? ""
17
+ ].join("\0");
18
+ }
19
+ function toKeyed(finding, rootDir) {
20
+ return {
21
+ rule: finding.rule,
22
+ file: relative(rootDir, finding.file),
23
+ symbol: finding.symbol,
24
+ toLayer: finding.toLayer
25
+ };
26
+ }
27
+ function compareEntries(a, b) {
28
+ return compareStrings(a.file, b.file) || compareStrings(a.rule, b.rule) || compareStrings(a.symbol, b.symbol) || compareStrings(a.toLayer ?? "", b.toLayer ?? "");
29
+ }
30
+ function createBaseline(findings, rootDir) {
31
+ const entries = /* @__PURE__ */ new Map();
32
+ for (const finding of findings) {
33
+ const keyed = toKeyed(finding, rootDir);
34
+ const key = keyOf(keyed);
35
+ const entry = entries.get(key);
36
+ if (entry === void 0) entries.set(key, keyed);
37
+ else entry.count = (entry.count ?? 1) + 1;
38
+ }
39
+ return {
40
+ version: 1,
41
+ entries: [...entries.values()].toSorted(compareEntries)
42
+ };
43
+ }
44
+ function writeBaseline(file, baseline) {
45
+ writeFileSync(file, `${JSON.stringify(baseline, null, 2)}\n`);
46
+ }
47
+ function isEntry(value) {
48
+ const entry = value;
49
+ return typeof entry === "object" && entry !== null && typeof entry.rule === "string" && isRuleName(entry.rule) && typeof entry.file === "string" && typeof entry.symbol === "string" && (entry.toLayer === null || typeof entry.toLayer === "string");
50
+ }
51
+ function readBaseline(file) {
52
+ if (!existsSync(file)) return null;
53
+ const baseline = readJson(file);
54
+ if (baseline?.version !== 1 || !Array.isArray(baseline.entries)) throw new LayerscopeError(`${file} is not a layerscope baseline (version 1). Recreate it with --update-baseline.`);
55
+ const invalid = baseline.entries.findIndex((entry) => !isEntry(entry));
56
+ if (invalid !== -1) throw new LayerscopeError(`${file}: entry ${invalid} is not a valid baseline entry`);
57
+ return {
58
+ version: 1,
59
+ entries: baseline.entries
60
+ };
61
+ }
62
+ /** Findings missing from the baseline stay; the rest are suppressed, up to each entry's count. */
63
+ function applyBaseline(findings, baseline, file, rootDir) {
64
+ const remaining = new Map(baseline.entries.map((entry) => [keyOf(entry), {
65
+ entry,
66
+ left: entry.count ?? 1
67
+ }]));
68
+ const fresh = [];
69
+ const suppressed = [];
70
+ for (const finding of findings) {
71
+ const slot = remaining.get(keyOf(toKeyed(finding, rootDir)));
72
+ if (slot === void 0 || slot.left === 0) fresh.push(finding);
73
+ else {
74
+ slot.left -= 1;
75
+ suppressed.push(finding);
76
+ }
77
+ }
78
+ return {
79
+ findings: fresh,
80
+ baseline: {
81
+ file,
82
+ suppressed,
83
+ removable: [...remaining.values()].flatMap(({ entry, left }) => left === 0 ? [] : [left === (entry.count ?? 1) ? entry : {
84
+ ...entry,
85
+ count: left
86
+ }])
87
+ }
88
+ };
89
+ }
90
+ /** The result with the baseline applied, or unchanged when the file does not exist. */
91
+ function applyBaselineFile(result, file) {
92
+ const baseline = readBaseline(file);
93
+ return baseline === null ? result : {
94
+ ...result,
95
+ ...applyBaseline(result.findings, baseline, file, result.rootDir)
96
+ };
97
+ }
98
+ //#endregion
99
+ //#region src/analyze/cache.ts
100
+ /**
101
+ * Keeps one file's analysis until its mtime or size changes. Analyses depend on the symbol table,
102
+ * so a changed `envKey` (registry, generated types, config) drops everything.
103
+ */
104
+ var AnalysisCache = class {
105
+ constructor() {
106
+ this.envKey = "";
107
+ this.entries = /* @__PURE__ */ new Map();
108
+ }
109
+ get size() {
110
+ return this.entries.size;
111
+ }
112
+ reset(envKey) {
113
+ if (envKey !== this.envKey) {
114
+ this.envKey = envKey;
115
+ this.entries.clear();
116
+ }
117
+ }
118
+ get(file, compute) {
119
+ const { mtimeMs, size } = statSync(file);
120
+ const entry = this.entries.get(file);
121
+ if (entry?.mtimeMs === mtimeMs && entry.size === size) return entry.analysis;
122
+ const analysis = compute();
123
+ this.entries.set(file, {
124
+ mtimeMs,
125
+ size,
126
+ analysis
127
+ });
128
+ return analysis;
129
+ }
130
+ retain(files) {
131
+ const keep = new Set(files);
132
+ for (const file of this.entries.keys()) if (!keep.has(file)) this.entries.delete(file);
133
+ }
134
+ };
135
+ /** Analyzes every file, reusing `cache` entries that are still current. */
136
+ function analyzeFiles(files, env, cache, envKey = "") {
137
+ cache?.reset(`${envKey}\0${[...files.keys()].toSorted().join("\0")}`);
138
+ const analyses = [...files].map(([file, layer]) => cache === void 0 ? analyzeFile(file, layer, env) : cache.get(file, () => analyzeFile(file, layer, env)));
139
+ cache?.retain(files.keys());
140
+ return analyses;
141
+ }
142
+ //#endregion
143
+ //#region src/registry/component-files.ts
144
+ /** Files in a component dir, matched the way Nuxt's components module scans it. */
145
+ async function globComponents(dir, pattern, ignore) {
146
+ return (await glob(pattern, {
147
+ cwd: dir,
148
+ absolute: true,
149
+ ignore
150
+ })).toSorted();
151
+ }
152
+ //#endregion
153
+ export { BASELINE_VERSION as a, writeBaseline as c, BASELINE_FILE as i, AnalysisCache as n, applyBaselineFile as o, analyzeFiles as r, createBaseline as s, globComponents as t };
@@ -123,4 +123,4 @@ interface AnalyzeResult {
123
123
  //#region src/config/define.d.ts
124
124
  declare function defineConfig(config: LayerscopeConfig): LayerscopeConfig;
125
125
  //#endregion
126
- export { Context as a, Layer as c, Reference as d, ReferenceKind as f, SourceOption as g, Severity as h, BaselineResult as i, LayerRule as l, RuleName as m, AnalyzeResult as n, Edge as o, ResolutionSource as p, BaselineEntry as r, Finding as s, defineConfig as t, LayerscopeConfig as u };
126
+ export { SymbolTable as _, Context as a, Layer as c, Reference as d, ReferenceKind as f, SourceOption as g, Severity as h, BaselineResult as i, LayerRule as l, RuleName as m, AnalyzeResult as n, Edge as o, ResolutionSource as p, BaselineEntry as r, Finding as s, defineConfig as t, LayerscopeConfig as u };
package/dist/define.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { t as defineConfig } from "./define-DNRGLVG_.mjs";
1
+ import { t as defineConfig } from "./define-DDg4HQRH.mjs";
2
2
  export { defineConfig };
@@ -1,4 +1,4 @@
1
- import { u as LayerscopeConfig } from "./define-DNRGLVG_.mjs";
1
+ import { u as LayerscopeConfig } from "./define-DDg4HQRH.mjs";
2
2
  //#region src/config/effective.d.ts
3
3
  /** The keys that describe the project; `buildDir` only says where to look. */
4
4
  declare const PROJECT_CONFIG_KEYS: readonly ['layers', 'rules', 'ignore', 'globals'];
package/dist/eslint.mjs CHANGED
@@ -1,68 +1,9 @@
1
- import { d as effectiveConfig, l as REGISTRY_FILE } from "./layer-factory-DFh7nkFH.mjs";
2
- import { t as packageVersion } from "./version-DFWOUgi5.mjs";
3
- import { _ as findConfigFile, a as lineColumn, c as createOwnerLookup, d as createAnalysisEnv, f as readRegistry, h as boundaryFindings, i as isCheckedFile, l as nameLayers, o as offsetAt, p as tableFromRegistry, s as scanFile, t as FileAnalysis, y as loadConfigSync } from "./file-analysis-Bh7qcWUK.mjs";
1
+ import { c as FileAnalysis, g as scanFile, h as offsetAt, m as lineColumn, o as REGISTRY_FILE, p as isCheckedFile, y as effectiveConfig } from "./layer-factory-DVSf2Wj4.mjs";
2
+ import { t as packageVersion } from "./version-D5JrgoO_.mjs";
3
+ import { a as readRegistry, c as boundaryFindings, f as loadConfigSync, i as createAnalysisEnv, n as nameLayers, o as tableFromRegistry, t as createOwnerLookup, u as findConfigFile } from "./owner-BA00V9VD.mjs";
4
4
  import { dirname, join, relative, resolve } from "pathe";
5
5
  import { existsSync, readFileSync, statSync } from "node:fs";
6
- //#region src/eslint/lint-file.ts
7
- /** Both rules lint the same text, so the last result per file is reused. */
8
- const lastLint = /* @__PURE__ */ new Map();
9
- /**
10
- * oxlint passes only the `<script>` of a `.vue` file to JS plugins, while the template matters
11
- * too; the saved file is read instead.
12
- */
13
- function fullSource(file, text) {
14
- return file.endsWith(".vue") && !/<(?:script|template)[\s>]/u.test(text) ? readFileSync(file, "utf8") : text;
15
- }
16
- /** Lints the text the editor holds, which may differ from the file on disk. */
17
- function lintFile(file, text, project) {
18
- const source = fullSource(file, text);
19
- const cached = lastLint.get(file);
20
- if (cached?.source === source) return cached.lint;
21
- const layer = project.env.ownerOf(file);
22
- let lint = {
23
- source,
24
- boundary: [],
25
- unresolved: []
26
- };
27
- if (layer !== null && isCheckedFile(file, layer, project.config.ignore ?? [])) {
28
- const analysis = new FileAnalysis(file, source, layer, project.env).run(scanFile(source, file));
29
- lint = {
30
- source,
31
- boundary: boundaryFindings(analysis.edges, project.config),
32
- unresolved: analysis.unresolved
33
- };
34
- }
35
- lastLint.set(file, {
36
- source,
37
- lint
38
- });
39
- return lint;
40
- }
41
- //#endregion
42
- //#region src/eslint/location.ts
43
- /**
44
- * Maps a finding from the full file into the text the linter passed. ESLint passes the full
45
- * file; oxlint passes only a `.vue` file's `<script>`, where template findings have no position.
46
- */
47
- function reportLocation(finding, source, text) {
48
- if (source === text) return {
49
- line: finding.line,
50
- column: finding.column,
51
- outside: false
52
- };
53
- const base = source.indexOf(text);
54
- const offset = offsetAt(source, finding) - base;
55
- if (base === -1 || offset < 0 || offset > text.length) return {
56
- line: 1,
57
- column: 1,
58
- outside: true
59
- };
60
- return {
61
- ...lineColumn(text, offset),
62
- outside: false
63
- };
64
- }
65
- //#endregion
6
+ import { createHash } from "node:crypto";
66
7
  //#region src/eslint/load-project.ts
67
8
  /** Config, layers and symbols of a project, from its registry; Nuxt itself is never loaded. */
68
9
  function loadProject(rootDir) {
@@ -98,8 +39,9 @@ function loadProject(rootDir) {
98
39
  const DEFAULT_REGISTRY = join(".nuxt", REGISTRY_FILE);
99
40
  /** Editors keep the linter running, so a project is reloaded when its inputs change. */
100
41
  const cache = /* @__PURE__ */ new Map();
101
- function findRoot(file) {
102
- for (let dir = dirname(file);; dir = dirname(dir)) {
42
+ /** The nearest directory from `startDir` up that holds a registry. */
43
+ function findProjectRoot(startDir) {
44
+ for (let dir = startDir;; dir = dirname(dir)) {
103
45
  if (existsSync(join(dir, DEFAULT_REGISTRY))) return dir;
104
46
  if (dirname(dir) === dir) return null;
105
47
  }
@@ -119,7 +61,7 @@ function tryLoadProject(rootDir) {
119
61
  }
120
62
  /** The Nuxt project a linted file belongs to, from its registry; Nuxt itself is never loaded. */
121
63
  function projectFor(file, root) {
122
- const rootDir = root === void 0 ? findRoot(file) : resolve(root);
64
+ const rootDir = root === void 0 ? findProjectRoot(dirname(file)) : resolve(root);
123
65
  if (rootDir === null) return {
124
66
  ok: false,
125
67
  message: `No ${DEFAULT_REGISTRY} above this file. Run "nuxi prepare", or set the rule's "root" option to the Nuxt project.`
@@ -137,6 +79,101 @@ function projectFor(file, root) {
137
79
  return load;
138
80
  }
139
81
  //#endregion
82
+ //#region src/eslint/stamp.ts
83
+ function digest(file) {
84
+ return file !== null && existsSync(file) ? createHash("sha1").update(readFileSync(file)).digest("hex") : "";
85
+ }
86
+ /**
87
+ * What layerscope's results depend on besides the linted file: the registry and the layer config.
88
+ * ESLint's cache keys a result on the file and the ESLint config only, so this goes into the
89
+ * config, which drops cached results when it changes. Content, not mtime: `nuxi prepare` rewrites
90
+ * an unchanged registry.
91
+ */
92
+ function projectStamp(cwd = process.cwd()) {
93
+ const root = findProjectRoot(cwd);
94
+ if (root === null) return "";
95
+ const parts = [join(root, DEFAULT_REGISTRY), findConfigFile(root)].map((file) => digest(file));
96
+ return createHash("sha1").update(parts.join(":")).digest("hex");
97
+ }
98
+ //#endregion
99
+ //#region src/eslint/recommended.ts
100
+ /** Built when `eslint.config.*` loads, so `--cache` sees the stamp of this run. */
101
+ function createRecommended(plugin, cwd) {
102
+ return {
103
+ name: "nuxt-layerscope/recommended",
104
+ plugins: { layerscope: plugin },
105
+ settings: { layerscope: { stamp: projectStamp(cwd) } },
106
+ rules: {
107
+ "layerscope/layer-boundary": "error",
108
+ "layerscope/unresolved-reference": "warn"
109
+ }
110
+ };
111
+ }
112
+ //#endregion
113
+ //#region src/eslint/lint-file.ts
114
+ /**
115
+ * Both rules lint the same text, so the last result per file is reused. A reloaded project
116
+ * (registry or config changed) is another object, so results from before it are not reused.
117
+ */
118
+ const lastLint = /* @__PURE__ */ new Map();
119
+ /**
120
+ * oxlint passes only the `<script>` of a `.vue` file to JS plugins, while the template matters
121
+ * too; the saved file is read instead.
122
+ */
123
+ function fullSource(file, text) {
124
+ return file.endsWith(".vue") && !/<(?:script|template)[\s>]/u.test(text) ? readFileSync(file, "utf8") : text;
125
+ }
126
+ /** Lints the text the editor holds, which may differ from the file on disk. */
127
+ function lintFile(file, text, project) {
128
+ const source = fullSource(file, text);
129
+ const cached = lastLint.get(file);
130
+ if (cached?.project === project && cached.source === source) return cached.lint;
131
+ const layer = project.env.ownerOf(file);
132
+ let lint = {
133
+ source,
134
+ boundary: [],
135
+ unresolved: []
136
+ };
137
+ if (layer !== null && isCheckedFile(file, layer, project.config.ignore ?? [])) {
138
+ const analysis = new FileAnalysis(file, source, layer, project.env).run(scanFile(source, file));
139
+ lint = {
140
+ source,
141
+ boundary: boundaryFindings(analysis.edges, project.config),
142
+ unresolved: analysis.unresolved
143
+ };
144
+ }
145
+ lastLint.set(file, {
146
+ project,
147
+ source,
148
+ lint
149
+ });
150
+ return lint;
151
+ }
152
+ //#endregion
153
+ //#region src/eslint/location.ts
154
+ /**
155
+ * Maps a finding from the full file into the text the linter passed. ESLint passes the full
156
+ * file; oxlint passes only a `.vue` file's `<script>`, where template findings have no position.
157
+ */
158
+ function reportLocation(finding, source, text) {
159
+ if (source === text) return {
160
+ line: finding.line,
161
+ column: finding.column,
162
+ outside: false
163
+ };
164
+ const base = source.indexOf(text);
165
+ const offset = offsetAt(source, finding) - base;
166
+ if (base === -1 || offset < 0 || offset > text.length) return {
167
+ line: 1,
168
+ column: 1,
169
+ outside: true
170
+ };
171
+ return {
172
+ ...lineColumn(text, offset),
173
+ outside: false
174
+ };
175
+ }
176
+ //#endregion
140
177
  //#region src/eslint/rules.ts
141
178
  const OPTIONS_SCHEMA = [{
142
179
  type: "object",
@@ -201,13 +238,6 @@ const plugin = {
201
238
  rules,
202
239
  configs: {}
203
240
  };
204
- plugin.configs.recommended = {
205
- name: "nuxt-layerscope/recommended",
206
- plugins: { layerscope: plugin },
207
- rules: {
208
- "layerscope/layer-boundary": "error",
209
- "layerscope/unresolved-reference": "warn"
210
- }
211
- };
241
+ plugin.configs.recommended = createRecommended(plugin);
212
242
  //#endregion
213
243
  export { plugin as default };