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 +13 -0
- package/README.md +3 -3
- package/dist/check.js +6 -0
- package/dist/cli.js +3 -1
- package/dist/config.js +4 -2
- package/dist/loaders/manifest.js +2 -8
- package/dist/loaders/project.js +2 -2
- package/dist/report.d.ts +5 -1
- package/dist/report.js +11 -3
- package/dist/types.d.ts +2 -0
- package/dist/util.d.ts +5 -0
- package/dist/util.js +23 -0
- package/package.json +3 -2
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
|
|
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
|
|
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
|
|
8
|
+
let text;
|
|
8
9
|
try {
|
|
9
|
-
|
|
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
|
}
|
package/dist/loaders/manifest.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/loaders/project.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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",
|