mfe-guard 0.1.0 → 0.1.1

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 ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1
4
+
5
+ - Manifests and config files saved with a UTF-8 byte-order mark (Windows Notepad) now load.
6
+ - Invalid JSON errors show the line and column, and say so when a URL returned HTML.
7
+ - Patch-only `version-drift` is folded into one summary line; `--verbose` lists each package.
8
+ - Minor `version-drift` now notes which participant will run on the newer version at runtime.
9
+
10
+ ## 0.1.0
11
+
12
+ - First release: manifest and project modes; rules `singleton-unsatisfied`, `major-mismatch`,
13
+ `duplicate-name`, `exact-pin`, `not-singleton`, `version-drift`; text and JSON output.
package/README.md CHANGED
@@ -24,7 +24,7 @@ module @angular/material/autocomplete does not satisfy the requirement of orders
24
24
  ## Install
25
25
 
26
26
  ```bash
27
- npx mfe-guard check --help # no install needed
27
+ npx -y mfe-guard check --help # no install needed
28
28
  npm i -D mfe-guard # or as a dev dependency
29
29
  ```
30
30
 
@@ -82,7 +82,7 @@ CLI flags are merged on top of the config.
82
82
  | `duplicate-name` | error | Two participants share a federation `name` or global/chunk name, so they overwrite each other's modules. |
83
83
  | `exact-pin` | warn | A singleton is required as an exact version (`"19.2.1"`), so any patch bump elsewhere breaks it. |
84
84
  | `not-singleton` | warn | Framework packages (`@angular/core`, `rxjs`, `zone.js`, `react`, …) shared without `singleton: true`. |
85
- | `version-drift` | info | Versions differ but are still compatible; shown so you know what will run. |
85
+ | `version-drift` | info | Versions differ but are still compatible. Minor drift is listed with a note on what will actually run; patch-only drift is folded into one line (`--verbose` lists it). |
86
86
 
87
87
  Secondary entry points (`@angular/material/autocomplete`, `@angular/common/http`, …) are collapsed into their root package, so you get one finding instead of forty.
88
88
 
@@ -104,7 +104,7 @@ jobs:
104
104
  - run: npx mfe-guard check # reads mfe-guard.config.json
