nuxt-layerscope 0.1.1 → 0.1.3

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.3
4
+
5
+ - Notes and the `--verbose` line are printed after the report, in every command, so they are the
6
+ last thing on screen. `note:` is highlighted in a terminal.
7
+ - `layerscope check` lists files with errors after files with only warnings, so the errors stay
8
+ next to the summary in a long report.
9
+ - The text report colors severities, rule names and the summary in a terminal. `NO_COLOR` and
10
+ `FORCE_COLOR` are honoured.
11
+
12
+ ## 0.1.2
13
+
14
+ - A note appears when `nuxt.config` is newer than the registry, because changes to its
15
+ `layerscope` key only apply after `nuxi prepare`, `dev` or `build`.
16
+ - Layer errors caused by the `layerscope` key of `nuxt.config` name it as their source.
17
+
3
18
  ## 0.1.1
4
19
 
5
20
  - Names inside type syntax are no longer reported as unresolved references: labeled tuple members
@@ -0,0 +1,2 @@
1
+ import { t as analyze } from "./analyze-QaYmBXyK.mjs";
2
+ export { analyze };
@@ -1,9 +1,9 @@
1
- import { c as realPath, d as effectiveConfig, g as LayerscopeError, h as ruleSeverity, m as isRuleName, s as readJson } from "./layer-factory-xZmIAPrA.mjs";
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";
2
2
  import { r as pascalCase, t as compareStrings } from "./strings-CrOjNZmB.mjs";
3
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-D9TGP_Nb.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";
5
5
  import { basename, extname, join, relative, resolve } from "pathe";
6
- import { existsSync, writeFileSync } from "node:fs";
6
+ import { existsSync, statSync, writeFileSync } from "node:fs";
7
7
  import { escapePath, glob } from "tinyglobby";
8
8
  import { spawnSync } from "node:child_process";
9
9
  //#region src/baseline/index.ts
@@ -149,6 +149,24 @@ function runRules(input) {
149
149
  };
150
150
  }
151
151
  //#endregion
152
+ //#region src/analyze/config-freshness.ts
153
+ const NUXT_CONFIG_FILES = [
154
+ "nuxt.config.ts",
155
+ "nuxt.config.mts",
156
+ "nuxt.config.js",
157
+ "nuxt.config.mjs"
158
+ ];
159
+ /**
160
+ * The registry holds the `layerscope` key of `nuxt.config` as it was at the last `nuxi prepare`,
161
+ * `dev` or `build`, so an edit made since then does not apply yet.
162
+ */
163
+ function staleConfigNote(rootDir, registryFile) {
164
+ const config = NUXT_CONFIG_FILES.map((name) => join(rootDir, name)).find((file) => existsSync(file));
165
+ if (config === void 0 || !existsSync(registryFile)) return null;
166
+ if (statSync(config).mtimeMs <= statSync(registryFile).mtimeMs) return null;
167
+ return "nuxt.config is newer than the layerscope registry, so changes to its \"layerscope\" key do not apply yet. Run \"nuxi prepare\" again, or pass --prepare.";
168
+ }
169
+ //#endregion
152
170
  //#region src/analyze/freshness.ts
153
171
  const RERUN_HINT = "Run \"nuxi prepare\" again, or pass --prepare.";
154
172
  const COMPONENT_SUFFIX = /(?:\.(?:client|server))?(?:\.global|\.island)*$/u;
