nuxt-layerscope 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.d.mts ADDED
@@ -0,0 +1,148 @@
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";
3
+ //#region src/analyze/index.d.ts
4
+ interface AnalyzeOptions {
5
+ /** Defaults to `process.cwd()`. */
6
+ rootDir?: string;
7
+ /** Defaults to `layerscope.config.*` in the root. */
8
+ configFile?: string;
9
+ /** Used instead of loading a config file. */
10
+ config?: LayerscopeConfig;
11
+ /** Run `nuxi prepare` before reading generated files. */
12
+ prepare?: boolean;
13
+ /** Where symbols come from. Defaults to `auto`: the registry when present, else `.d.ts`. */
14
+ source?: SourceOption;
15
+ /**
16
+ * Baseline file, relative to the root. Findings it lists are moved to `baseline.suppressed`.
17
+ * Ignored when the file does not exist.
18
+ */
19
+ baseline?: string;
20
+ }
21
+ export declare function analyze(options?: AnalyzeOptions): Promise<AnalyzeResult>;
22
+ //#endregion
23
+ //#region src/baseline/index.d.ts
24
+ export declare const BASELINE_FILE = "layerscope-baseline.json";
25
+ /** Bumped on breaking changes to the baseline file. */
26
+ export declare const BASELINE_VERSION = 1;
27
+ interface Baseline {
28
+ version: typeof BASELINE_VERSION;
29
+ entries: BaselineEntry[];
30
+ }
31
+ export declare function createBaseline(findings: Finding[], rootDir: string): Baseline;
32
+ //#endregion
33
+ //#region src/errors.d.ts
34
+ /** A config or input problem; the CLI exits with code 2. */
35
+ export declare class LayerscopeError extends Error {
36
+ name: string;
37
+ }
38
+ //#endregion
39
+ //#region src/rules/edge-status.d.ts
40
+ type EdgeStatus = 'same-layer' | 'external' | 'unrestricted' | 'allowed' | 'not-allowed';
41
+ //#endregion
42
+ //#region src/graph/index.d.ts
43
+ declare const GRAPH_LEVELS: readonly ['layer', 'file'];
44
+ type GraphLevel = (typeof GRAPH_LEVELS)[number];
45
+ interface GraphNode {
46
+ id: string;
47
+ /** Owning layer; for layer graphs the node's own name. */
48
+ layer: string;
49
+ }
50
+ interface GraphEdge {
51
+ from: string;
52
+ to: string;
53
+ /** References behind this edge. */
54
+ count: number;
55
+ /** `not-allowed` when any reference behind it breaks the layer rules. */
56
+ status: EdgeStatus;
57
+ }
58
+ interface Graph {
59
+ level: GraphLevel;
60
+ nodes: GraphNode[];
61
+ edges: GraphEdge[];
62
+ }
63
+ /** Dependencies between layers or files inside layers; packages are left out. */
64
+ export declare function buildGraph(result: AnalyzeResult, config: LayerscopeConfig, level: GraphLevel): Graph;
65
+ //#endregion
66
+ //#region src/registry/schema.d.ts
67
+ /** Bumped on breaking changes to `registry.json`; readers fall back to the `.d.ts` files. */
68
+ export declare const REGISTRY_VERSION = 1;
69
+ /** Relative to the Nuxt build dir. */
70
+ export declare const REGISTRY_FILE = "layerscope/registry.json";
71
+ type ComponentMode = 'all' | 'client' | 'server';
72
+ interface RegistryComponent {
73
+ /** PascalCase name, without the `Lazy` prefix. */
74
+ name: string;
75
+ /** Absolute path, or a bare module specifier for virtual components. */
76
+ file: string;
77
+ mode: ComponentMode;
78
+ island: boolean;
79
+ priority: number;
80
+ }
81
+ /** A component that lost to a higher-priority one with the same name. */
82
+ interface ShadowedComponent extends RegistryComponent {
83
+ /** File of the component Nuxt registered instead. */
84
+ shadowedBy: string;
85
+ }
86
+ interface RegistryComponentDir {
87
+ path: string;
88
+ /** Glob Nuxt scans the dir with, relative to `path`. */
89
+ pattern: string | string[];
90
+ ignore: string[];
91
+ /** Files matched when the registry was written, for the stale check. */
92
+ files: string[];
93
+ }
94
+ interface RegistryImport {
95
+ /** Name the symbol is auto-imported as. */
96
+ name: string;
97
+ /** Absolute path once aliases are resolved, or a bare module specifier. */
98
+ from: string;
99
+ }
100
+ interface Registry {
101
+ version: typeof REGISTRY_VERSION;
102
+ generator: {
103
+ name: string;
104
+ version: string;
105
+ nuxt: string;
106
+ };
107
+ /** Highest priority first, as Nuxt orders `_layers`. */
108
+ layers: Layer[];
109
+ components: RegistryComponent[];
110
+ shadowedComponents: ShadowedComponent[];
111
+ componentDirs: RegistryComponentDir[];
112
+ imports: Record<Context, RegistryImport[]>;
113
+ /** The `layerscope` key of `nuxt.config`, when it sets layers or rules. Added in 0.1.0. */
114
+ config?: ProjectConfig;
115
+ }
116
+ //#endregion
117
+ //#region src/report/index.d.ts
118
+ export declare const OUTPUT_FORMATS: readonly ['text', 'github', 'json'];
119
+ 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;
122
+ //#endregion
123
+ //#region src/report/json.d.ts
124
+ /** Bumped on breaking changes to the JSON report shape. */
125
+ export declare const JSON_REPORT_VERSION = 1;
126
+ //#endregion
127
+ //#region src/unused/index.d.ts
128
+ type UnusedKind = 'component' | 'auto-import';
129
+ interface UnusedSymbol {
130
+ name: string;
131
+ kind: UnusedKind;
132
+ /** Auto-import context; `null` for components. */
133
+ context: Context | null;
134
+ file: string;
135
+ layer: string;
136
+ /**
137
+ * The project renders components chosen at runtime, so a component may still be used
138
+ * without any static reference.
139
+ */
140
+ possiblyUsed: boolean;
141
+ }
142
+ /**
143
+ * Components and auto-imports registered by the project's own layers that nothing references.
144
+ * Layers installed as packages are not checked, so their symbols are never reported.
145
+ */
146
+ export declare function findUnused(result: AnalyzeResult): UnusedSymbol[];
147
+ //#endregion
148
+ export { type AnalyzeOptions, type AnalyzeResult, type Baseline, type BaselineEntry, type BaselineResult, type Context, type Edge, type Finding, type Graph, type GraphEdge, type GraphLevel, type GraphNode, type Layer, type LayerRule, type LayerscopeConfig, type OutputFormat, type Reference, type ReferenceKind, type Registry, type ResolutionSource, type RuleName, type Severity, type SourceOption, type UnusedSymbol, defineConfig };
package/dist/api.mjs ADDED
@@ -0,0 +1,6 @@
1
+ import { g as LayerscopeError, l as REGISTRY_FILE, u as REGISTRY_VERSION } from "./layer-factory-xZmIAPrA.mjs";
2
+ 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-DS51xy6X.mjs";
4
+ import { i as JSON_REPORT_VERSION, n as formatResult, t as OUTPUT_FORMATS } from "./report-BNW0GCl9.mjs";
5
+ import { r as buildGraph, t as findUnused } from "./unused-Dxep1p6u.mjs";
6
+ export { BASELINE_FILE, BASELINE_VERSION, JSON_REPORT_VERSION, LayerscopeError, OUTPUT_FORMATS, REGISTRY_FILE, REGISTRY_VERSION, analyze, buildGraph, createBaseline, defineConfig, findUnused, formatResult };
package/dist/bin.d.mts ADDED
@@ -0,0 +1 @@
1
+ export {}
package/dist/bin.mjs ADDED
@@ -0,0 +1,374 @@
1
+ #!/usr/bin/env node
2
+ import { g as LayerscopeError } from "./layer-factory-xZmIAPrA.mjs";
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-DS51xy6X.mjs";
5
+ import { n as formatResult, r as isOutputFormat, t as OUTPUT_FORMATS } from "./report-BNW0GCl9.mjs";
6
+ import { i as edgeStatus, n as GRAPH_LEVELS, r as buildGraph, t as findUnused } from "./unused-Dxep1p6u.mjs";
7
+ import { relative, resolve } from "pathe";
8
+ import { cac } from "cac";
9
+ //#region src/commands/shared.ts
10
+ const SOURCE_OPTIONS = [
11
+ "auto",
12
+ "registry",
13
+ "types"
14
+ ];
15
+ function withCommonOptions(command) {
16
+ return command.option("--config <file>", "Path to layerscope.config.ts").option("--prepare", "Run \"nuxi prepare\" before checking").option("--source <source>", `Symbol source: ${SOURCE_OPTIONS.join(", ")}`, { default: "auto" }).option("--verbose", "Print where symbols were read from");
17
+ }
18
+ function toSource(value) {
19
+ const source = SOURCE_OPTIONS.find((option) => option === value);
20
+ if (source === void 0) throw new LayerscopeError(`Unknown source "${value}". Use ${SOURCE_OPTIONS.join(", ")}.`);
21
+ return source;
22
+ }
23
+ /** Diagnostics go to stderr, so stdout stays machine-readable in every format. */
24
+ function writeNotes(result, verbose) {
25
+ const lines = result.notes.map((note) => `layerscope: note: ${note}`);
26
+ if (verbose) {
27
+ const kind = result.source === "registry" ? "the registry" : "the generated .d.ts files";
28
+ lines.unshift(`layerscope: symbols from ${kind} (${relative(process.cwd(), result.sourceFile)})`);
29
+ }
30
+ if (lines.length > 0) process.stderr.write(`${lines.join("\n")}\n`);
31
+ }
32
+ //#endregion
33
+ //#region src/commands/check.ts
34
+ async function updateBaseline(rootDir, flags) {
35
+ const result = await analyze({
36
+ rootDir,
37
+ configFile: flags.config,
38
+ prepare: flags.prepare,
39
+ source: toSource(flags.source)
40
+ });
41
+ writeNotes(result, flags.verbose === true);
42
+ const file = resolve(rootDir, flags.baseline);
43
+ writeBaseline(file, createBaseline(result.findings, rootDir));
44
+ const count = plural(result.findings.length, "finding");
45
+ process.stdout.write(`✔ Wrote ${count} to ${relative(process.cwd(), file)}\n`);
46
+ return 0;
47
+ }
48
+ async function check(root, flags) {
49
+ const { format } = flags;
50
+ if (!isOutputFormat(format)) throw new LayerscopeError(`Unknown format "${format}". Use ${OUTPUT_FORMATS.join(", ")}.`);
51
+ const rootDir = resolve(root ?? process.cwd());
52
+ if (flags.updateBaseline === true) return updateBaseline(rootDir, flags);
53
+ const result = await analyze({
54
+ rootDir,
55
+ configFile: flags.config,
56
+ prepare: flags.prepare,
57
+ source: toSource(flags.source),
58
+ baseline: flags.baseline
59
+ });
60
+ writeNotes(result, flags.verbose === true);
61
+ process.stdout.write(formatResult(result, format));
62
+ return result.findings.some((finding) => finding.severity === "error") ? 1 : 0;
63
+ }
64
+ function registerCheck(cli) {
65
+ withCommonOptions(cli.command("check [root]", "Check layer boundaries, including auto-imports").option("--format <format>", `Output format: ${OUTPUT_FORMATS.join(", ")}`, { default: "text" }).option("--baseline <file>", "Findings to accept, relative to the root", { default: BASELINE_FILE }).option("--update-baseline", "Write every current finding to the baseline file")).action(check);
66
+ }
67
+ //#endregion
68
+ //#region src/report/graph/dot.ts
69
+ function quote(text) {
70
+ return `"${text.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
71
+ }
72
+ function edgeLine$1(edge) {
73
+ const attributes = [...edge.count > 1 ? [`label=${edge.count}`] : [], ...edge.status === "not-allowed" ? ["color=red", "fontcolor=red"] : []];
74
+ const suffix = attributes.length > 0 ? ` [${attributes.join(", ")}]` : "";
75
+ return ` ${quote(edge.from)} -> ${quote(edge.to)}${suffix};`;
76
+ }
77
+ function nodeLines$1(graph) {
78
+ if (graph.level === "layer") return graph.nodes.map((node) => ` ${quote(node.id)};`);
79
+ const lines = [];
80
+ for (const [index, [layer, nodes]] of [...Map.groupBy(graph.nodes, (node) => node.layer)].entries()) lines.push(` subgraph cluster_${index} {`, ` label=${quote(layer)};`, ...nodes.map((node) => ` ${quote(node.id)};`), " }");
81
+ return lines;
82
+ }
83
+ /** Graphviz DOT; edges that break the layer rules are red. */
84
+ function formatGraphDot(graph) {
85
+ return `${[
86
+ "digraph layerscope {",
87
+ " rankdir=LR;",
88
+ ...nodeLines$1(graph),
89
+ ...graph.edges.map((edge) => edgeLine$1(edge)),
90
+ "}"
91
+ ].join("\n")}\n`;
92
+ }
93
+ //#endregion
94
+ //#region src/report/graph/json.ts
95
+ function formatGraphJson(graph) {
96
+ return `${JSON.stringify({
97
+ version: 1,
98
+ ...graph
99
+ }, null, 2)}\n`;
100
+ }
101
+ //#endregion
102
+ //#region src/report/graph/mermaid.ts
103
+ const VIOLATION_STYLE = "stroke:#d73a49,stroke-width:2px";
104
+ function label(text) {
105
+ return `"${text.replaceAll("\"", "#quot;")}"`;
106
+ }
107
+ function edgeLine(edge, ids) {
108
+ const arrow = edge.count > 1 ? `-- ${edge.count} -->` : "-->";
109
+ return ` ${ids.get(edge.from)} ${arrow} ${ids.get(edge.to)}`;
110
+ }
111
+ function nodeLines(graph, ids) {
112
+ if (graph.level === "layer") return graph.nodes.map((node) => ` ${ids.get(node.id)}[${label(node.id)}]`);
113
+ const lines = [];
114
+ for (const [index, [layer, nodes]] of [...Map.groupBy(graph.nodes, (node) => node.layer)].entries()) lines.push(` subgraph layer${index}[${label(layer)}]`, ...nodes.map((node) => ` ${ids.get(node.id)}[${label(node.id)}]`), " end");
115
+ return lines;
116
+ }
117
+ /** A Mermaid flowchart; edges that break the layer rules are drawn in red. */
118
+ function formatGraphMermaid(graph) {
119
+ const ids = new Map(graph.nodes.map((node, index) => [node.id, `n${index}`]));
120
+ const violations = graph.edges.flatMap((edge, index) => edge.status === "not-allowed" ? [index] : []);
121
+ const lines = [
122
+ "flowchart LR",
123
+ ...nodeLines(graph, ids),
124
+ ...graph.edges.map((edge) => edgeLine(edge, ids))
125
+ ];
126
+ if (violations.length > 0) lines.push(` linkStyle ${violations.join(",")} ${VIOLATION_STYLE}`);
127
+ return `${lines.join("\n")}\n`;
128
+ }
129
+ //#endregion
130
+ //#region src/report/graph/index.ts
131
+ const GRAPH_FORMATS = [
132
+ "mermaid",
133
+ "dot",
134
+ "json"
135
+ ];
136
+ const formatters = {
137
+ mermaid: formatGraphMermaid,
138
+ dot: formatGraphDot,
139
+ json: formatGraphJson
140
+ };
141
+ function isGraphFormat(value) {
142
+ return Object.hasOwn(formatters, value);
143
+ }
144
+ function formatGraph(graph, format) {
145
+ return formatters[format](graph);
146
+ }
147
+ //#endregion
148
+ //#region src/commands/graph.ts
149
+ function toLevel(value) {
150
+ const level = GRAPH_LEVELS.find((option) => option === value);
151
+ if (level === void 0) throw new LayerscopeError(`Unknown level "${value}". Use ${GRAPH_LEVELS.join(", ")}.`);
152
+ return level;
153
+ }
154
+ async function graph(root, flags) {
155
+ const { format } = flags;
156
+ if (!isGraphFormat(format)) throw new LayerscopeError(`Unknown format "${format}". Use ${GRAPH_FORMATS.join(", ")}.`);
157
+ const level = toLevel(flags.by);
158
+ const rootDir = resolve(root ?? process.cwd());
159
+ const result = await analyze({
160
+ rootDir,
161
+ configFile: flags.config,
162
+ prepare: flags.prepare,
163
+ source: toSource(flags.source)
164
+ });
165
+ writeNotes(result, flags.verbose === true);
166
+ process.stdout.write(formatGraph(buildGraph(result, result.config, level), format));
167
+ return 0;
168
+ }
169
+ function registerGraph(cli) {
170
+ withCommonOptions(cli.command("graph [root]", "Print the dependency graph between layers or files").option("--format <format>", `Output format: ${GRAPH_FORMATS.join(", ")}`, { default: "mermaid" }).option("--by <level>", `Graph level: ${GRAPH_LEVELS.join(", ")}`, { default: "layer" })).action(graph);
171
+ }
172
+ //#endregion
173
+ //#region src/report/unused.ts
174
+ const UNUSED_FORMATS = ["text", "json"];
175
+ function isUnusedFormat(value) {
176
+ return UNUSED_FORMATS.includes(value);
177
+ }
178
+ function describe(symbol) {
179
+ if (symbol.kind === "component") return symbol.possiblyUsed ? "component (possibly used at runtime)" : "component";
180
+ return `auto-import (${symbol.context ?? "app"})`;
181
+ }
182
+ function formatUnusedText(unused, cwd) {
183
+ if (unused.length === 0) return "✔ Every component and auto-import is referenced\n";
184
+ const rows = unused.map((symbol) => ({
185
+ symbol,
186
+ file: relative(cwd, symbol.file)
187
+ }));
188
+ const nameWidth = Math.max(...rows.map((row) => row.symbol.name.length));
189
+ const fileWidth = Math.max(...rows.map((row) => row.file.length));
190
+ const byLayer = Map.groupBy(rows, (row) => row.symbol.layer);
191
+ const sections = [...byLayer].map(([layer, layerRows]) => [layer, ...layerRows.map(({ symbol, file }) => ` ${symbol.name.padEnd(nameWidth)} ${file.padEnd(fileWidth)} ${describe(symbol)}`)].join("\n"));
192
+ const footer = `${plural(unused.length, "unused symbol")} in ${plural(byLayer.size, "layer")}`;
193
+ return `${[...sections, footer].join("\n\n")}\n`;
194
+ }
195
+ function formatUnusedJson(unused, cwd) {
196
+ const report = {
197
+ version: 1,
198
+ unused: unused.map((symbol) => ({
199
+ ...symbol,
200
+ file: relative(cwd, symbol.file)
201
+ }))
202
+ };
203
+ return `${JSON.stringify(report, null, 2)}\n`;
204
+ }
205
+ function formatUnused(unused, format, cwd = process.cwd()) {
206
+ return format === "json" ? formatUnusedJson(unused, cwd) : formatUnusedText(unused, cwd);
207
+ }
208
+ //#endregion
209
+ //#region src/commands/unused.ts
210
+ async function unused(root, flags) {
211
+ const { format } = flags;
212
+ if (!isUnusedFormat(format)) throw new LayerscopeError(`Unknown format "${format}". Use ${UNUSED_FORMATS.join(", ")}.`);
213
+ const result = await analyze({
214
+ rootDir: resolve(root ?? process.cwd()),
215
+ configFile: flags.config,
216
+ prepare: flags.prepare,
217
+ source: toSource(flags.source)
218
+ });
219
+ writeNotes(result, flags.verbose === true);
220
+ process.stdout.write(formatUnused(findUnused(result), format));
221
+ return 0;
222
+ }
223
+ function registerUnused(cli) {
224
+ withCommonOptions(cli.command("unused [root]", "List components and auto-imports nothing references").option("--format <format>", `Output format: ${UNUSED_FORMATS.join(", ")}`, { default: "text" })).action(unused);
225
+ }
226
+ //#endregion
227
+ //#region src/report/why.ts
228
+ const WHY_FORMATS = ["text", "json"];
229
+ const STATUS_LABELS = {
230
+ "same-layer": "✔ same layer",
231
+ external: "✔ external",
232
+ unrestricted: "✔ unrestricted",
233
+ allowed: "✔ allowed",
234
+ "not-allowed": "✖ not allowed"
235
+ };
236
+ function isWhyFormat(value) {
237
+ return WHY_FORMATS.includes(value);
238
+ }
239
+ function describeTarget(target, cwd) {
240
+ if (target.file === null) return `${target.external ?? "unknown"} (external)`;
241
+ return `${relative(cwd, target.file)} (${target.layer ?? "no layer"})`;
242
+ }
243
+ function formatTargetText(target, cwd) {
244
+ const rows = target.uses.map(({ edge, status }) => ({
245
+ location: `${relative(cwd, edge.file)}:${edge.line}:${edge.column}`,
246
+ path: `${edge.fromLayer} → ${edge.toLayer ?? edge.external ?? "unknown"}`,
247
+ status: STATUS_LABELS[status]
248
+ }));
249
+ const locationWidth = Math.max(...rows.map((row) => row.location.length));
250
+ const pathWidth = Math.max(...rows.map((row) => row.path.length));
251
+ const lines = rows.map((row) => ` ${row.location.padEnd(locationWidth)} ${row.path.padEnd(pathWidth)} ${row.status}`);
252
+ return [`${target.symbol} → ${describeTarget(target, cwd)}`, ...lines].join("\n");
253
+ }
254
+ function formatWhyJson(query, targets, cwd) {
255
+ const report = {
256
+ version: 1,
257
+ symbol: query,
258
+ targets: targets.map((target) => ({
259
+ symbol: target.symbol,
260
+ file: target.file === null ? null : relative(cwd, target.file),
261
+ layer: target.layer,
262
+ external: target.external,
263
+ uses: target.uses.map(({ edge, status }) => ({
264
+ file: relative(cwd, edge.file),
265
+ line: edge.line,
266
+ column: edge.column,
267
+ kind: edge.kind,
268
+ symbol: edge.symbol,
269
+ fromLayer: edge.fromLayer,
270
+ toLayer: edge.toLayer,
271
+ status
272
+ }))
273
+ }))
274
+ };
275
+ return `${JSON.stringify(report, null, 2)}\n`;
276
+ }
277
+ function formatWhy(query, targets, format, cwd = process.cwd()) {
278
+ if (format === "json") return formatWhyJson(query, targets, cwd);
279
+ return `${targets.map((target) => formatTargetText(target, cwd)).join("\n\n")}\n`;
280
+ }
281
+ //#endregion
282
+ //#region src/why/index.ts
283
+ /** `LazyBaseButton` and `base-button` both name `BaseButton`. */
284
+ function componentName(name) {
285
+ const pascal = pascalCase(name);
286
+ return /^Lazy[A-Z]/u.test(pascal) ? pascal.slice(4) : pascal;
287
+ }
288
+ /** The name an edge is known by: `#imports:useCart` is `useCart`. */
289
+ function edgeName(edge) {
290
+ const match = /^#(?:imports|components):(.+)$/u.exec(edge.symbol);
291
+ return match === null ? edge.symbol : match[1];
292
+ }
293
+ function matches(edge, query) {
294
+ if (edge.symbol === query) return true;
295
+ const name = edgeName(edge);
296
+ if (name === query) return true;
297
+ return (edge.kind === "component" || edge.symbol.startsWith("#components:")) && componentName(name) === componentName(query);
298
+ }
299
+ /** Every use of a symbol (auto-import, component or import specifier), in report order. */
300
+ function findUses(result, query, config) {
301
+ const targets = /* @__PURE__ */ new Map();
302
+ for (const edge of result.edges.filter((candidate) => matches(candidate, query))) {
303
+ const key = edge.to ?? `external:${edge.external}`;
304
+ let target = targets.get(key);
305
+ if (target === void 0) {
306
+ target = {
307
+ symbol: edge.kind === "component" ? componentName(query) : query,
308
+ file: edge.to,
309
+ layer: edge.toLayer,
310
+ external: edge.external,
311
+ uses: []
312
+ };
313
+ targets.set(key, target);
314
+ }
315
+ target.uses.push({
316
+ edge,
317
+ status: edgeStatus(edge, config)
318
+ });
319
+ }
320
+ return [...targets.values()];
321
+ }
322
+ //#endregion
323
+ //#region src/commands/why.ts
324
+ async function why(symbol, root, flags) {
325
+ const { format } = flags;
326
+ if (!isWhyFormat(format)) throw new LayerscopeError(`Unknown format "${format}". Use ${WHY_FORMATS.join(", ")}.`);
327
+ const rootDir = resolve(root ?? process.cwd());
328
+ const result = await analyze({
329
+ rootDir,
330
+ configFile: flags.config,
331
+ prepare: flags.prepare,
332
+ source: toSource(flags.source)
333
+ });
334
+ writeNotes(result, flags.verbose === true);
335
+ const targets = findUses(result, symbol, result.config);
336
+ if (targets.length === 0) throw new LayerscopeError(`No uses of "${symbol}" found.`);
337
+ process.stdout.write(formatWhy(symbol, targets, format));
338
+ return 0;
339
+ }
340
+ function registerWhy(cli) {
341
+ withCommonOptions(cli.command("why <symbol> [root]", "List every use of a symbol and the layers it crosses").option("--format <format>", `Output format: ${WHY_FORMATS.join(", ")}`, { default: "text" })).action(why);
342
+ }
343
+ //#endregion
344
+ //#region src/cli.ts
345
+ function createCli() {
346
+ const cli = cac("layerscope");
347
+ registerCheck(cli);
348
+ registerWhy(cli);
349
+ registerGraph(cli);
350
+ registerUnused(cli);
351
+ cli.help();
352
+ return cli;
353
+ }
354
+ /** Runs the CLI and resolves to its exit code. */
355
+ async function run(argv = process.argv) {
356
+ const cli = createCli();
357
+ try {
358
+ cli.parse(argv, { run: false });
359
+ if (cli.matchedCommand === void 0) {
360
+ if (cli.options.help === true) return 0;
361
+ cli.outputHelp();
362
+ return 2;
363
+ }
364
+ return await cli.runMatchedCommand();
365
+ } catch (error) {
366
+ process.stderr.write(`layerscope: ${error.message}\n`);
367
+ return 2;
368
+ }
369
+ }
370
+ //#endregion
371
+ //#region src/bin.ts
372
+ process.exitCode = await run();
373
+ //#endregion
374
+ export {};
@@ -0,0 +1,12 @@
1
+ import { glob } from "tinyglobby";
2
+ //#region src/registry/component-files.ts
3
+ /** Files in a component dir, matched the way Nuxt's components module scans it. */
4
+ async function globComponents(dir, pattern, ignore) {
5
+ return (await glob(pattern, {
6
+ cwd: dir,
7
+ absolute: true,
8
+ ignore
9
+ })).toSorted();
10
+ }
11
+ //#endregion
12
+ export { globComponents as t };
@@ -0,0 +1,126 @@
1
+ //#region src/nuxt/symbols.d.ts
2
+ interface SymbolTarget {
3
+ /** Source file, when there is one; a layer that owns it wins over `module`. */
4
+ file: string | null;
5
+ /** Package or virtual module, used when no layer owns `file`. */
6
+ module: string | null;
7
+ }
8
+ type SymbolMap = Map<string, SymbolTarget>;
9
+ interface SymbolTable {
10
+ imports: Record<Context, SymbolMap>;
11
+ components: SymbolMap;
12
+ }
13
+ //#endregion
14
+ //#region src/types.d.ts
15
+ type Severity = 'off' | 'warn' | 'error';
16
+ type RuleName = 'layer-boundary' | 'unresolved-reference' | 'shadowed-component';
17
+ /** The Nuxt auto-import context a source file runs in. */
18
+ type Context = 'app' | 'server' | 'shared';
19
+ interface Layer {
20
+ name: string;
21
+ root: string;
22
+ srcDir: string;
23
+ serverDir: string;
24
+ sharedDir: string;
25
+ /** Whether the layer uses Nuxt's default `<srcDir>/components` dir. */
26
+ defaultComponents: boolean;
27
+ }
28
+ type ReferenceKind = 'auto-import' | 'component' | 'import';
29
+ interface Reference {
30
+ file: string;
31
+ line: number;
32
+ column: number;
33
+ kind: ReferenceKind;
34
+ /** Identifier, component name or import specifier. */
35
+ symbol: string;
36
+ }
37
+ interface Edge extends Reference {
38
+ fromLayer: string;
39
+ /** `null` when the target is outside every layer. */
40
+ to: string | null;
41
+ toLayer: string | null;
42
+ /** Package or virtual module name for targets outside every layer. */
43
+ external: string | null;
44
+ }
45
+ interface Finding {
46
+ rule: RuleName;
47
+ severity: Exclude<Severity, 'off'>;
48
+ file: string;
49
+ line: number;
50
+ column: number;
51
+ symbol: string;
52
+ fromLayer: string;
53
+ toLayer: string | null;
54
+ /** File the symbol resolves to, when known. */
55
+ target: string | null;
56
+ /** Layers `fromLayer` may depend on, for `layer-boundary` findings. */
57
+ allowed?: string[];
58
+ message: string;
59
+ }
60
+ interface LayerRule {
61
+ /** Layers this layer may depend on. Layers missing from config are unrestricted. */
62
+ allow?: string[];
63
+ /** Layer root relative to the project root; names a layer or declares it without @nuxt/kit. */
64
+ path?: string;
65
+ /** The `extends` source of a remote layer (`github:org/repo`); names the layer c12 cloned from it. */
66
+ source?: string;
67
+ }
68
+ interface LayerscopeConfig {
69
+ layers?: Record<string, LayerRule>;
70
+ rules?: Partial<Record<RuleName | 'unused-symbol', Severity>>;
71
+ /** Globs relative to each scanned dir. */
72
+ ignore?: string[];
73
+ /** Identifiers and component names never reported as unresolved. */
74
+ globals?: string[];
75
+ /** Relative to the project root. Defaults to `.nuxt`. */
76
+ buildDir?: string;
77
+ }
78
+ /**
79
+ * `registry`: `.nuxt/layerscope/registry.json`, written by the `nuxt-layerscope` Nuxt
80
+ * module. `types`: the generated `.d.ts` files, used when the module is not installed.
81
+ */
82
+ type ResolutionSource = 'registry' | 'types';
83
+ /** `auto` reads the registry when the Nuxt module wrote one, and the `.d.ts` files otherwise. */
84
+ type SourceOption = ResolutionSource | 'auto';
85
+ /** A finding counts by rule, file, symbol and target layer, never by line. */
86
+ interface BaselineEntry {
87
+ rule: RuleName;
88
+ /** Relative to the project root. */
89
+ file: string;
90
+ symbol: string;
91
+ toLayer: string | null;
92
+ /** How often the key occurs; defaults to 1. */
93
+ count?: number;
94
+ }
95
+ interface BaselineResult {
96
+ file: string;
97
+ /** Findings present in the baseline, left out of `findings`. */
98
+ suppressed: Finding[];
99
+ /** Entries that no longer occur and can be removed with `--update-baseline`. */
100
+ removable: BaselineEntry[];
101
+ }
102
+ interface AnalyzeResult {
103
+ rootDir: string;
104
+ /** The config that applied: `layerscope.config.*` or the `layerscope` key of `nuxt.config`. */
105
+ config: LayerscopeConfig;
106
+ source: ResolutionSource;
107
+ /** File the symbols were read from. */
108
+ sourceFile: string;
109
+ layers: Layer[];
110
+ files: string[];
111
+ edges: Edge[];
112
+ findings: Finding[];
113
+ /** Things the user should know about the run, such as rules that could not run. */
114
+ notes: string[];
115
+ /** Every auto-import and component Nuxt registered, by context. */
116
+ symbols: SymbolTable;
117
+ /** Files rendering a component chosen at runtime (`<component :is>`, `resolveComponent(x)`). */
118
+ dynamicComponentFiles: string[];
119
+ /** Set when a baseline was applied; `findings` then holds only the new ones. */
120
+ baseline?: BaselineResult;
121
+ }
122
+ //#endregion
123
+ //#region src/config/define.d.ts
124
+ declare function defineConfig(config: LayerscopeConfig): LayerscopeConfig;
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 };
@@ -0,0 +1,6 @@
1
+ //#region src/config/define.ts
2
+ function defineConfig(config) {
3
+ return config;
4
+ }
5
+ //#endregion
6
+ export { defineConfig as t };
@@ -0,0 +1,2 @@
1
+ import { t as defineConfig } from "./define-DNRGLVG_.mjs";
2
+ export { defineConfig };
@@ -0,0 +1,2 @@
1
+ import { t as defineConfig } from "./define-l62B9fVk.mjs";
2
+ export { defineConfig };