@memberjunction/cli 6.1.0-edge.5 → 6.1.0-edge.7
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/README.md +23 -0
- package/dist/commands/dev/workspace/index.d.ts +9 -0
- package/dist/commands/dev/workspace/index.d.ts.map +1 -1
- package/dist/commands/dev/workspace/index.js +82 -6
- package/dist/commands/dev/workspace/index.js.map +1 -1
- package/dist/config.d.ts +127 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +24 -2
- package/dist/config.js.map +1 -1
- package/dist/hooks/prerun.d.ts +6 -0
- package/dist/hooks/prerun.d.ts.map +1 -1
- package/dist/hooks/prerun.js +29 -3
- package/dist/hooks/prerun.js.map +1 -1
- package/dist/lib/dev-workspace/build.d.ts +155 -18
- package/dist/lib/dev-workspace/build.d.ts.map +1 -1
- package/dist/lib/dev-workspace/build.js +522 -39
- package/dist/lib/dev-workspace/build.js.map +1 -1
- package/dist/lib/dev-workspace/detect.d.ts +14 -1
- package/dist/lib/dev-workspace/detect.d.ts.map +1 -1
- package/dist/lib/dev-workspace/detect.js +60 -3
- package/dist/lib/dev-workspace/detect.js.map +1 -1
- package/dist/lib/dev-workspace/doctor.d.ts +17 -1
- package/dist/lib/dev-workspace/doctor.d.ts.map +1 -1
- package/dist/lib/dev-workspace/doctor.js +92 -1
- package/dist/lib/dev-workspace/doctor.js.map +1 -1
- package/dist/lib/dev-workspace/types.d.ts +185 -2
- package/dist/lib/dev-workspace/types.d.ts.map +1 -1
- package/dist/lib/dev-workspace/usage.d.ts.map +1 -1
- package/dist/lib/dev-workspace/usage.js +5 -3
- package/dist/lib/dev-workspace/usage.js.map +1 -1
- package/dist/lib/dynamic-packages.d.ts +31 -0
- package/dist/lib/dynamic-packages.d.ts.map +1 -0
- package/dist/lib/dynamic-packages.js +56 -0
- package/dist/lib/dynamic-packages.js.map +1 -0
- package/oclif.manifest.json +1802 -1742
- package/package.json +22 -21
|
@@ -13,11 +13,18 @@
|
|
|
13
13
|
* so detection filters them out before they reach this module). A member with no
|
|
14
14
|
* workspace file of its own contributes the proven `packages/*` default; the MJ
|
|
15
15
|
* monorepo contributes its 42 nested globs (#3795).
|
|
16
|
-
*
|
|
16
|
+
* The same file carries the pnpm SETTINGS (overrides, patches, peer rules):
|
|
17
|
+
* pnpm 10 reads them only from `pnpm-workspace.yaml` — a `pnpm` block in the
|
|
18
|
+
* root package.json is ignored with a per-field warning that drowns in the
|
|
19
|
+
* install output (946 overrides silently inert).
|
|
20
|
+
* - .npmrc: exactly one settings line. There is deliberately no
|
|
17
21
|
* `public-hoist-pattern[]` block — see "Why no hoist block" below.
|
|
18
|
-
*
|
|
22
|
+
* A package name two members both provide (every MJ-based app repo has a
|
|
23
|
+
* `mj_generatedentities`) gets consumer-scoped `link:` overrides so each
|
|
24
|
+
* member's own consumers keep its own copy — see {@link ResolveDuplicateProviderLinks}.
|
|
25
|
+
* - package.json: private root manifest, pnpm `packageManager` pin, and the
|
|
19
26
|
* devDependency union of member roots (highest version wins, every conflict
|
|
20
|
-
* reported)
|
|
27
|
+
* reported). No `pnpm` block — see pnpm-workspace.yaml above.
|
|
21
28
|
* - turbo.json: copied verbatim from a member, with a minimal fallback.
|
|
22
29
|
*
|
|
23
30
|
* Why no hoist block: the 78-entry `public-hoist-pattern[]` set the manual setup
|
|
@@ -37,7 +44,7 @@
|
|
|
37
44
|
*
|
|
38
45
|
* @module lib/dev-workspace/build
|
|
39
46
|
*/
|
|
40
|
-
import { DeriveLockfilePins } from './lockfile.js';
|
|
47
|
+
import { DeriveLockfilePins, IsResolvedVersion, MajorOf } from './lockfile.js';
|
|
41
48
|
/** Build-scripts allowlist proven on the core-monorepo spike. */
|
|
42
49
|
export const ONLY_BUILT_DEPENDENCIES = [
|
|
43
50
|
'@apollo/protobufjs',
|
|
@@ -57,12 +64,25 @@ export const ONLY_BUILT_DEPENDENCIES = [
|
|
|
57
64
|
'sharp',
|
|
58
65
|
'tesseract.js',
|
|
59
66
|
];
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
/**
|
|
68
|
+
* The one .npmrc settings line. The peer settings that used to sit beside it
|
|
69
|
+
* (`strict-peer-dependencies`, `auto-install-peers`) now live in
|
|
70
|
+
* `pnpm-workspace.yaml` with the rest of the pnpm settings — see
|
|
71
|
+
* {@link PEER_INSTALL_SETTINGS}.
|
|
72
|
+
*/
|
|
73
|
+
export const NPMRC_BASE_LINES = ['package-manager-strict=false'];
|
|
74
|
+
/**
|
|
75
|
+
* Peer-resolution settings for the parent workspace, emitted into
|
|
76
|
+
* `pnpm-workspace.yaml`. Peers are auto-installed but NOT strict: a mixed
|
|
77
|
+
* workspace links MJ from source while a member may still resolve published
|
|
78
|
+
* packages (Skip-Brain's `@mj-biz-apps/*` 5.33) whose peer ranges name an older
|
|
79
|
+
* MJ major than the linked source — a strict install fails on peers no member
|
|
80
|
+
* can fix, while a non-strict one reports them and completes.
|
|
81
|
+
*/
|
|
82
|
+
export const PEER_INSTALL_SETTINGS = {
|
|
83
|
+
strictPeerDependencies: false,
|
|
84
|
+
autoInstallPeers: true,
|
|
85
|
+
};
|
|
66
86
|
/**
|
|
67
87
|
* Packages an MJ library declares as a `peerDependency` because the choice belongs
|
|
68
88
|
* to the app shell, not the library — the auth SDK family especially, where a shell
|
|
@@ -72,6 +92,11 @@ export const NPMRC_BASE_LINES = [
|
|
|
72
92
|
*
|
|
73
93
|
* Kept here (rather than as prose) so the command's guidance text has one source of
|
|
74
94
|
* truth. Grouped by the MJ package whose peer declaration creates the requirement.
|
|
95
|
+
*
|
|
96
|
+
* Scope: this list covers MJ's OWN libraries. Open App client packages declare shell-provided
|
|
97
|
+
* peers too, and they are NOT enumerated here — MJ's CLI does not hardcode knowledge of specific
|
|
98
|
+
* Open App repos. They are DERIVED instead, per shell, by {@link ResolveShellPeerGaps} from the
|
|
99
|
+
* members' committed `mj-app.json` files (#4364).
|
|
75
100
|
*/
|
|
76
101
|
export const SHELL_PROVIDED_PEERS = [
|
|
77
102
|
{
|
|
@@ -152,14 +177,99 @@ function memberGlobLines(member) {
|
|
|
152
177
|
}
|
|
153
178
|
lines.push(negated ? ` - '!${member.Name}/${body}'` : ` - '${member.Name}/${glob}'`);
|
|
154
179
|
}
|
|
180
|
+
// App shells the user admitted for this member with --apps (already validated by SelectAppGlobs).
|
|
181
|
+
for (const glob of member.AppGlobs ?? []) {
|
|
182
|
+
if (glob.startsWith('!') || glob.startsWith('packages/')) {
|
|
183
|
+
throw new Error(`Member ${member.Name} app glob '${glob}' is not an admitted app-shell glob — SelectAppGlobs must run first`);
|
|
184
|
+
}
|
|
185
|
+
lines.push(` - '${member.Name}/${glob}'`);
|
|
186
|
+
}
|
|
187
|
+
return lines;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The #3795 guard, made explicit for admitted app shells: every package an `--apps` glob
|
|
191
|
+
* enumerates must have a name no other workspace package uses, or pnpm would silently pick one.
|
|
192
|
+
* Throws naming both sides; returns the admitted app package names otherwise. Pure.
|
|
193
|
+
*/
|
|
194
|
+
export function AssertAppPackageNamesUnique(members) {
|
|
195
|
+
const isUnder = (relPath, glob) => glob.endsWith('/*') ? relPath.startsWith(`${glob.slice(0, -2)}/`) && !relPath.slice(glob.length - 1).includes('/') : relPath === glob;
|
|
196
|
+
const owners = new Map(); // package name -> "member/relPath"
|
|
197
|
+
const appNames = [];
|
|
198
|
+
for (const member of members) {
|
|
199
|
+
for (const pkg of member.Packages) {
|
|
200
|
+
const name = pkg.PackageJson.name;
|
|
201
|
+
if (!name)
|
|
202
|
+
continue;
|
|
203
|
+
const isApp = (member.AppGlobs ?? []).some((g) => isUnder(pkg.RelPath, g));
|
|
204
|
+
const here = `${member.Name}/${pkg.RelPath}`;
|
|
205
|
+
const other = owners.get(name);
|
|
206
|
+
if (other !== undefined && (isApp || appNames.includes(name))) {
|
|
207
|
+
throw new Error(`--apps: package name '${name}' is declared by both ${other} and ${here} — app-shell names must be unique across the workspace`);
|
|
208
|
+
}
|
|
209
|
+
if (other === undefined)
|
|
210
|
+
owners.set(name, here);
|
|
211
|
+
if (isApp)
|
|
212
|
+
appNames.push(name);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return appNames;
|
|
216
|
+
}
|
|
217
|
+
/** A YAML mapping key: bare when it is a plain identifier (the pnpm setting names), single-quoted otherwise. */
|
|
218
|
+
function yamlKey(key) {
|
|
219
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) ? key : yamlScalar(key);
|
|
220
|
+
}
|
|
221
|
+
/** A YAML scalar: booleans and numbers bare, strings always single-quoted (a `'` doubles). */
|
|
222
|
+
function yamlScalar(value) {
|
|
223
|
+
if (typeof value === 'boolean' || typeof value === 'number')
|
|
224
|
+
return String(value);
|
|
225
|
+
if (typeof value === 'string')
|
|
226
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
227
|
+
throw new Error(`pnpm settings may hold only strings, numbers, booleans, arrays and objects — got ${typeof value}`);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Renders one JSON-shaped record as block-style YAML lines at the given indent.
|
|
231
|
+
* Deliberately tiny: the pnpm settings are strings, booleans, string lists and
|
|
232
|
+
* nested records, nothing else — anything else throws rather than mis-rendering.
|
|
233
|
+
*/
|
|
234
|
+
function yamlMappingLines(record, indent) {
|
|
235
|
+
const pad = ' '.repeat(indent);
|
|
236
|
+
const lines = [];
|
|
237
|
+
for (const [key, value] of Object.entries(record)) {
|
|
238
|
+
if (Array.isArray(value)) {
|
|
239
|
+
if (value.length === 0) {
|
|
240
|
+
lines.push(`${pad}${yamlKey(key)}: []`);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
lines.push(`${pad}${yamlKey(key)}:`);
|
|
244
|
+
for (const item of value)
|
|
245
|
+
lines.push(`${pad} - ${yamlScalar(item)}`);
|
|
246
|
+
}
|
|
247
|
+
else if (value !== null && typeof value === 'object') {
|
|
248
|
+
const nested = value;
|
|
249
|
+
if (Object.keys(nested).length === 0) {
|
|
250
|
+
lines.push(`${pad}${yamlKey(key)}: {}`);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
lines.push(`${pad}${yamlKey(key)}:`);
|
|
254
|
+
lines.push(...yamlMappingLines(nested, indent + 2));
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
lines.push(`${pad}${yamlKey(key)}: ${yamlScalar(value)}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
155
260
|
return lines;
|
|
156
261
|
}
|
|
157
262
|
/**
|
|
158
263
|
* Builds `pnpm-workspace.yaml`: per member (sorted by name) the repo root plus the
|
|
159
264
|
* member's own packages-rooted workspace globs re-prefixed with its directory name.
|
|
160
265
|
* Producer packages only — never `apps/*` (app-shell names collide across repos).
|
|
266
|
+
*
|
|
267
|
+
* `settings` — the assembled pnpm settings from {@link BuildRootPackageJson}
|
|
268
|
+
* (`PnpmSettings`: overrides, patches, peer rules, the peer install switches) —
|
|
269
|
+
* are appended as block YAML. pnpm 10 honours them ONLY in this file; the same
|
|
270
|
+
* block in the root package.json is ignored with a per-field warning.
|
|
161
271
|
*/
|
|
162
|
-
export function BuildWorkspaceYaml(members) {
|
|
272
|
+
export function BuildWorkspaceYaml(members, settings = {}) {
|
|
163
273
|
if (members.length === 0) {
|
|
164
274
|
throw new Error('BuildWorkspaceYaml requires at least one member repo');
|
|
165
275
|
}
|
|
@@ -174,6 +284,10 @@ export function BuildWorkspaceYaml(members) {
|
|
|
174
284
|
for (const member of [...members].sort((a, b) => (a.Name < b.Name ? -1 : a.Name > b.Name ? 1 : 0))) {
|
|
175
285
|
lines.push(...memberGlobLines(member));
|
|
176
286
|
}
|
|
287
|
+
if (Object.keys(settings).length > 0) {
|
|
288
|
+
lines.push('', '# pnpm settings — pnpm 10 reads these ONLY from this file; a package.json#pnpm block is ignored at a workspace root.');
|
|
289
|
+
lines.push(...yamlMappingLines(settings, 0));
|
|
290
|
+
}
|
|
177
291
|
return `${lines.join('\n')}\n`;
|
|
178
292
|
}
|
|
179
293
|
/**
|
|
@@ -190,7 +304,7 @@ export function BuildShellPeerGuidance() {
|
|
|
190
304
|
return lines;
|
|
191
305
|
}
|
|
192
306
|
/**
|
|
193
|
-
* Builds `.npmrc`: exactly the
|
|
307
|
+
* Builds `.npmrc`: exactly the one settings line pnpm still needs there.
|
|
194
308
|
*
|
|
195
309
|
* No `public-hoist-pattern[]` block by design — see the "Why no hoist block" note
|
|
196
310
|
* at the top of this module.
|
|
@@ -312,6 +426,10 @@ function workspaceName(parentDirName) {
|
|
|
312
426
|
const cleaned = parentDirName.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
313
427
|
return `${cleaned.length > 0 ? cleaned : 'mj'}-dev-workspace`;
|
|
314
428
|
}
|
|
429
|
+
/** Members in plain codepoint order by name — the order every derivation resolves ties in. */
|
|
430
|
+
function sortedByName(members) {
|
|
431
|
+
return [...members].sort((a, b) => (a.Name < b.Name ? -1 : a.Name > b.Name ? 1 : 0));
|
|
432
|
+
}
|
|
315
433
|
/**
|
|
316
434
|
* Collects every package name the workspace members provide, from the members'
|
|
317
435
|
* OWN package enumerations. These names get `workspace:*` overrides in the
|
|
@@ -323,7 +441,7 @@ function workspaceName(parentDirName) {
|
|
|
323
441
|
*/
|
|
324
442
|
export function CollectFamilyPackages(members) {
|
|
325
443
|
const providers = new Map();
|
|
326
|
-
for (const member of
|
|
444
|
+
for (const member of sortedByName(members)) {
|
|
327
445
|
for (const pkg of member.Packages) {
|
|
328
446
|
const name = pkg.PackageJson.name;
|
|
329
447
|
if (name === undefined || name.length === 0)
|
|
@@ -340,6 +458,252 @@ export function CollectFamilyPackages(members) {
|
|
|
340
458
|
}
|
|
341
459
|
return { Names: [...providers.keys()].sort(), Duplicates: duplicates.sort((a, b) => (a.Package < b.Package ? -1 : 1)) };
|
|
342
460
|
}
|
|
461
|
+
/** True when any dependency section of the manifest names `name`. */
|
|
462
|
+
function declaresDependency(manifest, name) {
|
|
463
|
+
return [manifest.dependencies, manifest.devDependencies, manifest.peerDependencies].some((section) => section !== undefined && name in section);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Keeps each member's OWN copy of a duplicated package name in front of that
|
|
467
|
+
* member's own consumers. A plain `name: workspace:*` override can only pick
|
|
468
|
+
* one provider (pnpm takes the first by sort order), so every MJ-based app repo's
|
|
469
|
+
* `mj_generatedentities` / `mj_generatedactions` collided as soon as two such
|
|
470
|
+
* repos shared a workspace: Skip-Brain's packages type-checked against MJ's
|
|
471
|
+
* generated entities and could not find their own.
|
|
472
|
+
* pnpm's `parent>child` override selector with a root-relative `link:` value
|
|
473
|
+
* scopes the fix to the consumer: `@skip-brain/core>mj_generatedentities:
|
|
474
|
+
* link:Skip-Brain/packages/GeneratedEntities` (verified live).
|
|
475
|
+
*
|
|
476
|
+
* Two consumer shapes cannot be linked and are reported instead: a consumer whose
|
|
477
|
+
* own name is duplicated (the selector would hit every copy of it), and a consumer
|
|
478
|
+
* in a member that provides no copy (nothing of its own to link to — it gets the
|
|
479
|
+
* sort-order provider like before). Pure; deterministic over sorted names.
|
|
480
|
+
*/
|
|
481
|
+
export function ResolveDuplicateProviderLinks(members, duplicates) {
|
|
482
|
+
const duplicatedNames = new Set(duplicates.map((d) => d.Package));
|
|
483
|
+
const overrides = {};
|
|
484
|
+
const links = [];
|
|
485
|
+
const unlinked = [];
|
|
486
|
+
const sortedMembers = [...members].sort((a, b) => (a.Name < b.Name ? -1 : a.Name > b.Name ? 1 : 0));
|
|
487
|
+
for (const dup of [...duplicates].sort((a, b) => (a.Package < b.Package ? -1 : 1))) {
|
|
488
|
+
for (const member of sortedMembers) {
|
|
489
|
+
const provider = [...member.Packages]
|
|
490
|
+
.filter((p) => p.PackageJson.name === dup.Package)
|
|
491
|
+
.sort((a, b) => (a.RelPath < b.RelPath ? -1 : 1))[0];
|
|
492
|
+
for (const consumer of [...member.Packages].sort((a, b) => (a.RelPath < b.RelPath ? -1 : 1))) {
|
|
493
|
+
const consumerName = consumer.PackageJson.name;
|
|
494
|
+
if (consumerName === undefined || consumerName === dup.Package || !declaresDependency(consumer.PackageJson, dup.Package))
|
|
495
|
+
continue;
|
|
496
|
+
if (provider === undefined) {
|
|
497
|
+
unlinked.push({ Consumer: consumerName, Package: dup.Package, Repo: member.Name, Reason: 'no-own-copy' });
|
|
498
|
+
}
|
|
499
|
+
else if (duplicatedNames.has(consumerName)) {
|
|
500
|
+
unlinked.push({ Consumer: consumerName, Package: dup.Package, Repo: member.Name, Reason: 'ambiguous-consumer' });
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
const target = `link:${member.Name}/${provider.RelPath}`;
|
|
504
|
+
overrides[`${consumerName}>${dup.Package}`] = target;
|
|
505
|
+
links.push({ Consumer: consumerName, Package: dup.Package, Target: target, Repo: member.Name });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return { Overrides: sortedRecord(overrides), Links: links, Unlinked: unlinked };
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Indexes every package the workspace members provide, by package name. One lookup structure
|
|
514
|
+
* serves both "does the workspace provide this?" (what {@link CollectFamilyPackages} answers with
|
|
515
|
+
* a name list) and "what does this package declare?" — the peer resolution in
|
|
516
|
+
* {@link ResolveShellPeerGaps} needs the manifest, not just the name.
|
|
517
|
+
*/
|
|
518
|
+
export function IndexWorkspacePackages(members) {
|
|
519
|
+
const index = new Map();
|
|
520
|
+
for (const member of sortedByName(members)) {
|
|
521
|
+
for (const pkg of member.Packages) {
|
|
522
|
+
const name = pkg.PackageJson.name;
|
|
523
|
+
if (name === undefined || name.length === 0)
|
|
524
|
+
continue;
|
|
525
|
+
if (!index.has(name))
|
|
526
|
+
index.set(name, pkg.PackageJson);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return index;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* The client-side packages the members' own `mj-app.json` files declare.
|
|
533
|
+
*
|
|
534
|
+
* These are invisible to every other derivation in this module: nothing in the tree DEPENDS on
|
|
535
|
+
* them. A host registers them (`dynamicPackages.client`) and `mj codegen manifest` appends a
|
|
536
|
+
* side-effect import to the app shell's generated class-registrations manifest — so the shell
|
|
537
|
+
* imports a package it never declared, and pnpm, correctly, never linked (#4364). Emitting them as
|
|
538
|
+
* parent dependencies is the only place that can be fixed.
|
|
539
|
+
*
|
|
540
|
+
* Deliberately registration-INDEPENDENT: a member's client package is collected whether or not a
|
|
541
|
+
* host has registered it, per the linking ⊥ registration axiom
|
|
542
|
+
* (`guides/OPEN_APP_WORKSPACE_LINKING_SPEC.md` §17). Over-linking is the sanctioned direction —
|
|
543
|
+
* an unregistered package resolves but does not load.
|
|
544
|
+
*/
|
|
545
|
+
/**
|
|
546
|
+
* Validates and returns one declared package array from a member's `mj-app.json`.
|
|
547
|
+
*
|
|
548
|
+
* `mj-app.json` is committed in a SIBLING repo and hand-editable, so its parsed shape is genuinely
|
|
549
|
+
* unknown here — {@link MjAppJson} is an assertion about a file this repo does not own, and
|
|
550
|
+
* `readJsonFile` validates only that the TEXT parses. Narrowing it with real guards is what makes
|
|
551
|
+
* that type true for every consumer downstream, and it is the same fail-fast-and-name-the-file
|
|
552
|
+
* contract `readJsonFile` already sets ("Unparseable JSON at <path>"). Without it a missing `name`
|
|
553
|
+
* surfaced as `Cannot read properties of undefined (reading 'length')`, which names neither the
|
|
554
|
+
* repo nor the file — and a developer may have a dozen of them.
|
|
555
|
+
*/
|
|
556
|
+
function readDeclaredEntries(member, section) {
|
|
557
|
+
const declared = member.MjAppJson?.packages?.[section];
|
|
558
|
+
if (declared === undefined || declared === null)
|
|
559
|
+
return [];
|
|
560
|
+
const where = `${member.Name}/mj-app.json: packages.${section}`;
|
|
561
|
+
if (!Array.isArray(declared)) {
|
|
562
|
+
throw new Error(`${where} must be an array of package entries, got ${typeof declared}`);
|
|
563
|
+
}
|
|
564
|
+
for (const [index, entry] of declared.entries()) {
|
|
565
|
+
if (entry === null || typeof entry !== 'object') {
|
|
566
|
+
throw new Error(`${where}[${index}] must be an object, got ${entry === null ? 'null' : typeof entry}`);
|
|
567
|
+
}
|
|
568
|
+
if (typeof entry.name !== 'string') {
|
|
569
|
+
throw new Error(`${where}[${index}] has no "name" string — a package entry must name its package`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return declared;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Every package a member declares that an app shell will be asked to import.
|
|
576
|
+
*
|
|
577
|
+
* This set mirrors the HOST's rule exactly, because the host is the only thing that decides what
|
|
578
|
+
* the shell imports. `GetClientPackagesFromManifest`
|
|
579
|
+
* (`packages/OpenApp/Engine/src/install/config-manager.ts`) builds the client dynamic-package list
|
|
580
|
+
* as `[...packages.client, ...packages.shared]` with **no role test** — its own comment: "every
|
|
581
|
+
* client/shared package is emitted regardless of startupExport — client entries are side-effect
|
|
582
|
+
* imports" — and `mj codegen manifest --open-app-client-bootstrap` turns every enabled entry into
|
|
583
|
+
* an import in the shell's generated class-registrations manifest, with no role field even present
|
|
584
|
+
* on its entry type.
|
|
585
|
+
*
|
|
586
|
+
* So there is deliberately no `role` filter here. An earlier revision kept only `role: 'bootstrap'`
|
|
587
|
+
* on the premise that other roles "are imported normally"; the host contradicts that, and `role`
|
|
588
|
+
* is a required seven-value enum whose `components` / `module` members are the documented Angular
|
|
589
|
+
* roles. Anything narrower leaves a schema-valid package imported by the shell and linked by
|
|
590
|
+
* nobody — the exact page-load-with-a-green-build failure this module exists to prevent (#4364).
|
|
591
|
+
*
|
|
592
|
+
* `packages.server[]` is NOT here, and that is not an oversight: the host routes it to
|
|
593
|
+
* `dynamicPackages.server`, a Node process that resolves importer-relative rather than from the
|
|
594
|
+
* vite root, so it is not part of the shell's resolution problem.
|
|
595
|
+
*/
|
|
596
|
+
function readShellImportedEntries(member) {
|
|
597
|
+
return [...readDeclaredEntries(member, 'client'), ...readDeclaredEntries(member, 'shared')];
|
|
598
|
+
}
|
|
599
|
+
export function CollectOpenAppClientPackages(members, workspacePackages) {
|
|
600
|
+
const collected = new Map();
|
|
601
|
+
const declaredBy = new Map();
|
|
602
|
+
for (const member of sortedByName(members)) {
|
|
603
|
+
// Detection carried this rather than throwing, so an excluded sibling's broken file could not
|
|
604
|
+
// abort the command. This is the consumer that actually reads the declaration, so it raises.
|
|
605
|
+
if (member.MjAppJsonError) {
|
|
606
|
+
throw new Error(`${member.Name}/mj-app.json could not be read: ${member.MjAppJsonError}`);
|
|
607
|
+
}
|
|
608
|
+
const seenInMember = new Set();
|
|
609
|
+
for (const entry of readShellImportedEntries(member)) {
|
|
610
|
+
if (entry.name.length === 0)
|
|
611
|
+
continue;
|
|
612
|
+
// One repo naming a package in BOTH client[] and shared[] is not an ambiguity — it is one
|
|
613
|
+
// member declaring one package twice, and the link target is not in question.
|
|
614
|
+
if (!seenInMember.has(entry.name)) {
|
|
615
|
+
seenInMember.add(entry.name);
|
|
616
|
+
declaredBy.set(entry.name, [...(declaredBy.get(entry.name) ?? []), member.Name]);
|
|
617
|
+
}
|
|
618
|
+
if (collected.has(entry.name))
|
|
619
|
+
continue;
|
|
620
|
+
collected.set(entry.name, { Package: entry.name, Repo: member.Name, Provided: workspacePackages.has(entry.name) });
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const byPackage = (a, b) => (a.Package < b.Package ? -1 : a.Package > b.Package ? 1 : 0);
|
|
624
|
+
return {
|
|
625
|
+
Packages: [...collected.values()].sort(byPackage),
|
|
626
|
+
Duplicates: [...declaredBy.entries()]
|
|
627
|
+
.filter(([, repos]) => repos.length > 1)
|
|
628
|
+
.map(([Package, Repos]) => ({ Package, Repos }))
|
|
629
|
+
.sort(byPackage),
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
/** The prefix every MJ Angular library carries — the surface an Open App client package registers into. */
|
|
633
|
+
const MJ_ANGULAR_PREFIX = '@memberjunction/ng-';
|
|
634
|
+
/**
|
|
635
|
+
* The MJ Angular app shells the workspace enumerates — the shells that could actually host an Open
|
|
636
|
+
* App client package.
|
|
637
|
+
*
|
|
638
|
+
* Structural, with no configuration, and all three conditions earn their place:
|
|
639
|
+
* - depends on `@angular/core` — a LIBRARY takes it as a peer, so this alone separates app from library;
|
|
640
|
+
* - carries an Angular application builder (`@angular/cli` / `@angular/build`) in devDependencies;
|
|
641
|
+
* - declares at least one `@memberjunction/ng-*` package — it has an MJ Angular surface.
|
|
642
|
+
*
|
|
643
|
+
* The third condition is not decoration. Without it the sole other Angular app in the MJ monorepo,
|
|
644
|
+
* `mj_angular_elements_demo`, is reported as missing five peers of packages it will never load —
|
|
645
|
+
* five warnings on every regenerate, which is how a report teaches people to stop reading it. It
|
|
646
|
+
* declares zero `@memberjunction/ng-*` packages while MJExplorer declares 22, and an Open App
|
|
647
|
+
* client package peer-depends on eleven of them, so a shell with none cannot be hosting one. It is
|
|
648
|
+
* a predicate, deliberately not a count threshold.
|
|
649
|
+
*
|
|
650
|
+
* Scope, stated rather than implied: only packages the workspace enumerates are candidates, and
|
|
651
|
+
* detection filters members' `apps/*` globs out because every Open App repo names its shells
|
|
652
|
+
* `mj_api`/`mj_explorer` and they collide (#3795). A shell under `apps/` is outside the workspace
|
|
653
|
+
* by design and is not checked here.
|
|
654
|
+
*/
|
|
655
|
+
export function CollectWorkspaceShells(members) {
|
|
656
|
+
const shells = [];
|
|
657
|
+
for (const member of sortedByName(members)) {
|
|
658
|
+
for (const pkg of member.Packages) {
|
|
659
|
+
const { name, dependencies = {}, devDependencies = {} } = pkg.PackageJson;
|
|
660
|
+
if (name === undefined || name.length === 0)
|
|
661
|
+
continue;
|
|
662
|
+
const isApp = dependencies['@angular/core'] !== undefined;
|
|
663
|
+
const hasBuilder = devDependencies['@angular/cli'] !== undefined || devDependencies['@angular/build'] !== undefined;
|
|
664
|
+
const hostsMJAngular = Object.keys(dependencies).some((dep) => dep.startsWith(MJ_ANGULAR_PREFIX));
|
|
665
|
+
if (!isApp || !hasBuilder || !hostsMJAngular)
|
|
666
|
+
continue;
|
|
667
|
+
shells.push({
|
|
668
|
+
Name: name,
|
|
669
|
+
RelPath: pkg.RelPath,
|
|
670
|
+
Repo: member.Name,
|
|
671
|
+
Declares: [...Object.keys(dependencies), ...Object.keys(devDependencies)].sort(),
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return shells.sort((a, b) => (a.Name < b.Name ? -1 : a.Name > b.Name ? 1 : 0));
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Peers of the registered client-side packages that a shell cannot resolve.
|
|
679
|
+
*
|
|
680
|
+
* A peer is a gap for a shell when the workspace does not PROVIDE it and the shell does not
|
|
681
|
+
* DECLARE it. Both halves are required: without the first, every `@memberjunction/*` peer is
|
|
682
|
+
* noise; without the second, `@angular/core` is a false positive in every shell.
|
|
683
|
+
*
|
|
684
|
+
* Why this must not be auto-added to the parent manifest: an Angular dev server externalizes
|
|
685
|
+
* `@angular/*` and resolves it from the VITE ROOT (the shell), so the copy `auto-install-peers`
|
|
686
|
+
* places in the consumer's own tree is never consulted — measured, with a control, on 2026-09-11.
|
|
687
|
+
* The fix belongs in the shell's own tracked package.json, which is what {@link SHELL_PROVIDED_PEERS}
|
|
688
|
+
* has always said and what MJExplorer already does for `@angular/service-worker`.
|
|
689
|
+
*/
|
|
690
|
+
export function ResolveShellPeerGaps(clientPackages, shells, workspacePackages, overrides) {
|
|
691
|
+
const gaps = [];
|
|
692
|
+
for (const shell of shells) {
|
|
693
|
+
const declares = new Set(shell.Declares);
|
|
694
|
+
for (const client of clientPackages) {
|
|
695
|
+
const manifest = workspacePackages.get(client.Package);
|
|
696
|
+
if (manifest === undefined)
|
|
697
|
+
continue; // unprovided: reported by OpenAppClientPackages, nothing to resolve
|
|
698
|
+
for (const [peer, range] of Object.entries(manifest.peerDependencies ?? {})) {
|
|
699
|
+
if (workspacePackages.has(peer) || declares.has(peer))
|
|
700
|
+
continue;
|
|
701
|
+
gaps.push({ Shell: shell.Name, Package: client.Package, Peer: peer, Range: range, Pin: findPeerPin(overrides, peer, range) });
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return gaps;
|
|
706
|
+
}
|
|
343
707
|
/** Records a first-member-wins value, reporting any differing later declaration as a conflict. */
|
|
344
708
|
function recordFirstWins(chosen, conflicts, key, candidate) {
|
|
345
709
|
const incumbent = chosen.get(key);
|
|
@@ -420,6 +784,31 @@ function overrideKeyName(key) {
|
|
|
420
784
|
const at = key.lastIndexOf('@');
|
|
421
785
|
return at <= 0 ? key : key.slice(0, at);
|
|
422
786
|
}
|
|
787
|
+
/** First integer in a version or range — `^21.1.3` -> `21`, `21.2.22` -> `21`. Null when there is none. */
|
|
788
|
+
function majorOf(versionOrRange) {
|
|
789
|
+
return /(\d+)/.exec(versionOrRange)?.[1] ?? null;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* The exact version the parent's assembled overrides pin for a peer, or null when nothing does.
|
|
793
|
+
*
|
|
794
|
+
* Must not be a bare `overrides[peer]` lookup. {@link DeriveLockfilePins} emits a bare key ONLY
|
|
795
|
+
* when a package resolves to a single major across every member's graph; the moment two majors
|
|
796
|
+
* exist anywhere it emits per-major selector keys instead (`chalk@^5` / `chalk@^4`). A bare lookup
|
|
797
|
+
* misses those entirely and the command then reports "nothing in the parent pins it" for a peer the
|
|
798
|
+
* parent does pin — inverting the one actionable fact the warning carries. When several majors are
|
|
799
|
+
* pinned, the useful one is the major the client package actually asks for.
|
|
800
|
+
*/
|
|
801
|
+
function findPeerPin(overrides, peer, range) {
|
|
802
|
+
const exact = overrides[peer];
|
|
803
|
+
if (exact !== undefined)
|
|
804
|
+
return exact;
|
|
805
|
+
const matches = Object.entries(overrides).filter(([key]) => overrideKeyName(key) === peer);
|
|
806
|
+
if (matches.length === 0)
|
|
807
|
+
return null;
|
|
808
|
+
const wantedMajor = majorOf(range);
|
|
809
|
+
const sameMajor = matches.find(([key]) => majorOf(key.slice(peer.length)) === wantedMajor);
|
|
810
|
+
return (sameMajor ?? matches[0])[1];
|
|
811
|
+
}
|
|
423
812
|
/** Removes every pin entry (plain or per-major selector) for a name; records what was displaced. */
|
|
424
813
|
function displacePinsForName(overrides, name, newValue, superseded) {
|
|
425
814
|
for (const key of Object.keys(overrides)) {
|
|
@@ -431,14 +820,51 @@ function displacePinsForName(overrides, name, newValue, superseded) {
|
|
|
431
820
|
}
|
|
432
821
|
}
|
|
433
822
|
/**
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
823
|
+
* Pins a patched package to the exact version its patch is keyed to. A patch
|
|
824
|
+
* `name@version` only ever applies to that version: letting a lockfile pin or a
|
|
825
|
+
* member override resolve `name` elsewhere leaves the patch inert (pnpm merely
|
|
826
|
+
* warns "patches were not used" — SaaS's committed
|
|
827
|
+
* `type-graphql@2.0.0-rc.3` out-voted MJ's patched `2.0.0-beta.3`, and every
|
|
828
|
+
* consumer of MJ's server then type-checked against the wrong type-graphql).
|
|
829
|
+
* The pin takes the per-major selector shape when other majors of the name are
|
|
830
|
+
* already pinned (never forcing another major), the plain name otherwise.
|
|
831
|
+
* Returns the override key it wrote, or null when the patch key is not pinnable
|
|
832
|
+
* (a range or a bare name — pnpm allows those; there is nothing exact to pin).
|
|
833
|
+
*/
|
|
834
|
+
function pinPatchedPackage(overrides, patchKey, superseded) {
|
|
835
|
+
const name = overrideKeyName(patchKey);
|
|
836
|
+
const version = name === patchKey ? '' : patchKey.slice(name.length + 1);
|
|
837
|
+
if (!IsResolvedVersion(version))
|
|
838
|
+
return null;
|
|
839
|
+
const selector = `${name}@^${MajorOf(version)}`;
|
|
840
|
+
const otherSelectors = Object.keys(overrides).filter((key) => key !== name && overrideKeyName(key) === name);
|
|
841
|
+
const target = otherSelectors.length > 0 ? selector : name;
|
|
842
|
+
if (target === selector && name in overrides) {
|
|
843
|
+
// a plain-name entry and a selector must not fight: the plain entry gives way
|
|
844
|
+
if (overrides[name] !== version)
|
|
845
|
+
superseded.add(name);
|
|
846
|
+
delete overrides[name];
|
|
847
|
+
}
|
|
848
|
+
else if (target === name) {
|
|
849
|
+
displacePinsForName(overrides, name, version, superseded);
|
|
850
|
+
}
|
|
851
|
+
if (target in overrides && overrides[target] !== version)
|
|
852
|
+
superseded.add(target);
|
|
853
|
+
overrides[target] = version;
|
|
854
|
+
return target;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Layers the override sources into the parent overrides, weakest first:
|
|
858
|
+
* lockfile-derived pins < explicit member overrides < patched-package pins <
|
|
859
|
+
* family `workspace:*` (local source always wins). A member's or family's
|
|
860
|
+
* whole-name entry displaces every per-major pin selector for that name (a plain
|
|
861
|
+
* key and a `name@^N` selector must not fight). Displacements are returned so
|
|
862
|
+
* the command reports them — nothing is overwritten silently.
|
|
863
|
+
*
|
|
864
|
+
* `patchedPackages` are the members' `patchedDependencies` keys (`name@version`);
|
|
865
|
+
* see {@link pinPatchedPackage} for why a patch outranks an override.
|
|
440
866
|
*/
|
|
441
|
-
export function AssembleParentOverrides(lockfilePins, memberOverrides, familyNames) {
|
|
867
|
+
export function AssembleParentOverrides(lockfilePins, memberOverrides, familyNames, patchedPackages = []) {
|
|
442
868
|
const overrides = { ...lockfilePins };
|
|
443
869
|
const superseded = new Set();
|
|
444
870
|
for (const [key, version] of Object.entries(memberOverrides)) {
|
|
@@ -451,66 +877,117 @@ export function AssembleParentOverrides(lockfilePins, memberOverrides, familyNam
|
|
|
451
877
|
}
|
|
452
878
|
overrides[key] = version;
|
|
453
879
|
}
|
|
880
|
+
const patchPins = new Set();
|
|
881
|
+
for (const patchKey of [...patchedPackages].sort()) {
|
|
882
|
+
const written = pinPatchedPackage(overrides, patchKey, superseded);
|
|
883
|
+
if (written !== null)
|
|
884
|
+
patchPins.add(written);
|
|
885
|
+
}
|
|
454
886
|
for (const name of familyNames) {
|
|
455
887
|
displacePinsForName(overrides, name, 'workspace:*', superseded);
|
|
456
888
|
overrides[name] = 'workspace:*';
|
|
889
|
+
for (const key of [...patchPins])
|
|
890
|
+
if (overrideKeyName(key) === name)
|
|
891
|
+
patchPins.delete(key); // source beats patch
|
|
457
892
|
}
|
|
458
|
-
return { Overrides: sortedRecord(overrides), SupersededPins: [...superseded].sort() };
|
|
893
|
+
return { Overrides: sortedRecord(overrides), SupersededPins: [...superseded].sort(), PatchPins: [...patchPins].sort() };
|
|
459
894
|
}
|
|
460
|
-
/**
|
|
461
|
-
|
|
895
|
+
/**
|
|
896
|
+
* Builds the pnpm settings for `pnpm-workspace.yaml` from the assembled parts,
|
|
897
|
+
* omitting empty sections: the peer install switches, the peer bridge rules, the
|
|
898
|
+
* layered overrides, hoisted patches and package extensions. Key order is fixed
|
|
899
|
+
* so a regenerated workspace is byte-identical.
|
|
900
|
+
*/
|
|
901
|
+
export function BuildPnpmWorkspaceSettings(overrides, blocks) {
|
|
462
902
|
const peerDependencyRules = {
|
|
463
903
|
allowedVersions: sortedRecord({ ...PEER_DEPENDENCY_RULES.allowedVersions, ...blocks.PeerAllowedVersions }),
|
|
464
904
|
ignoreMissing: [...new Set([...PEER_DEPENDENCY_RULES.ignoreMissing, ...blocks.PeerIgnoreMissing])].sort(),
|
|
465
905
|
};
|
|
466
|
-
const
|
|
906
|
+
const settings = { ...PEER_INSTALL_SETTINGS, peerDependencyRules };
|
|
467
907
|
if (Object.keys(overrides).length > 0)
|
|
468
|
-
|
|
908
|
+
settings.overrides = overrides;
|
|
469
909
|
if (Object.keys(blocks.PatchedDependencies).length > 0) {
|
|
470
|
-
|
|
910
|
+
settings.patchedDependencies = blocks.PatchedDependencies;
|
|
471
911
|
// A member patch is keyed to pkg@version; when the parent graph never resolves
|
|
472
912
|
// that exact version, pnpm hard-fails the WHOLE install with ERR_PNPM_UNUSED_PATCH.
|
|
473
913
|
// One member's stale patch must not brick every member's workspace — allow it,
|
|
474
|
-
// and rely on the assembly report, which names every hoisted patch.
|
|
475
|
-
|
|
914
|
+
// and rely on the assembly report, which names every hoisted patch. (The
|
|
915
|
+
// patched-package pins in AssembleParentOverrides make this the rare case.)
|
|
916
|
+
settings.allowUnusedPatches = true;
|
|
476
917
|
}
|
|
477
918
|
if (Object.keys(blocks.PackageExtensions).length > 0)
|
|
478
|
-
|
|
479
|
-
return
|
|
919
|
+
settings.packageExtensions = blocks.PackageExtensions;
|
|
920
|
+
return settings;
|
|
480
921
|
}
|
|
481
922
|
/**
|
|
482
|
-
* Builds the private parent `package.json
|
|
483
|
-
* devDependency union
|
|
484
|
-
* patches hoisted, lockfile-derived pins,
|
|
485
|
-
*
|
|
923
|
+
* Builds the private parent `package.json` — pnpm pin and the cleaned member
|
|
924
|
+
* devDependency union — and, alongside it, the pnpm settings the workspace yaml
|
|
925
|
+
* carries: member overrides and patches hoisted, lockfile-derived pins,
|
|
926
|
+
* patched-package pins, and `workspace:*` overrides for every member-provided
|
|
927
|
+
* package. The settings are returned rather than written into the manifest
|
|
928
|
+
* because pnpm 10 ignores a `pnpm` block at a workspace root. Every decision
|
|
929
|
+
* lands in the returned Report.
|
|
486
930
|
*/
|
|
931
|
+
/**
|
|
932
|
+
* The parent manifest's `dependencies`: every member-PROVIDED client-side package at
|
|
933
|
+
* `workspace:*`.
|
|
934
|
+
*
|
|
935
|
+
* This key is what puts the package at the parent's `node_modules` root, which is the only place
|
|
936
|
+
* an app shell that never declared it can resolve it from. An unprovided package is skipped — a
|
|
937
|
+
* `workspace:*` specifier on a package nothing provides is unresolvable and fails the install, the
|
|
938
|
+
* same reasoning as the devDependency union's `DroppedWorkspace` branch.
|
|
939
|
+
*/
|
|
940
|
+
export function BuildClientDependencies(clientPackages) {
|
|
941
|
+
const deps = {};
|
|
942
|
+
for (const client of clientPackages) {
|
|
943
|
+
if (client.Provided)
|
|
944
|
+
deps[client.Package] = 'workspace:*';
|
|
945
|
+
}
|
|
946
|
+
return deps;
|
|
947
|
+
}
|
|
487
948
|
export function BuildRootPackageJson(parentDirName, members) {
|
|
488
949
|
if (members.length === 0) {
|
|
489
950
|
throw new Error('BuildRootPackageJson requires at least one member repo');
|
|
490
951
|
}
|
|
952
|
+
const workspacePackages = IndexWorkspacePackages(members);
|
|
953
|
+
const clients = CollectOpenAppClientPackages(members, workspacePackages);
|
|
954
|
+
const clientPackages = clients.Packages;
|
|
955
|
+
const clientDependencies = BuildClientDependencies(clientPackages);
|
|
491
956
|
const family = CollectFamilyPackages(members);
|
|
492
957
|
const union = ResolveDevDependencyUnion(members, new Set(family.Names));
|
|
958
|
+
// A provided client package is by definition family-provided, so classifyDevDep already mapped it
|
|
959
|
+
// to workspace:* in the union — and a member devDepending on its own client package is ordinary.
|
|
960
|
+
// Emitting it in both blocks of a GENERATED manifest is noise, not a conflict (identical
|
|
961
|
+
// specifier), so `dependencies` keeps it: that block is what puts the package at the parent root,
|
|
962
|
+
// which is the whole point of registering it.
|
|
963
|
+
for (const name of Object.keys(clientDependencies))
|
|
964
|
+
delete union.DevDependencies[name];
|
|
493
965
|
const pins = DeriveLockfilePins(members.flatMap((m) => (m.Lockfile !== null && m.Lockfile.Kind !== 'unsupported' ? [{ Repo: m.Name, Lockfile: m.Lockfile }] : [])), new Set(family.Names));
|
|
494
966
|
const blocks = ResolveMemberPnpmBlocks(members);
|
|
495
|
-
const assembled = AssembleParentOverrides(pins.Pins, blocks.Overrides, family.Names);
|
|
967
|
+
const assembled = AssembleParentOverrides(pins.Pins, blocks.Overrides, family.Names, Object.keys(blocks.PatchedDependencies));
|
|
968
|
+
const duplicateLinks = ResolveDuplicateProviderLinks(members, family.Duplicates);
|
|
969
|
+
const shellPeerGaps = ResolveShellPeerGaps(clientPackages, CollectWorkspaceShells(members), workspacePackages, assembled.Overrides);
|
|
496
970
|
const { Pin, Source } = ResolvePnpmPin(members);
|
|
497
971
|
const manifest = {
|
|
498
972
|
name: workspaceName(parentDirName),
|
|
499
973
|
private: true,
|
|
500
974
|
packageManager: Pin,
|
|
975
|
+
// Omitted entirely when empty, so a parent with no Open App member regenerates byte-identically.
|
|
976
|
+
...(Object.keys(clientDependencies).length > 0 ? { dependencies: clientDependencies } : {}),
|
|
501
977
|
devDependencies: union.DevDependencies,
|
|
502
|
-
pnpm: buildPnpmBlock(assembled.Overrides, blocks),
|
|
503
978
|
};
|
|
504
979
|
return {
|
|
505
980
|
Content: `${JSON.stringify(manifest, null, 2)}\n`,
|
|
981
|
+
// scoped `consumer>package` keys sit beside the plain ones — they never collide
|
|
982
|
+
PnpmSettings: BuildPnpmWorkspaceSettings(sortedRecord({ ...assembled.Overrides, ...duplicateLinks.Overrides }), blocks),
|
|
506
983
|
Conflicts: union.Conflicts,
|
|
507
984
|
PinSource: Source,
|
|
508
985
|
Pin,
|
|
509
|
-
Report: buildManifestReport(members, family, union, pins, blocks, assembled
|
|
986
|
+
Report: buildManifestReport(members, family, union, pins, blocks, assembled, duplicateLinks, clients, shellPeerGaps),
|
|
510
987
|
};
|
|
511
988
|
}
|
|
512
989
|
/** Assembles the absorption report — one place, so nothing the build decided goes unreported. */
|
|
513
|
-
function buildManifestReport(members, family, union, pins, blocks,
|
|
990
|
+
function buildManifestReport(members, family, union, pins, blocks, assembled, duplicateLinks, clients, shellPeerGaps) {
|
|
514
991
|
const lockfileSkips = members.flatMap((m) => m.Lockfile !== null && m.Lockfile.Kind !== 'unsupported' ? m.Lockfile.Skipped.map((skip) => ({ Repo: m.Name, Skip: skip })) : []);
|
|
515
992
|
const unsupportedLockfiles = members.flatMap((m) => m.Lockfile !== null && m.Lockfile.Kind === 'unsupported' ? [{ Repo: m.Name, File: m.Lockfile.File, Version: m.Lockfile.Version }] : []);
|
|
516
993
|
return {
|
|
@@ -523,9 +1000,15 @@ function buildManifestReport(members, family, union, pins, blocks, supersededPin
|
|
|
523
1000
|
Patches: blocks.Patches,
|
|
524
1001
|
FamilyOverrideCount: family.Names.length,
|
|
525
1002
|
DuplicateFamilyPackages: family.Duplicates,
|
|
1003
|
+
DuplicateProviderLinks: duplicateLinks.Links,
|
|
1004
|
+
UnlinkedDuplicateConsumers: duplicateLinks.Unlinked,
|
|
526
1005
|
SkippedTypesDevDeps: [...union.SkippedTypes].sort(),
|
|
527
1006
|
DroppedWorkspaceDevDeps: union.DroppedWorkspace,
|
|
528
|
-
SupersededPins:
|
|
1007
|
+
SupersededPins: assembled.SupersededPins,
|
|
1008
|
+
PatchPins: assembled.PatchPins,
|
|
1009
|
+
OpenAppClientPackages: clients.Packages,
|
|
1010
|
+
ShellPeerGaps: shellPeerGaps,
|
|
1011
|
+
DuplicateClientPackages: clients.Duplicates,
|
|
529
1012
|
};
|
|
530
1013
|
}
|
|
531
1014
|
/**
|