@@ -266,13 +284,21 @@ function recordedConfig(buildDir, registry) {
266
284
  async function loadEnvironment(options) {
267
285
  const { rootDir, buildDir } = options;
268
286
  const { registry, table, ...symbols } = loadSymbols(buildDir, options.source);
269
- const config = effectiveConfig(options.config, recordedConfig(buildDir, registry));
270
- const { layers, ownerOf, notes } = await loadLayers(rootDir, config, registry?.layers);
287
+ const recorded = recordedConfig(buildDir, registry);
288
+ const config = effectiveConfig(options.config, recorded);
289
+ const { layers, ownerOf, notes } = await loadLayers(rootDir, config, registry?.layers).catch((error) => {
290
+ throw labelConfigError(error, recorded);
291
+ });
292
+ const stale = registry === null ? null : staleConfigNote(rootDir, symbols.sourceFile);
271
293
  await (registry === null ? assertFresh(table, layers) : assertRegistryFresh(table, registry));
272
294
  return {
273
295
  ...symbols,
274
296
  config,
275
- notes: [...symbols.notes, ...notes],
297
+ notes: [
298
+ ...symbols.notes,
299
+ ...notes,
300
+ ...stale === null ? [] : [stale]
301
+ ],
276
302
  registry,
277
303
  layers,
278
304
  env: createAnalysisEnv(table, ownerOf, buildDir, config)
package/dist/api.d.mts CHANGED
@@ -1,5 +1,6 @@
1
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
2
  import { t as ProjectConfig } from "./effective-Dqg4EwH_.mjs";
3
+ import { styleText } from "node:util";
3
4
  //#region src/analyze/index.d.ts
4
5
  interface AnalyzeOptions {
5
6
  /** Defaults to `process.cwd()`. */
@@ -114,11 +115,15 @@ interface Registry {
114
115
  config?: ProjectConfig;
115
116
  }
116
117
  //#endregion
118
+ //#region src/utils/style.d.ts
119
+ /** Wraps text in terminal styles, e.g. `paint('red', 'error')`. */
120
+ type Paint = (format: Parameters<typeof styleText>[0], text: string) => string;
121
+ //#endregion
117
122
  //#region src/report/index.d.ts
118
123
  export declare const OUTPUT_FORMATS: readonly ['text', 'github', 'json'];
119
124
  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;
125
+ /** Paths in the output are relative to `cwd`. Only the text format uses `paint`. */
126
+ export declare function formatResult(result: AnalyzeResult, format: OutputFormat, cwd?: string, paint?: Paint): string;
122
127
  //#endregion
123
128
  //#region src/report/json.d.ts
124
129
  /** Bumped on breaking changes to the JSON report shape. */
package/dist/api.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { g as LayerscopeError, l as REGISTRY_FILE, u as REGISTRY_VERSION } from "./layer-factory-xZmIAPrA.mjs";
1
+ import { l as REGISTRY_FILE, u as REGISTRY_VERSION, v as LayerscopeError } from "./layer-factory-DFh7nkFH.mjs";
2
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-C78UDFtY.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-Cgg4-LKM.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";
6
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.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { g as LayerscopeError } from "./layer-factory-xZmIAPrA.mjs";
2
+ import { v as LayerscopeError } from "./layer-factory-DFh7nkFH.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-C78UDFtY.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-Cgg4-LKM.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";
7
7
  import { relative, resolve } from "pathe";
8
8
  import { cac } from "cac";
9
9
  //#region src/commands/shared.ts
@@ -20,14 +20,19 @@ function toSource(value) {
20
20
  if (source === void 0) throw new LayerscopeError(`Unknown source "${value}". Use ${SOURCE_OPTIONS.join(", ")}.`);
21
21
  return source;
22
22
  }
23
- /** Diagnostics go to stderr, so stdout stays machine-readable in every format. */
23
+ /**
24
+ * Diagnostics go to stderr, so stdout stays machine-readable in every format. Call it after the
25
+ * report is written: notes at the end are the ones a reader still sees when the report scrolled.
26
+ */
24
27
  function writeNotes(result, verbose) {
25
- const lines = result.notes.map((note) => `layerscope: note: ${note}`);
28
+ const paint = paintFor(process.stderr);
29
+ const lines = result.notes.map((note) => `${paint(["yellow", "bold"], "layerscope: note:")} ${note}`);
26
30
  if (verbose) {
27
31
  const kind = result.source === "registry" ? "the registry" : "the generated .d.ts files";
28
- lines.unshift(`layerscope: symbols from ${kind} (${relative(process.cwd(), result.sourceFile)})`);
32
+ const source = relative(process.cwd(), result.sourceFile);
33
+ lines.unshift(paint("dim", `layerscope: symbols from ${kind} (${source})`));
29
34
  }
30
- if (lines.length > 0) process.stderr.write(`${lines.join("\n")}\n`);
35
+ if (lines.length > 0) process.stderr.write(`${process.stdout.isTTY ? "\n" : ""}${lines.join("\n")}\n`);
31
36
  }
32
37
  //#endregion
33
38
  //#region src/commands/check.ts
@@ -38,11 +43,11 @@ async function updateBaseline(rootDir, flags) {
38
43
  prepare: flags.prepare,
39
44
  source: toSource(flags.source)
40
45
  });
41
- writeNotes(result, flags.verbose === true);
42
46
  const file = resolve(rootDir, flags.baseline);
43
47
  writeBaseline(file, createBaseline(result.findings, rootDir));
44
48
  const count = plural(result.findings.length, "finding");
45
49
  process.stdout.write(`✔ Wrote ${count} to ${relative(process.cwd(), file)}\n`);
50
+ writeNotes(result, flags.verbose === true);
46
51
  return 0;
47
52
  }
48
53
  async function check(root, flags) {
@@ -57,8 +62,8 @@ async function check(root, flags) {
57
62
  source: toSource(flags.source),
58
63
  baseline: flags.baseline
59
64
  });
65
+ process.stdout.write(formatResult(result, format, process.cwd(), paintFor(process.stdout)));
60
66
  writeNotes(result, flags.verbose === true);
61
- process.stdout.write(formatResult(result, format));
62
67
  return result.findings.some((finding) => finding.severity === "error") ? 1 : 0;
63
68
  }
64
69
  function registerCheck(cli) {
@@ -162,8 +167,8 @@ async function graph(root, flags) {
162
167
  prepare: flags.prepare,
163
168
  source: toSource(flags.source)
164
169
  });
165
- writeNotes(result, flags.verbose === true);
166
170
  process.stdout.write(formatGraph(buildGraph(result, result.config, level), format));
171
+ writeNotes(result, flags.verbose === true);
167
172
  return 0;
168
173
  }