105
105
  ```
106
106
 
107
- Exit codes: `0` ok · `1` findings at or above `--fail-on` (default `error`) · `2` usage or load error. Use `--json` for machine-readable output.
107
+ Exit codes: `0` ok · `1` findings at or above `--fail-on` (default `error`) · `2` usage or load error. Use `--json` for machine-readable output (always includes every finding) and `--verbose` to list patch-only drift in the text report.
108
108
 
109
109
  ## Programmatic API
110
110
 
package/dist/check.js CHANGED
@@ -131,13 +131,19 @@ export function check(input, opts = {}) {
131
131
  else {
132
132
  const distinct = [...new Set(providers.map((u) => u.e.version))];
133
133
  if (distinct.length > 1) {
134
+ const drift = new Set(distinct.map((v) => semver.minor(v))).size > 1 ? 'minor' : 'patch';
135
+ const behind = providers.filter((u) => u.e.version !== winVer).map((u) => u.p.name);
134
136
  add({
135
137
  rule: 'version-drift',
136
138
  severity: 'info',
137
139
  package: pkg,
138
140
  entryPoints,
141
+ drift,
139
142
  message: `${pkg} versions differ (${distinct.join(', ')}); singleton resolves to ${winVer}`,
140
143
  details: providers.map((u) => `${u.p.name} ships ${versionLabel(u)}`),
144
+ fix: drift === 'minor'
145
+ ? `${behind.join(', ')} will run on ${pkg}@${winVer} at runtime — test against it or align on ${winVer}`
146
+ : undefined,
141
147
  participants: names,
142
148
  });
143
149
  }
package/dist/cli.js CHANGED
@@ -21,6 +21,7 @@ Options
21
21
  --singletons <a,b,...> project mode: packages treated as singletons ("*" = Nx default)
22
22
  --ignore <rule|pkg|rule:pkg,...>
23
23
  --fail-on <error|warn|never> exit 1 at this severity (default: error)
24
+ --verbose list every finding, including patch-only version drift
24
25
  --json machine-readable output
25
26
  -h, --help
26
27
  -v, --version
@@ -54,6 +55,7 @@ async function main(argv) {
54
55
  ignore: { type: 'string' },
55
56
  'fail-on': { type: 'string' },
56
57
  json: { type: 'boolean' },
58
+ verbose: { type: 'boolean' },
57
59
  help: { type: 'boolean', short: 'h' },
58
60
  version: { type: 'boolean', short: 'v' },
59
61
  },
@@ -94,7 +96,7 @@ async function main(argv) {
94
96
  ignore: [...(config.ignore ?? []), ...(list(values.ignore) ?? [])],
95
97
  criticalSingletons: config.criticalSingletons,
96
98
  });
97
- console.log(values.json ? formatJson(result, VERSION) : formatText(result, VERSION));
99
+ console.log(values.json ? formatJson(result, VERSION) : formatText(result, VERSION, { verbose: values.verbose }));
98
100
  if (failOn === 'never')
99
101
  return 0;
100
102
  if (result.summary.error > 0)
package/dist/config.js CHANGED
@@ -1,16 +1,18 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { dirname, isAbsolute, resolve } from 'node:path';
3
+ import { parseJson } from './util.js';
3
4
  export const DEFAULT_CONFIG_FILE = 'mfe-guard.config.json';
4
5
  /** Load a JSON config; relative project/manifest paths resolve against the config file. */
5
6
  export async function loadConfig(path) {
6
7
  const abs = resolve(path);
7
- let raw;
8
+ let text;
8
9
  try {
9
- raw = JSON.parse(await readFile(abs, 'utf8'));
10
+ text = await readFile(abs, 'utf8');
10
11
  }
11
12
  catch (e) {
12
13
  throw new Error(`cannot read config ${path}: ${e.message}`);
13
14
  }
15
+ const raw = parseJson(text, path);
14
16
  if (!Array.isArray(raw.participants)) {
15
17
  throw new Error(`${path}: "participants" must be an array`);
16
18
  }
@@ -1,4 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises';
2
+ import { parseJson } from '../util.js';
2
3
  /**
3
4
  * Parse an `mf-manifest.json` (or `mf-stats.json`) produced by
4
5
  * @module-federation/enhanced, webpack or rspack.
@@ -44,12 +45,5 @@ export async function loadManifest(source, label) {
44
45
  else {
45
46
  text = await readFile(source, 'utf8');
46
47
  }
47
- let json;
48
- try {
49
- json = JSON.parse(text);
50
- }
51
- catch {
52
- throw new Error(`${source}: response is not valid JSON`);
53
- }
54
- return parseManifest(json, source, label);
48
+ return parseManifest(parseJson(text, source), source, label);
55
49
  }
@@ -1,7 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import semver from 'semver';
3
3
  import { join } from 'node:path';
4
- import { cleanVersion, isRange, matchesPattern } from '../util.js';
4
+ import { cleanVersion, isRange, matchesPattern, parseJson } from '../util.js';
5
5
  /** Packages assumed to be shared singletons in project mode unless configured otherwise. */
6
6
  export const DEFAULT_SINGLETONS = [
7
7
  '@angular/*',
@@ -17,7 +17,7 @@ export const DEFAULT_SINGLETONS = [
17
17
  ];
18
18
  async function readJson(path) {
19
19
  try {
20
- return JSON.parse(await readFile(path, 'utf8'));
20
+ return parseJson(await readFile(path, 'utf8'), path);
21
21
  }
22
22
  catch {
23
23
  return undefined;
package/dist/report.d.ts CHANGED
@@ -1,3 +1,7 @@
1
1
  import type { CheckResult } from './types.js';
2
- export declare function formatText(result: CheckResult, version: string): string;
2
+ export interface TextOptions {
3
+ /** Show every finding, including patch-only version drift. */
4
+ verbose?: boolean;
5
+ }
6
+ export declare function formatText(result: CheckResult, version: string, opts?: TextOptions): string;
3
7
  export declare function formatJson(result: CheckResult, version: string): string;
package/dist/report.js CHANGED
@@ -9,7 +9,7 @@ const green = paint(32);
9
9
  const ICON = { error: '✖', warn: '⚠', info: 'ℹ' };
10
10
  const COLOR = { error: red, warn: yellow, info: cyan };
11
11
  const plural = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`;
