nuxt-layerscope 0.1.2 → 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 +23 -0
- package/dist/{analyze-QaYmBXyK.mjs → analyze-BwmKhroS.mjs} +6 -99
- package/dist/analyze-vZUz30sN.mjs +2 -0
- package/dist/api.d.mts +100 -4
- package/dist/api.mjs +5 -4
- package/dist/bin.mjs +23 -14
- package/dist/component-files-B_qvkKwv.mjs +153 -0
- package/dist/{define-DNRGLVG_.d.mts → define-DDg4HQRH.d.mts} +1 -1
- package/dist/define.d.mts +1 -1
- package/dist/{effective-Dqg4EwH_.d.mts → effective-BPMNHe9A.d.mts} +1 -1
- package/dist/eslint.mjs +104 -74
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +203 -23
- package/dist/{file-analysis-Bh7qcWUK.mjs → layer-factory-DVSf2Wj4.mjs} +201 -462
- package/dist/owner-BA00V9VD.mjs +451 -0
- package/dist/report-DG-XzEq4.mjs +2 -0
- package/dist/{report-BNW0GCl9.mjs → report-jEMtNXS8.mjs} +27 -13
- package/dist/{unused-BtsPSfxb.mjs → unused-DbvZ4gRV.mjs} +1 -1
- package/dist/{version-DFWOUgi5.mjs → version-D5JrgoO_.mjs} +1 -1
- package/package.json +1 -1
- package/dist/analyze-DVzyPH7N.mjs +0 -2
- package/dist/component-files-DgOXNE6l.mjs +0 -12
- package/dist/layer-factory-DFh7nkFH.mjs +0 -182
- package/dist/report-DtAi0vPb.mjs +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
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
|
+
|
|
17
|
+
## 0.1.3
|
|
18
|
+
|
|
19
|
+
- Notes and the `--verbose` line are printed after the report, in every command, so they are the
|
|
20
|
+
last thing on screen. `note:` is highlighted in a terminal.
|
|
21
|
+
- `layerscope check` lists files with errors after files with only warnings, so the errors stay
|
|
22
|
+
next to the summary in a long report.
|
|
23
|
+
- The text report colors severities, rule names and the summary in a terminal. `NO_COLOR` and
|
|
24
|
+
`FORCE_COLOR` are honoured.
|
|
25
|
+
|
|
3
26
|
## 0.1.2
|
|
4
27
|
|
|
5
28
|
- A note appears when `nuxt.config` is newer than the registry, because changes to its
|
|
@@ -1,104 +1,11 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
4
|
-
import {
|
|
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
|
|
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 =
|
|
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 {
|
|
270
|
+
export { analyze as t };
|
package/dist/api.d.mts
CHANGED
|
@@ -1,5 +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-
|
|
2
|
-
import { t as ProjectConfig } from "./effective-
|
|
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
|
+
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
|
|
3
91
|
//#region src/analyze/index.d.ts
|
|
4
92
|
interface AnalyzeOptions {
|
|
5
93
|
/** Defaults to `process.cwd()`. */
|
|
@@ -17,6 +105,10 @@ interface AnalyzeOptions {
|
|
|
17
105
|
* Ignored when the file does not exist.
|
|
18
106
|
*/
|
|
19
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;
|
|
20
112
|
}
|
|
21
113
|
export declare function analyze(options?: AnalyzeOptions): Promise<AnalyzeResult>;
|
|
22
114
|
//#endregion
|
|
@@ -114,11 +206,15 @@ interface Registry {
|
|
|
114
206
|
config?: ProjectConfig;
|
|
115
207
|
}
|
|
116
208
|
//#endregion
|
|
209
|
+
//#region src/utils/style.d.ts
|
|
210
|
+
/** Wraps text in terminal styles, e.g. `paint('red', 'error')`. */
|
|
211
|
+
type Paint = (format: Parameters<typeof styleText>[0], text: string) => string;
|
|
212
|
+
//#endregion
|
|
117
213
|
//#region src/report/index.d.ts
|
|
118
214
|
export declare const OUTPUT_FORMATS: readonly ['text', 'github', 'json'];
|
|
119
215
|
type OutputFormat = (typeof OUTPUT_FORMATS)[number];
|
|
120
|
-
/** Paths in the output are relative to `cwd`. */
|
|
121
|
-
export declare function formatResult(result: AnalyzeResult, format: OutputFormat, cwd?: string): string;
|
|
216
|
+
/** Paths in the output are relative to `cwd`. Only the text format uses `paint`. */
|
|
217
|
+
export declare function formatResult(result: AnalyzeResult, format: OutputFormat, cwd?: string, paint?: Paint): string;
|
|
122
218
|
//#endregion
|
|
123
219
|
//#region src/report/json.d.ts
|
|
124
220
|
/** Bumped on breaking changes to the JSON report shape. */
|
package/dist/api.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
4
|
-
import { i as JSON_REPORT_VERSION, n as formatResult, t as OUTPUT_FORMATS } from "./report-
|
|
5
|
-
import { r as buildGraph, t as findUnused } from "./unused-
|
|
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 {
|
|
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 {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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
|
|
@@ -20,14 +21,19 @@ function toSource(value) {
|
|
|
20
21
|
if (source === void 0) throw new LayerscopeError(`Unknown source "${value}". Use ${SOURCE_OPTIONS.join(", ")}.`);
|
|
21
22
|
return source;
|
|
22
23
|
}
|
|
23
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Diagnostics go to stderr, so stdout stays machine-readable in every format. Call it after the
|
|
26
|
+
* report is written: notes at the end are the ones a reader still sees when the report scrolled.
|
|
27
|
+
*/
|
|
24
28
|
function writeNotes(result, verbose) {
|
|
25
|
-
const
|
|
29
|
+
const paint = paintFor(process.stderr);
|
|
30
|
+
const lines = result.notes.map((note) => `${paint(["yellow", "bold"], "layerscope: note:")} ${note}`);
|
|
26
31
|
if (verbose) {
|
|
27
32
|
const kind = result.source === "registry" ? "the registry" : "the generated .d.ts files";
|
|
28
|
-
|
|
33
|
+
const source = relative(process.cwd(), result.sourceFile);
|
|
34
|
+
lines.unshift(paint("dim", `layerscope: symbols from ${kind} (${source})`));
|
|
29
35
|
}
|
|
30
|
-
if (lines.length > 0) process.stderr.write(`${lines.join("\n")}\n`);
|
|
36
|
+
if (lines.length > 0) process.stderr.write(`${process.stdout.isTTY ? "\n" : ""}${lines.join("\n")}\n`);
|
|
31
37
|
}
|
|
32
38
|
//#endregion
|
|
33
39
|
//#region src/commands/check.ts
|
|
@@ -38,11 +44,11 @@ async function updateBaseline(rootDir, flags) {
|
|
|
38
44
|
prepare: flags.prepare,
|
|
39
45
|
source: toSource(flags.source)
|
|
40
46
|
});
|
|
41
|
-
writeNotes(result, flags.verbose === true);
|
|
42
47
|
const file = resolve(rootDir, flags.baseline);
|
|
43
48
|
writeBaseline(file, createBaseline(result.findings, rootDir));
|
|
44
49
|
const count = plural(result.findings.length, "finding");
|
|
45
50
|
process.stdout.write(`✔ Wrote ${count} to ${relative(process.cwd(), file)}\n`);
|
|
51
|
+
writeNotes(result, flags.verbose === true);
|
|
46
52
|
return 0;
|
|
47
53
|
}
|
|
48
54
|
async function check(root, flags) {
|
|
@@ -57,8 +63,8 @@ async function check(root, flags) {
|
|
|
57
63
|
source: toSource(flags.source),
|
|
58
64
|
baseline: flags.baseline
|
|
59
65
|
});
|
|
66
|
+
process.stdout.write(formatResult(result, format, process.cwd(), paintFor(process.stdout)));
|
|
60
67
|
writeNotes(result, flags.verbose === true);
|
|
61
|
-
process.stdout.write(formatResult(result, format));
|
|
62
68
|
return result.findings.some((finding) => finding.severity === "error") ? 1 : 0;
|
|
63
69
|
}
|
|
64
70
|
function registerCheck(cli) {
|
|
@@ -162,8 +168,8 @@ async function graph(root, flags) {
|
|
|
162
168
|
prepare: flags.prepare,
|
|
163
169
|
source: toSource(flags.source)
|
|
164
170
|
});
|
|
165
|
-
writeNotes(result, flags.verbose === true);
|
|
166
171
|
process.stdout.write(formatGraph(buildGraph(result, result.config, level), format));
|
|
172
|
+
writeNotes(result, flags.verbose === true);
|
|
167
173
|
return 0;
|
|
168
174
|
}
|
|
169
175
|
function registerGraph(cli) {
|
|
@@ -216,8 +222,8 @@ async function unused(root, flags) {
|
|
|
216
222
|
prepare: flags.prepare,
|
|
217
223
|
source: toSource(flags.source)
|
|
218
224
|
});
|
|
219
|
-
writeNotes(result, flags.verbose === true);
|
|
220
225
|
process.stdout.write(formatUnused(findUnused(result), format));
|
|
226
|
+
writeNotes(result, flags.verbose === true);
|
|
221
227
|
return 0;
|
|
222
228
|
}
|
|
223
229
|
function registerUnused(cli) {
|
|
@@ -331,10 +337,13 @@ async function why(symbol, root, flags) {
|
|
|
331
337
|
prepare: flags.prepare,
|
|
332
338
|
source: toSource(flags.source)
|
|
333
339
|
});
|
|
334
|
-
writeNotes(result, flags.verbose === true);
|
|
335
340
|
const targets = findUses(result, symbol, result.config);
|
|
336
|
-
if (targets.length === 0)
|
|
341
|
+
if (targets.length === 0) {
|
|
342
|
+
writeNotes(result, flags.verbose === true);
|
|
343
|
+
throw new LayerscopeError(`No uses of "${symbol}" found.`);
|
|
344
|
+
}
|
|
337
345
|
process.stdout.write(formatWhy(symbol, targets, format));
|
|
346
|
+
writeNotes(result, flags.verbose === true);
|
|
338
347
|
return 0;
|
|
339
348
|
}
|
|
340
349
|
function registerWhy(cli) {
|
|
@@ -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-
|
|
1
|
+
import { t as defineConfig } from "./define-DDg4HQRH.mjs";
|
|
2
2
|
export { defineConfig };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { u as LayerscopeConfig } from "./define-
|
|
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'];
|