169
174
  function registerGraph(cli) {
@@ -216,8 +221,8 @@ async function unused(root, flags) {
216
221
  prepare: flags.prepare,
217
222
  source: toSource(flags.source)
218
223
  });
219
- writeNotes(result, flags.verbose === true);
220
224
  process.stdout.write(formatUnused(findUnused(result), format));
225
+ writeNotes(result, flags.verbose === true);
221
226
  return 0;
222
227
  }
223
228
  function registerUnused(cli) {
@@ -331,10 +336,13 @@ async function why(symbol, root, flags) {
331
336
  prepare: flags.prepare,
332
337
  source: toSource(flags.source)
333
338
  });
334
- writeNotes(result, flags.verbose === true);
335
339
  const targets = findUses(result, symbol, result.config);
336
- if (targets.length === 0) throw new LayerscopeError(`No uses of "${symbol}" found.`);
340
+ if (targets.length === 0) {
341
+ writeNotes(result, flags.verbose === true);
342
+ throw new LayerscopeError(`No uses of "${symbol}" found.`);
343
+ }
337
344
  process.stdout.write(formatWhy(symbol, targets, format));
345
+ writeNotes(result, flags.verbose === true);
338
346
  return 0;
339
347
  }
340
348
  function registerWhy(cli) {
package/dist/eslint.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { d as effectiveConfig, l as REGISTRY_FILE } from "./layer-factory-xZmIAPrA.mjs";
2
- import { t as packageVersion } from "./version-D6whhsHH.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-D9TGP_Nb.mjs";
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";
4
4
  import { dirname, join, relative, resolve } from "pathe";
5
5
  import { existsSync, readFileSync, statSync } from "node:fs";
6
6
  //#region src/eslint/lint-file.ts
@@ -1,4 +1,4 @@
1
- import { a as isClonedFrom, c as realPath, g as LayerscopeError, h as ruleSeverity, i as layerFromPath, l as REGISTRY_FILE, n as deriveDirectories, o as isFile, p as validateConfig, r as layerFromNuxt, s as readJson, t as ROOT_LAYER_NAME } from "./layer-factory-xZmIAPrA.mjs";
1
+ import { _ as LayerConfigError, a as isClonedFrom, c as realPath, g as ruleSeverity, i as layerFromPath, l as REGISTRY_FILE, m as validateConfig, n as deriveDirectories, o as isFile, r as layerFromNuxt, s as readJson, t as ROOT_LAYER_NAME, v as LayerscopeError } from "./layer-factory-DFh7nkFH.mjs";
2
2
  import { n as escapeRegExp, r as pascalCase, t as compareStrings } from "./strings-CrOjNZmB.mjs";
3
3
  import { dirname, isAbsolute, join, relative, resolve } from "pathe";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -408,10 +408,10 @@ function validateLayers(layers, config) {
408
408
  for (const [name, rule] of Object.entries(config.layers ?? {})) {
409
409
  if (!names.has(name)) {
410
410
  const known = [...names].toSorted().join(", ");
411
- throw new LayerscopeError(`Unknown layer "${name}" in config. Known layers: ${known}`);
411
+ throw new LayerConfigError(`Unknown layer "${name}" in config. Known layers: ${known}`);
412
412
  }
413
413
  const unknown = rule.allow?.find((allowed) => !names.has(allowed));
414
- if (unknown !== void 0) throw new LayerscopeError(`layers.${name}.allow: unknown layer "${unknown}"`);
414
+ if (unknown !== void 0) throw new LayerConfigError(`layers.${name}.allow: unknown layer "${unknown}"`);
415
415
  }
416
416
  }
417
417
  //#endregion
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { f as pickProjectConfig, l as REGISTRY_FILE, r as layerFromNuxt } from "./layer-factory-xZmIAPrA.mjs";
1
+ import { l as REGISTRY_FILE, p as pickProjectConfig, r as layerFromNuxt } from "./layer-factory-DFh7nkFH.mjs";
2
2
  import { t as summarize } from "./summary-DiUYKMPe.mjs";
3
3
  import { i as plural, t as compareStrings } from "./strings-CrOjNZmB.mjs";
4
- import { t as packageVersion } from "./version-D6whhsHH.mjs";
4
+ import { t as packageVersion } from "./version-DFWOUgi5.mjs";
5
5
  import { t as globComponents } from "./component-files-DgOXNE6l.mjs";
6
6
  import { t as defineConfig } from "./define-l62B9fVk.mjs";
7
7
  import { mkdir, writeFile } from "node:fs/promises";
@@ -106,7 +106,7 @@ function renderError(message) {
106
106
  //#region src/devtools/index.ts
107
107
  const DEVTOOLS_ROUTE = "/__layerscope";
108
108
  async function renderReport(nuxt, format) {
109
- const [{ analyze }, { formatResult }] = await Promise.all([import("./analyze-Du9Dkd8L.mjs"), import("./report-DtAi0vPb.mjs")]);
109
+ const [{ analyze }, { formatResult }] = await Promise.all([import("./analyze-DVzyPH7N.mjs"), import("./report-CKhXT50W.mjs")]);
110
110
  const { rootDir, app } = nuxt.options;
111
111
  const result = await analyze({ rootDir });
112
112
  if (format === "json") return formatResult(result, "json", rootDir);
@@ -9,6 +9,15 @@ var LayerscopeError = class extends Error {
9
9
  }
10
10
  };
11
11
  //#endregion
12
+ //#region src/layer-config-error.ts
13
+ /** A layer name in the config that matches no layer. */
14
+ var LayerConfigError = class extends LayerscopeError {
15
+ constructor(..._args) {
16
+ super(..._args);
17
+ this.name = "LayerConfigError";
18
+ }
19
+ };
20
+ //#endregion
12
21
  //#region src/config/rules.ts
13
22
  const DEFAULT_SEVERITY = {
14
23
  "layer-boundary": "error",
@@ -70,6 +79,11 @@ function effectiveConfig(fileConfig, recorded) {
70
79
  ...recorded
71
80
  };
72
81
  }
82
+ /** Names the `nuxt.config` key in a layer error, when the settings that caused it came from there. */
83
+ function labelConfigError(error, recorded) {
84
+ if (error instanceof LayerConfigError && recorded !== void 0 && hasProjectConfig(recorded)) return new LayerscopeError(`${NUXT_CONFIG_LABEL}: ${error.message}`);
85
+ return error;
86
+ }
73
87
  //#endregion
74
88
  //#region src/registry/schema.ts
75
89
  /** Bumped on breaking changes to `registry.json`; readers fall back to the `.d.ts` files. */
@@ -165,4 +179,4 @@ function layerFromPath(name, root) {
165
179
  };
166
180
  }
167
181
  //#endregion
168
- export { isClonedFrom as a, realPath as c, effectiveConfig as d, pickProjectConfig as f, LayerscopeError as g, ruleSeverity as h, layerFromPath as i, REGISTRY_FILE as l, isRuleName as m, deriveDirectories as n, isFile as o, validateConfig as p, layerFromNuxt as r, readJson as s, ROOT_LAYER_NAME as t, REGISTRY_VERSION as u };
182
+ export { LayerConfigError as _, isClonedFrom as a, realPath as c, effectiveConfig as d, labelConfigError as f, ruleSeverity as g, isRuleName as h, layerFromPath as i, REGISTRY_FILE as l, validateConfig as m, deriveDirectories as n, isFile as o, pickProjectConfig as p, layerFromNuxt as r, readJson as s, ROOT_LAYER_NAME as t, REGISTRY_VERSION as u, LayerscopeError as v };
@@ -1,2 +1,2 @@
1
- import { n as formatResult, r as isOutputFormat, t as OUTPUT_FORMATS } from "./report-BNW0GCl9.mjs";
1
+ import { n as formatResult, r as isOutputFormat, t as OUTPUT_FORMATS } from "./report-DShTCB59.mjs";
2
2
  export { OUTPUT_FORMATS, formatResult, isOutputFormat };
@@ -1,6 +1,15 @@
1
1
  import { t as summarize } from "./summary-DiUYKMPe.mjs";
2
2
  import { i as plural } from "./strings-CrOjNZmB.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-D9TGP_Nb.mjs";
2
+ import { c as createOwnerLookup, g as isAllowed } from "./file-analysis-Bh7qcWUK.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-xZmIAPrA.mjs";
1
+ import { s as readJson } from "./layer-factory-DFh7nkFH.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.1",
3
+ "version": "0.1.3",
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-C78UDFtY.mjs";
2
- export { analyze };