12
- export function formatText(result, version) {
12
+ export function formatText(result, version, opts = {}) {
13
13
  const out = [];
14
14
  out.push(bold(`mfe-guard v${version}`) +
15
15
  dim(` — ${plural(result.participants.length, 'participant')}, ${plural(result.packagesCompared, 'shared package')} compared`));
@@ -21,7 +21,10 @@ export function formatText(result, version) {
21
21
  out.push(green('✔ No conflicts found.'));
22
22
  return out.join('\n');
23
23
  }
24
- for (const f of result.findings) {
24
+ const isPatchDrift = (f) => f.rule === 'version-drift' && f.drift === 'patch';
25
+ const collapsed = opts.verbose ? [] : result.findings.filter(isPatchDrift);
26
+ const shown = opts.verbose ? result.findings : result.findings.filter((f) => !isPatchDrift(f));
27
+ for (const f of shown) {
25
28
  const c = COLOR[f.severity];
26
29
  const pkg = f.package ? ` ${bold(f.package)}${f.entryPoints && f.entryPoints > 1 ? dim(` (${f.entryPoints} entry points)`) : ''}` : '';
27
30
  out.push(`${c(`${ICON[f.severity]} ${f.severity.padEnd(5)}`)} ${dim(f.rule)}${pkg}`);
@@ -29,7 +32,12 @@ export function formatText(result, version) {
29
32
  for (const d of f.details)
30
33
  out.push(dim(` ${d}`));
31
34
  if (f.fix)
32
- out.push(` ${green('fix:')} ${f.fix}`);
35
+ out.push(` ${green(f.severity === 'info' ? 'note:' : 'fix:')} ${f.fix}`);
36
+ out.push('');
37
+ }
38
+ if (collapsed.length) {
39
+ out.push(`${cyan(`${ICON.info} info `)} ${dim('version-drift')} ${plural(collapsed.length, 'package')} ${collapsed.length === 1 ? 'differs' : 'differ'} by patch version only ${dim('(--verbose to list)')}`);
40
+ out.push(dim(` ${collapsed.map((f) => f.package).join(', ')}`));
33
41
  out.push('');
34
42
  }
35
43
  const s = result.summary;
package/dist/types.d.ts CHANGED
@@ -40,6 +40,8 @@ export interface Finding {
40
40
  details: string[];
41
41
  fix?: string;
42
42
  participants: string[];
43
+ /** version-drift only: "patch" when only patch versions differ, "minor" otherwise. */
44
+ drift?: 'patch' | 'minor';
43
45
  }
44
46
  export interface CheckOptions {
45
47
  /** Packages that must always be singletons (warned by `not-singleton`). */
package/dist/util.d.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import type { SharedEntry } from './types.js';
2
2
  /** "primeng/autocomplete" -> "primeng", "@angular/common/http" -> "@angular/common". */
3
3
  export declare function rootPackage(name: string): string;
4
+ /**
5
+ * JSON.parse that tolerates a UTF-8 byte-order mark (Windows Notepad adds one)
6
+ * and reports where the JSON is broken.
7
+ */
8
+ export declare function parseJson(text: string, source: string): unknown;
4
9
  /** Clean a version string into valid semver, or undefined. */
5
10
  export declare function cleanVersion(v: string | undefined): string | undefined;
6
11
  /** True for a usable semver range (rejects workspace:, git urls, "*"-less garbage). */
package/dist/util.js CHANGED
@@ -4,6 +4,29 @@ export function rootPackage(name) {
4
4
  const parts = name.split('/');
5
5
  return name.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
6
6
  }
7
+ /**
8
+ * JSON.parse that tolerates a UTF-8 byte-order mark (Windows Notepad adds one)
9
+ * and reports where the JSON is broken.
10
+ */
11
+ export function parseJson(text, source) {
12
+ const clean = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
13
+ try {
14
+ return JSON.parse(clean);
15
+ }
16
+ catch (e) {
17
+ const msg = e.message;
18
+ const pos = /position (\d+)/.exec(msg);
19
+ let where = '';
20
+ if (pos && !/line \d+/.test(msg)) {
21
+ const before = clean.slice(0, Number(pos[1]));
22
+ const line = before.split('\n').length;
23
+ const col = Number(pos[1]) - before.lastIndexOf('\n');
24
+ where = ` (line ${line}, column ${col})`;
25
+ }
26
+ const hint = /^\s*</.test(clean) ? ' — got HTML, check the URL points at mf-manifest.json' : '';
27
+ throw new Error(`${source}: invalid JSON: ${msg}${where}${hint}`);
28
+ }
29
+ }
7
30
  /** Clean a version string into valid semver, or undefined. */
8
31
  export function cleanVersion(v) {
9
32
  if (!v)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mfe-guard",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Catch Module Federation shared-dependency conflicts (singleton version mismatches, duplicate remote names) before they crash your micro-frontends in production.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,7 +17,8 @@
17
17
  "files": [
18
18
  "dist",
19
19
  "README.md",
20
- "LICENSE"
20
+ "LICENSE",
21
+ "CHANGELOG.md"
21
22
  ],
22
23
  "scripts": {
23
24
  "build": "node scripts/clean.mjs && tsc -p tsconfig.json",