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.
@@ -0,0 +1,451 @@
1
+ import { E as LayerscopeError, S as validateConfig, T as LayerConfigError, _ as readJson, a as isClonedFrom, d as resolveFile, i as layerFromPath, n as deriveDirectories, o as REGISTRY_FILE, r as layerFromNuxt, t as ROOT_LAYER_NAME, u as packageName, v as realPath, w as ruleSeverity } from "./layer-factory-DVSf2Wj4.mjs";
2
+ import { dirname, isAbsolute, join, resolve } from "pathe";
3
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { createJiti } from "jiti";
6
+ import globals from "globals";
7
+ //#region src/config/load.ts
8
+ const CONFIG_NAMES = [
9
+ "layerscope.config.ts",
10
+ "layerscope.config.mts",
11
+ "layerscope.config.js",
12
+ "layerscope.config.mjs"
13
+ ];
14
+ /**
15
+ * Config files import `defineConfig` from this package. Aliasing it to our own module lets
16
+ * them load even when layerscope runs through npx without being a project dependency.
17
+ */
18
+ function defineConfigModule() {
19
+ const candidates = ["./define.ts", "./define.mjs"].map((path) => fileURLToPath(new URL(path, import.meta.url)));
20
+ return candidates.find((path) => existsSync(path)) ?? candidates[0];
21
+ }
22
+ function findConfigFile(rootDir, configFile) {
23
+ if (configFile !== void 0) {
24
+ const file = resolve(process.cwd(), configFile);
25
+ if (!existsSync(file)) throw new LayerscopeError(`Config file not found: ${file}`);
26
+ return file;
27
+ }
28
+ return CONFIG_NAMES.map((name) => resolve(rootDir, name)).find((path) => existsSync(path)) ?? null;
29
+ }
30
+ function createLoader() {
31
+ const self = defineConfigModule();
32
+ return createJiti(import.meta.url, {
33
+ alias: {
34
+ "nuxt-layerscope": self,
35
+ layerscope: self
36
+ },
37
+ moduleCache: false
38
+ });
39
+ }
40
+ function toConfig(value, file) {
41
+ if (typeof value !== "object" || value === null) throw new LayerscopeError(`${file} must default-export a config object`);
42
+ validateConfig(value, file);
43
+ return value;
44
+ }
45
+ function loadFailed(file, error) {
46
+ return new LayerscopeError(`Failed to load ${file}: ${error.message}`);
47
+ }
48
+ async function loadConfig(rootDir, configFile) {
49
+ const file = findConfigFile(rootDir, configFile);
50
+ if (file === null) return {};
51
+ return toConfig(await createLoader().import(file, { default: true }).catch((error) => {
52
+ throw loadFailed(file, error);
53
+ }), file);
54
+ }
55
+ function requireConfig(file) {
56
+ try {
57
+ return createLoader()(file);
58
+ } catch (error) {
59
+ throw loadFailed(file, error);
60
+ }
61
+ }
62
+ /** For callers that cannot await, such as lint rules. */
63
+ function loadConfigSync(rootDir) {
64
+ const file = findConfigFile(rootDir);
65
+ if (file === null) return {};
66
+ const module = requireConfig(file);
67
+ return toConfig(module?.default ?? module, file);
68
+ }
69
+ //#endregion
70
+ //#region src/rules/layer-boundary.ts
71
+ function describeEdge(edge) {
72
+ if (edge.kind === "component") return `Component <${edge.symbol}>`;
73
+ return edge.kind === "import" ? `Import "${edge.symbol}"` : `Auto-import "${edge.symbol}"`;
74
+ }
75
+ /** Edges to packages and within a layer are always allowed. */
76
+ function isAllowed(edge, allow) {
77
+ return edge.toLayer === null || edge.toLayer === edge.fromLayer || allow.includes(edge.toLayer);
78
+ }
79
+ function boundaryFindings(edges, config) {
80
+ const severity = ruleSeverity(config, "layer-boundary");
81
+ if (severity === "off") return [];
82
+ return edges.flatMap((edge) => {
83
+ const allow = config.layers?.[edge.fromLayer]?.allow;
84
+ if (allow === void 0 || isAllowed(edge, allow)) return [];
85
+ return [{
86
+ rule: "layer-boundary",
87
+ severity,
88
+ file: edge.file,
89
+ line: edge.line,
90
+ column: edge.column,
91
+ symbol: edge.symbol,
92
+ fromLayer: edge.fromLayer,
93
+ toLayer: edge.toLayer,
94
+ target: edge.to,
95
+ allowed: allow,
96
+ message: `${describeEdge(edge)} crosses from layer "${edge.fromLayer}" into "${edge.toLayer}"`
97
+ }];
98
+ });
99
+ }
100
+ //#endregion
101
+ //#region src/nuxt/symbols.ts
102
+ const GLOBAL_CONST = /^ {2}const ([\w$]+): (.*)$/u;
103
+ const EXPORT_CONST = /^export const ([\w$]+): (.*)$/u;
104
+ const INTERFACE_MEMBER = /^ {2}([\w$]+): (.*)$/u;
105
+ const TYPEOF_IMPORT = /typeof import\((['"])(.+?)\1\)/u;
106
+ const INLINE_TYPE = {
107
+ file: null,
108
+ module: "nuxt"
109
+ };
110
+ /** Target of an absolute path or a bare module specifier. */
111
+ function targetOf(source, buildDir) {
112
+ if (!isAbsolute(source)) return {
113
+ file: null,
114
+ module: packageName(source)
115
+ };
116
+ if (source === buildDir || source.startsWith(`${buildDir}/`)) return {
117
+ file: null,
118
+ module: "#build"
119
+ };
120
+ if (source.includes("/node_modules/")) {
121
+ const file = resolveFile(source);
122
+ return {
123
+ file: file === null ? null : realPath(file),
124
+ module: packageName(source)
125
+ };
126
+ }
127
+ return {
128
+ file: realPath(resolveFile(source) ?? source),
129
+ module: null
130
+ };
131
+ }
132
+ function toTarget(declaration, fromDir, buildDir) {
133
+ const specifier = TYPEOF_IMPORT.exec(declaration)?.[2];
134
+ if (specifier === void 0) return INLINE_TYPE;
135
+ return targetOf(specifier.startsWith(".") || specifier.startsWith("/") ? resolve(fromDir, specifier) : specifier, buildDir);
136
+ }
137
+ /** Only `declare global { ... }` blocks hold auto-imports; the rest are type augmentations. */
138
+ function parseGlobalDeclarations(file, buildDir) {
139
+ const table = /* @__PURE__ */ new Map();
140
+ if (!existsSync(file)) return table;
141
+ let inGlobal = false;
142
+ for (const line of readFileSync(file, "utf8").split("\n")) {
143
+ if (line.startsWith("declare global") || line.startsWith("}")) {
144
+ inGlobal = line.startsWith("declare global");
145
+ continue;
146
+ }
147
+ const match = inGlobal ? GLOBAL_CONST.exec(line) : null;
148
+ if (match) table.set(match[1], toTarget(match[2], dirname(file), buildDir));
149
+ }
150
+ return table;
151
+ }
152
+ /** `.nuxt/components.d.ts`: one `export const` per component. */
153
+ function parseComponentExports(file, buildDir) {
154
+ const table = /* @__PURE__ */ new Map();
155
+ for (const line of readFileSync(file, "utf8").split("\n")) {
156
+ const match = EXPORT_CONST.exec(line);
157
+ if (match && TYPEOF_IMPORT.test(match[2])) table.set(match[1], toTarget(match[2], dirname(file), buildDir));
158
+ }
159
+ return table;
160
+ }
161
+ /** `.nuxt/types/components.d.ts` (Nuxt 4.1+): members of `interface _GlobalComponents`. */
162
+ function parseComponentInterface(file, buildDir) {
163
+ const table = /* @__PURE__ */ new Map();
164
+ let inInterface = false;
165
+ for (const line of readFileSync(file, "utf8").split("\n")) {
166
+ if (line.startsWith("interface _GlobalComponents") || line.startsWith("}")) {
167
+ inInterface = line.startsWith("interface _GlobalComponents");
168
+ continue;
169
+ }
170
+ const match = inInterface ? INTERFACE_MEMBER.exec(line) : null;
171
+ if (match && TYPEOF_IMPORT.test(match[2])) table.set(match[1], toTarget(match[2], dirname(file), buildDir));
172
+ }
173
+ return table;
174
+ }
175
+ function loadComponents(buildDir) {
176
+ const root = join(buildDir, "components.d.ts");
177
+ if (existsSync(root)) return parseComponentExports(root, buildDir);
178
+ const types = join(buildDir, "types/components.d.ts");
179
+ return existsSync(types) ? parseComponentInterface(types, buildDir) : null;
180
+ }
181
+ function sameTarget(a, b) {
182
+ return a.file === b.file && a.module === b.module;
183
+ }
184
+ /**
185
+ * Nuxt 3 writes no `shared-imports.d.ts`; Nuxt 4 derives it from the imports both contexts
186
+ * share, so the same rule applies here.
187
+ */
188
+ function loadSharedImports(buildDir, app, server) {
189
+ const file = join(buildDir, "types/shared-imports.d.ts");
190
+ if (existsSync(file)) return parseGlobalDeclarations(file, buildDir);
191
+ return new Map([...app].filter(([name, target]) => {
192
+ const other = server.get(name);
193
+ return other !== void 0 && sameTarget(target, other);
194
+ }));
195
+ }
196
+ function loadSymbolTable(buildDir) {
197
+ const app = join(buildDir, "types/imports.d.ts");
198
+ const components = existsSync(app) ? loadComponents(buildDir) : null;
199
+ if (components === null) {
200
+ const missing = existsSync(app) ? join(buildDir, "components.d.ts") : app;
201
+ throw new LayerscopeError(`${missing} not found. Run "nuxi prepare" first, or pass --prepare.`);
202
+ }
203
+ const appImports = parseGlobalDeclarations(app, buildDir);
204
+ const serverImports = parseGlobalDeclarations(join(buildDir, "types/nitro-imports.d.ts"), buildDir);
205
+ return {
206
+ imports: {
207
+ app: appImports,
208
+ server: serverImports,
209
+ shared: loadSharedImports(buildDir, appImports, serverImports)
210
+ },
211
+ components
212
+ };
213
+ }
214
+ //#endregion
215
+ //#region src/registry/read.ts
216
+ function readRegistry(buildDir) {
217
+ const file = join(buildDir, REGISTRY_FILE);
218
+ if (!existsSync(file)) return {
219
+ kind: "missing",
220
+ file
221
+ };
222
+ const registry = readJson(file);
223
+ if (registry.version !== 1) return {
224
+ kind: "incompatible",
225
+ file,
226
+ version: registry.version
227
+ };
228
+ return {
229
+ kind: "ok",
230
+ file,
231
+ registry
232
+ };
233
+ }
234
+ function importMap(imports, buildDir) {
235
+ return new Map(imports.map((entry) => [entry.name, targetOf(entry.from, buildDir)]));
236
+ }
237
+ /** The same table the `.d.ts` parser builds, from Nuxt's own registry. */
238
+ function tableFromRegistry(registry, buildDir) {
239
+ const components = /* @__PURE__ */ new Map();
240
+ const lazy = /* @__PURE__ */ new Map();
241
+ for (const component of registry.components) {
242
+ const target = targetOf(component.file, buildDir);
243
+ components.set(component.name, target);
244
+ lazy.set(`Lazy${component.name}`, target);
245
+ }
246
+ return {
247
+ imports: {
248
+ app: importMap(registry.imports.app, buildDir),
249
+ server: importMap(registry.imports.server, buildDir),
250
+ shared: importMap(registry.imports.shared, buildDir)
251
+ },
252
+ components: new Map([...components, ...lazy])
253
+ };
254
+ }
255
+ //#endregion
256
+ //#region src/nuxt/aliases.ts
257
+ const TSCONFIG_FILES = [
258
+ "tsconfig.app.json",
259
+ "tsconfig.json",
260
+ "tsconfig.server.json",
261
+ "tsconfig.shared.json"
262
+ ];
263
+ /** Nuxt's alias map, as written to the generated tsconfig files. */
264
+ function loadAliases(buildDir) {
265
+ const map = /* @__PURE__ */ new Map();
266
+ const files = TSCONFIG_FILES.map((name) => join(buildDir, name)).filter((file) => existsSync(file));
267
+ for (const file of files) {
268
+ const paths = readJson(file).compilerOptions?.paths ?? {};
269
+ for (const [key, targets] of Object.entries(paths)) {
270
+ const alias = key.replace(/\/\*$/u, "");
271
+ const target = targets.at(0)?.replace(/\/\*$/u, "");
272
+ if (target !== void 0 && !map.has(alias)) {
273
+ const isPath = isAbsolute(target) || target.startsWith(".");
274
+ map.set(alias, isPath ? resolve(buildDir, target) : target);
275
+ }
276
+ }
277
+ }
278
+ map.set("#build", buildDir);
279
+ return [...map].toSorted((a, b) => b[0].length - a[0].length);
280
+ }
281
+ //#endregion
282
+ //#region src/analyze/known-globals.ts
283
+ const SFC_MACROS = [
284
+ "defineProps",
285
+ "defineEmits",
286
+ "defineExpose",
287
+ "defineOptions",
288
+ "defineSlots",
289
+ "defineModel",
290
+ "withDefaults"
291
+ ];
292
+ /** Defined on `globalThis` by Nuxt (app) and Nitro (server), so absent from the registry there. */
293
+ const RUNTIME_GLOBALS = ["$fetch"];
294
+ /** Registered globally by Vue and vue-router, so never listed in components.d.ts. */
295
+ const GLOBAL_COMPONENTS = ["RouterLink", "RouterView"];
296
+ function knownIdentifiers(extra) {
297
+ return /* @__PURE__ */ new Set([
298
+ ...Object.keys(globals.builtin),
299
+ ...Object.keys(globals.browser),
300
+ ...Object.keys(globals.node),
301
+ ...SFC_MACROS,
302
+ ...RUNTIME_GLOBALS,
303
+ "undefined",
304
+ "arguments",
305
+ ...extra
306
+ ]);
307
+ }
308
+ function knownComponents(extra) {
309
+ return /* @__PURE__ */ new Set([...GLOBAL_COMPONENTS, ...extra]);
310
+ }
311
+ //#endregion
312
+ //#region src/analyze/analysis-env.ts
313
+ /** What every file analysis needs: symbols, aliases, ownership and known globals. */
314
+ function createAnalysisEnv(table, ownerOf, buildDir, config) {
315
+ return {
316
+ table,
317
+ aliases: loadAliases(buildDir),
318
+ buildDir,
319
+ ownerOf,
320
+ identifiers: knownIdentifiers(config.globals ?? []),
321
+ components: knownComponents(config.globals ?? []),
322
+ unresolvedSeverity: ruleSeverity(config, "unresolved-reference")
323
+ };
324
+ }
325
+ //#endregion
326
+ //#region src/nuxt/kit.ts
327
+ function findPackageDir(from, name) {
328
+ for (let dir = from;; dir = dirname(dir)) {
329
+ const candidate = join(dir, "node_modules", name);
330
+ if (existsSync(join(candidate, "package.json"))) return realpathSync(candidate);
331
+ if (dirname(dir) === dir) return null;
332
+ }
333
+ }
334
+ function packageEntry(dir) {
335
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
336
+ let entry = pkg.exports?.["."];
337
+ while (typeof entry === "object" && entry !== null) {
338
+ const conditions = entry;
339
+ entry = conditions.import ?? conditions.node ?? conditions.default;
340
+ }
341
+ return join(dir, typeof entry === "string" ? entry : pkg.module ?? pkg.main ?? "index.js");
342
+ }
343
+ /**
344
+ * Loads `@nuxt/kit` from the project's own Nuxt install rather than bundling one, so the
345
+ * config defaults match the project's Nuxt major version.
346
+ */
347
+ async function loadNuxtKit(rootDir) {
348
+ let kitDir = findPackageDir(rootDir, "@nuxt/kit");
349
+ if (kitDir === null) {
350
+ const nuxtDir = findPackageDir(rootDir, "nuxt");
351
+ kitDir = nuxtDir === null ? null : findPackageDir(nuxtDir, "@nuxt/kit");
352
+ }
353
+ if (kitDir === null) return null;
354
+ return await import(pathToFileURL(packageEntry(kitDir)).href);
355
+ }
356
+ //#endregion
357
+ //#region src/nuxt/validate-layers.ts
358
+ function assertUniqueNames(layers) {
359
+ const roots = /* @__PURE__ */ new Map();
360
+ for (const layer of layers) {
361
+ const other = roots.get(layer.name);
362
+ if (other !== void 0) throw new LayerscopeError(`Two layers are named "${layer.name}" (${other} and ${layer.root}). Set "path" in layerscope.config.ts to tell them apart.`);
363
+ roots.set(layer.name, layer.root);
364
+ }
365
+ return new Set(roots.keys());
366
+ }
367
+ function validateLayers(layers, config) {
368
+ const names = assertUniqueNames(layers);
369
+ for (const [name, rule] of Object.entries(config.layers ?? {})) {
370
+ if (!names.has(name)) {
371
+ const known = [...names].toSorted().join(", ");
372
+ throw new LayerConfigError(`Unknown layer "${name}" in config. Known layers: ${known}`);
373
+ }
374
+ const unknown = rule.allow?.find((allowed) => !names.has(allowed));
375
+ if (unknown !== void 0) throw new LayerConfigError(`layers.${name}.allow: unknown layer "${unknown}"`);
376
+ }
377
+ }
378
+ //#endregion
379
+ //#region src/nuxt/layers.ts
380
+ function configuredPaths(rootDir, config) {
381
+ return Object.entries(config.layers ?? {}).flatMap(([name, rule]) => rule.path === void 0 ? [] : [[name, realPath(resolve(rootDir, rule.path))]]);
382
+ }
383
+ function configuredSources(config) {
384
+ return Object.entries(config.layers ?? {}).flatMap(([name, rule]) => rule.source === void 0 ? [] : [[name, rule.source]]);
385
+ }
386
+ function layersFromConfig(rootDir, config) {
387
+ const paths = configuredPaths(rootDir, config);
388
+ if (paths.length === 0) throw new LayerscopeError(`Could not load @nuxt/kit from ${rootDir}. Install dependencies, or give every layer a "path" in layerscope.config.ts.`);
389
+ const layers = paths.map(([name, root]) => layerFromPath(name, root));
390
+ const root = realPath(resolve(rootDir));
391
+ return layers.some((layer) => layer.root === root) ? layers : [layerFromPath(ROOT_LAYER_NAME, root), ...layers];
392
+ }
393
+ async function layersFromNuxt(kit, rootDir) {
394
+ let options = null;
395
+ try {
396
+ options = await kit.loadNuxtConfig({ cwd: rootDir });
397
+ } catch (error) {
398
+ throw new LayerscopeError(`Failed to load Nuxt config: ${error.message}`);
399
+ }
400
+ const dirs = kit.getLayerDirectories?.({ options }) ?? options._layers.map((layer, index) => deriveDirectories(layer, index, options));
401
+ return options._layers.map((layer, index) => layerFromNuxt(layer, index, dirs[index]));
402
+ }
403
+ /** `path` in config names the Nuxt layer at that root. */
404
+ function applyConfiguredNames(layers, rootDir, config) {
405
+ const named = layers.map((layer) => ({ ...layer }));
406
+ for (const [name, root] of configuredPaths(rootDir, config)) {
407
+ const layer = named.find((candidate) => candidate.root === root);
408
+ if (layer === void 0) throw new LayerscopeError(`layers.${name}.path: no Nuxt layer at ${root}`);
409
+ layer.name = name;
410
+ }
411
+ for (const [name, source] of configuredSources(config)) {
412
+ const matches = named.filter((candidate) => isClonedFrom(candidate.root, source));
413
+ if (matches.length !== 1) {
414
+ const found = matches.length === 0 ? "no layer cloned" : "several layers cloned";
415
+ throw new LayerscopeError(`layers.${name}.source: ${found} from "${source}". Set $meta.name in the layer's nuxt.config instead.`);
416
+ }
417
+ matches[0].name = name;
418
+ }
419
+ return named;
420
+ }
421
+ /** Applies `path` names from config to recorded layers and checks config against them. */
422
+ function nameLayers(layers, rootDir, config) {
423
+ const named = applyConfiguredNames(layers, rootDir, config);
424
+ validateLayers(named, config);
425
+ return named;
426
+ }
427
+ /**
428
+ * The ordered layer list, highest priority first. `recorded` are the layers the Nuxt module
429
+ * wrote to the registry; without them the layers come from the project's `@nuxt/kit`.
430
+ */
431
+ async function resolveLayers(rootDir, config, recorded) {
432
+ let layers = recorded;
433
+ if (layers === void 0) {
434
+ const kit = await loadNuxtKit(rootDir);
435
+ layers = kit === null ? layersFromConfig(rootDir, config) : await layersFromNuxt(kit, rootDir);
436
+ }
437
+ return nameLayers(layers, rootDir, config);
438
+ }
439
+ //#endregion
440
+ //#region src/nuxt/owner.ts
441
+ /** The layer whose root is the longest prefix of the file; files in node_modules have none. */
442
+ function createOwnerLookup(layers) {
443
+ const deepestFirst = layers.toSorted((a, b) => b.root.length - a.root.length);
444
+ return (file) => {
445
+ const layer = deepestFirst.find((candidate) => file.startsWith(`${candidate.root}/`));
446
+ if (layer === void 0) return null;
447
+ return file.slice(layer.root.length).includes("/node_modules/") ? null : layer;
448
+ };
449
+ }
450
+ //#endregion
451
+ export { readRegistry as a, boundaryFindings as c, loadConfig as d, loadConfigSync as f, createAnalysisEnv as i, isAllowed as l, nameLayers as n, tableFromRegistry as o, resolveLayers as r, loadSymbolTable as s, createOwnerLookup as t, findConfigFile as u };
@@ -0,0 +1,2 @@
1
+ import { n as formatResult } from "./report-jEMtNXS8.mjs";
2
+ export { formatResult };
@@ -1,6 +1,15 @@
1
- import { t as summarize } from "./summary-DiUYKMPe.mjs";
2
1
  import { i as plural } from "./strings-CrOjNZmB.mjs";
2
+ import { t as summarize } from "./summary-DiUYKMPe.mjs";
3
3
  import { relative } from "pathe";
4
+ import { styleText } from "node:util";
5
+ //#region src/utils/style.ts
6
+ /** No styles: what the report functions use unless the CLI passes a painter for its terminal. */
7
+ const plain = (_format, text) => text;
8
+ /** Styles only when `stream` is a terminal; `NO_COLOR` and `FORCE_COLOR` are honoured. */
9
+ function paintFor(stream) {
10
+ return (format, text) => styleText(format, text, { stream });
11
+ }
12
+ //#endregion
4
13
  //#region src/report/baseline.ts
5
14
  /** `layer-boundary useCart → web (×2)` */
6
15
  function describeEntry(entry) {
@@ -87,8 +96,8 @@ function formatJson(result, cwd) {
87
96
  //#endregion
88
97
  //#region src/report/text.ts
89
98
  const DETAIL_INDENT = " ".repeat(17);
90
- function formatFinding(finding, cwd) {
91
- return [` ${`${finding.line}:${finding.column}`.padEnd(8)}${finding.severity.padEnd(7)}${finding.message} ${finding.rule}`, ...findingDetails(finding, cwd).map((detail) => `${DETAIL_INDENT}${detail}`)].join("\n");
99
+ function formatFinding(finding, cwd, paint) {
100
+ return [` ${paint("dim", `${finding.line}:${finding.column}`.padEnd(8))}${paint(finding.severity === "error" ? "red" : "yellow", finding.severity) + " ".repeat(7 - finding.severity.length)}${finding.message} ${paint("dim", finding.rule)}`, ...findingDetails(finding, cwd).map((detail) => `${DETAIL_INDENT}${paint("dim", detail)}`)].join("\n");
92
101
  }
93
102
  function formatRemovable(baseline, cwd) {
94
103
  if (baseline.removable.length === 0) return null;
@@ -96,17 +105,22 @@ function formatRemovable(baseline, cwd) {
96
105
  const count = baseline.removable.length;
97
106
  return [`${relative(cwd, baseline.file)}: ${plural(count, "fixed entry", "fixed entries")}. ${UPDATE_HINT} ${count === 1 ? "it" : "them"}.`, ...entries].join("\n");
98
107
  }
99
- function formatFooter(result) {
108
+ function formatFooter(result, paint) {
100
109
  const suppressed = result.baseline?.suppressed.length ?? 0;
101
110
  const baselined = suppressed > 0 ? `, ${suppressed} more in the baseline` : "";
102
- if (result.findings.length === 0) return `✔ No problems in ${plural(result.files.length, "file")} across ${plural(result.layers.length, "layer")}${baselined}`;
111
+ if (result.findings.length === 0) return `${paint(["green", "bold"], `✔ No problems in ${plural(result.files.length, "file")} across ${plural(result.layers.length, "layer")}`)}${baselined}`;
103
112
  const { errors, warnings } = summarize(result.findings);
104
- return `✖ ${plural(result.findings.length, "problem")} (${plural(errors, "error")}, ${plural(warnings, "warning")})${baselined}`;
113
+ const counts = `✖ ${plural(result.findings.length, "problem")} (${plural(errors, "error")}, ${plural(warnings, "warning")})`;
114
+ return `${paint(["bold", errors > 0 ? "red" : "yellow"], counts)}${baselined}`;
115
+ }
116
+ function hasError(findings) {
117
+ return findings.some((finding) => finding.severity === "error");
105
118
  }
106
- function formatText(result, cwd) {
107
- const sections = [...Map.groupBy(result.findings, (finding) => relative(cwd, finding.file))].map(([file, findings]) => [file, ...findings.map((finding) => formatFinding(finding, cwd))].join("\n"));
119
+ /** Files with errors come last, next to the summary, where they are least likely to scroll away. */
120
+ function formatText(result, cwd, paint = plain) {
121
+ const sections = [...Map.groupBy(result.findings, (finding) => relative(cwd, finding.file))].toSorted(([, a], [, b]) => Number(hasError(a)) - Number(hasError(b))).map(([file, findings]) => [paint("underline", file), ...findings.map((finding) => formatFinding(finding, cwd, paint))].join("\n"));
108
122
  const removable = result.baseline === void 0 ? null : formatRemovable(result.baseline, cwd);
109
- return `${[...removable === null ? sections : [...sections, removable], formatFooter(result)].join("\n\n")}\n`;
123
+ return `${[...removable === null ? sections : [...sections, removable], formatFooter(result, paint)].join("\n\n")}\n`;
110
124
  }
111
125
  //#endregion
112
126
  //#region src/report/index.ts
@@ -123,9 +137,9 @@ const formatters = {
123
137
  function isOutputFormat(value) {
124
138
  return Object.hasOwn(formatters, value);
125
139
  }
126
- /** Paths in the output are relative to `cwd`. */
127
- function formatResult(result, format, cwd = process.cwd()) {
128
- return formatters[format](result, cwd);
140
+ /** Paths in the output are relative to `cwd`. Only the text format uses `paint`. */
141
+ function formatResult(result, format, cwd = process.cwd(), paint = plain) {
142
+ return formatters[format](result, cwd, paint);
129
143
  }
130
144
  //#endregion
131
- export { JSON_REPORT_VERSION as i, formatResult as n, isOutputFormat as r, OUTPUT_FORMATS as t };
145
+ export { paintFor as a, JSON_REPORT_VERSION as i, formatResult as n, isOutputFormat as r, OUTPUT_FORMATS as t };
@@ -1,5 +1,5 @@
1
1
  import { t as compareStrings } from "./strings-CrOjNZmB.mjs";
2
- import { c as createOwnerLookup, g as isAllowed } from "./file-analysis-Bh7qcWUK.mjs";
2
+ import { l as isAllowed, t as createOwnerLookup } from "./owner-BA00V9VD.mjs";
3
3
  import { relative } from "pathe";
4
4
  //#region src/rules/edge-status.ts
5
5
  /** How the layer rules see one edge. */
@@ -1,4 +1,4 @@
1
- import { s as readJson } from "./layer-factory-DFh7nkFH.mjs";
1
+ import { _ as readJson } from "./layer-factory-DVSf2Wj4.mjs";
2
2
  import { fileURLToPath } from "node:url";
3
3
  //#region src/version.ts
4
4
  /** Both `src/` and the bundled `dist/` sit next to package.json. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-layerscope",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Layer boundary checks for Nuxt apps that see auto-imports",
5
5
  "keywords": [
6
6
  "architecture",
@@ -1,2 +0,0 @@
1
- import { t as analyze } from "./analyze-QaYmBXyK.mjs";
2
- export { analyze };
@@ -1,12 +0,0 @@
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 };