@ws-test-realm/admin-kit 0.1.8 → 0.1.11

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/bin/ws-modules.js CHANGED
@@ -142,10 +142,11 @@ async function main() {
142
142
  process.exit(1);
143
143
  }
144
144
 
145
+ // No positional but step flags present → implicit `all`. Lets npm scripts
146
+ // like `"wire": "ws-modules --build --pack --deploy"` run cleanly without
147
+ // requiring callers to remember `-- all`.
145
148
  if (!argv.positional.length) {
146
- console.error("ws-modules: need module names or `all`.");
147
- console.error(USAGE);
148
- process.exit(1);
149
+ argv.positional.push("all");
149
150
  }
150
151
 
151
152
  let restrictTo;
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ // ws-purge — strip prior federation contributions across an admin-modules
3
+ // workspace's deploy targets (jars + ext dirs) + the host's .federation/<id>/
4
+ // types dirs. Mirrors `ws-modules --deploy`'s target-resolution exactly so the
5
+ // two stay in lockstep (same wsconfig override > package.json rules).
6
+ //
7
+ // Usage:
8
+ // ws-purge # all remotes, each by own resolved target
9
+ // ws-purge jar # only jar-targeted remotes
10
+ // ws-purge ext # only ext-targeted remotes
11
+ // ws-purge jar <pathtojar> # purge a specific jar by path (drops every
12
+ // META-INF/federation/<*>/, regardless of
13
+ // which project owned it)
14
+ // ws-purge <name>... # restrict to those modules (each by own
15
+ // target)
16
+ // ws-purge <name>... jar # restrict + require jar-targeted; errors
17
+ // on mismatch with hint to use
18
+ // `ws-purge jar <pathtojar>`
19
+ // ws-purge <name>... ext # restrict + require ext-targeted
20
+ //
21
+ // End-of-run report lists every target acted on (path + entries dropped or
22
+ // dir removed), every skipped target with reason, and every error.
23
+
24
+ const path = require("path");
25
+ const fs = require("fs");
26
+ const { purgeRemotes, purgeJarPath } = require("../lib/purge-remotes");
27
+
28
+ const USAGE = `ws-purge [<name>...] [jar | ext] [<pathtojar>]
29
+
30
+ Strip prior federation contributions across the workspace's deploy targets.
31
+
32
+ Modes:
33
+ (default) every remote, each by its own resolved target
34
+ jar only jar-targeted remotes
35
+ ext only ext-targeted remotes
36
+ jar <pathtojar> purge a specific jar by path (no module resolution)
37
+
38
+ Restrict to specific modules by listing names first:
39
+ ws-purge crm shop-common
40
+ ws-purge crm jar # errors if crm isn't jar-targeted
41
+ ws-purge crm ext # errors if crm isn't ext-targeted
42
+
43
+ The end-of-run report lists exactly what was purged.
44
+ `;
45
+
46
+ function parseArgv(argv) {
47
+ const tokens = argv.slice(2);
48
+ let mode = null;
49
+ let jarPath = null;
50
+ const names = [];
51
+
52
+ for (let i = 0; i < tokens.length; i++) {
53
+ const t = tokens[i];
54
+ if (t === "--help" || t === "-h") {
55
+ process.stdout.write(USAGE);
56
+ process.exit(0);
57
+ }
58
+ if (t === "jar" || t === "ext") {
59
+ if (mode) {
60
+ throw new Error(`mode specified twice: ${mode} then ${t}`);
61
+ }
62
+ mode = t;
63
+ // Look ahead: if jar and next looks like a path, consume it
64
+ const next = tokens[i + 1];
65
+ if (mode === "jar" && next && /[/.]/.test(next)) {
66
+ jarPath = next;
67
+ i++;
68
+ }
69
+ continue;
70
+ }
71
+ names.push(t);
72
+ }
73
+
74
+ if (jarPath && names.length) {
75
+ throw new Error(
76
+ `\`jar <pathtojar>\` can't be combined with module names — the jar path purges that file regardless of which module owned it.`
77
+ );
78
+ }
79
+
80
+ return { mode, jarPath, names };
81
+ }
82
+
83
+ function main() {
84
+ let parsed;
85
+ try {
86
+ parsed = parseArgv(process.argv);
87
+ } catch (e) {
88
+ console.error(`ws-purge: ${e.message}`);
89
+ console.error(USAGE);
90
+ process.exit(1);
91
+ }
92
+
93
+ if (parsed.jarPath) {
94
+ const abs = path.resolve(parsed.jarPath);
95
+ if (!fs.existsSync(abs)) {
96
+ console.error(`ws-purge: jar not found at ${abs}`);
97
+ process.exit(1);
98
+ }
99
+ const { errors } = purgeJarPath(abs);
100
+ process.exit(errors.length ? 1 : 0);
101
+ }
102
+
103
+ const workspaceDir = process.cwd();
104
+ const { errors } = purgeRemotes({
105
+ workspaceDir,
106
+ restrictTo: parsed.names,
107
+ modeFilter: parsed.mode,
108
+ strict: parsed.mode !== null && parsed.names.length > 0,
109
+ });
110
+ process.exit(errors.length ? 1 : 0);
111
+ }
112
+
113
+ main();
@@ -4,13 +4,31 @@ const { packIntoJar } = require("./pack-into-jar");
4
4
  const { loadOrder, partitionByKind } = require("./build-modules");
5
5
  const { findMatches } = require("./jar-glob");
6
6
  const { loadDeployContext, resolveTarget } = require("./target-resolution");
7
+ const { promotePeerDeps, syncFederationWorkspace } = require("./federation-peers");
7
8
 
8
9
  function copyTypes(distLib, hostFederationDir) {
10
+ // Preserve any existing node_modules dir under the staged location —
11
+ // npm's workspace install populated it and the next syncFederationWorkspace
12
+ // run will reconcile. Replacing it on every type-copy would force a
13
+ // fresh per-lib install instead of letting npm hoist across siblings.
14
+ const preservedNodeModules = path.join(hostFederationDir, "node_modules");
15
+ let cached = null;
16
+ if (fs.existsSync(preservedNodeModules)) {
17
+ cached = preservedNodeModules + `.preserve-${process.pid}`;
18
+ fs.renameSync(preservedNodeModules, cached);
19
+ }
9
20
  if (fs.existsSync(hostFederationDir)) {
10
21
  fs.rmSync(hostFederationDir, { recursive: true, force: true });
11
22
  }
12
23
  fs.mkdirSync(hostFederationDir, { recursive: true });
13
24
  fs.cpSync(distLib, hostFederationDir, { recursive: true });
25
+ if (cached) {
26
+ fs.renameSync(cached, preservedNodeModules);
27
+ }
28
+ // Promote peerDependencies → dependencies in the staged package.json so
29
+ // npm's workspace install actually installs them. The producer's source
30
+ // package.json is untouched; this only affects the host-staged copy.
31
+ promotePeerDeps(hostFederationDir);
14
32
  }
15
33
 
16
34
  // Copy the federation artifact (dist/admin-remotes/<id>/) to the ext target
@@ -154,6 +172,21 @@ function deployRemotes({ workspaceDir, restrictTo = [], withDeps = false }) {
154
172
  }
155
173
 
156
174
  console.log(`\nDeployed ${results.length} module(s).`);
175
+
176
+ // Now that the .federation/<id>/ dirs are fresh, run a single workspace
177
+ // install at .federation/ to populate node_modules. npm hoists shared
178
+ // versions to .federation/node_modules/ and only nests on per-lib version
179
+ // conflicts. Consumers' TypeScript walks up from .federation/<id>/ and
180
+ // resolves transitive types without consumer-side dep management.
181
+ try {
182
+ syncFederationWorkspace(ctx.adminDir);
183
+ } catch (e) {
184
+ console.warn(
185
+ `\x1b[33mWarning:\x1b[0m federation peer install failed: ${e.message}`
186
+ );
187
+ console.warn(` Consumer workspaces may fail to type-check federated libs.`);
188
+ }
189
+
157
190
  return { results };
158
191
  }
159
192
 
@@ -0,0 +1,80 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { execSync } = require("child_process");
4
+
5
+ // Mirror a federation lib's peerDependencies into its dependencies in the
6
+ // STAGED package.json under <adminDir>/.federation/<id>/. ng-packagr emits
7
+ // peerDependencies — the canonical contract for a published library — but
8
+ // for our publish-less federation we want npm to actually INSTALL those
9
+ // peers (so consuming workspaces' TypeScript can resolve their types via
10
+ // walk-up from .federation/<id>/). Promoting peer → dep in the staged copy
11
+ // only doesn't touch the producer's source.
12
+ function promotePeerDeps(libDir) {
13
+ const pkgPath = path.join(libDir, "package.json");
14
+ if (!fs.existsSync(pkgPath)) return false;
15
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
16
+ if (!pkg.peerDependencies || !Object.keys(pkg.peerDependencies).length) {
17
+ return false;
18
+ }
19
+ pkg.dependencies = Object.assign({}, pkg.dependencies, pkg.peerDependencies);
20
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
21
+ return true;
22
+ }
23
+
24
+ // Reconcile the entire .federation/ dir as an npm workspace root. Lists
25
+ // every lib subdir as a workspace, then runs `npm install` once — npm
26
+ // hoists shared peer versions to .federation/node_modules/ and nests only
27
+ // where individual libs disagree, automatically scoping conflicts the way
28
+ // npm normally would.
29
+ //
30
+ // Idempotent. Re-runs at the end of every deploy.
31
+ function syncFederationWorkspace(adminDir) {
32
+ const federationDir = path.join(adminDir, ".federation");
33
+ if (!fs.existsSync(federationDir)) return { installed: false };
34
+
35
+ const libs = listFederationLibs(federationDir);
36
+ if (!libs.length) return { installed: false };
37
+
38
+ writeWorkspaceManifest(federationDir, libs);
39
+
40
+ console.log(
41
+ `\n=== federation peers: install (${libs.length} workspace${libs.length === 1 ? "" : "s"}) ===`
42
+ );
43
+ console.log(` at ${federationDir}`);
44
+ execSync("npm install --no-package-lock --no-audit --no-fund", {
45
+ cwd: federationDir,
46
+ stdio: "inherit",
47
+ });
48
+ return { installed: true, libs };
49
+ }
50
+
51
+ function listFederationLibs(federationDir) {
52
+ return fs
53
+ .readdirSync(federationDir, { withFileTypes: true })
54
+ .filter((e) => e.isDirectory())
55
+ .map((e) => e.name)
56
+ .filter((name) => {
57
+ if (name === "node_modules") return false;
58
+ return fs.existsSync(path.join(federationDir, name, "package.json"));
59
+ })
60
+ .sort();
61
+ }
62
+
63
+ function writeWorkspaceManifest(federationDir, libs) {
64
+ const pkg = {
65
+ name: "ws-federation-host",
66
+ private: true,
67
+ version: "0.0.0",
68
+ workspaces: libs.map((id) => `./${id}`),
69
+ };
70
+ fs.writeFileSync(
71
+ path.join(federationDir, "package.json"),
72
+ JSON.stringify(pkg, null, 2) + "\n"
73
+ );
74
+ }
75
+
76
+ module.exports = {
77
+ promotePeerDeps,
78
+ syncFederationWorkspace,
79
+ listFederationLibs,
80
+ };
package/lib/purge-jar.js CHANGED
@@ -2,22 +2,32 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const AdmZip = require("adm-zip");
4
4
 
5
- // Strip a federated remote contribution from a jar by dropping every entry
6
- // under META-INF/federation/<id>/. Inverse of packIntoJar. Returns
7
- // { jarPath, droppedEntries, hadContribution }.
5
+ // Strip federated remote contributions from a jar by dropping entries under
6
+ // META-INF/federation/.
7
+ //
8
+ // purgeJar({ jarPath, id }) — drops only META-INF/federation/<id>/.
9
+ // purgeJar({ jarPath }) — drops every META-INF/federation/<*>/.
10
+ //
11
+ // Returns { jarPath, droppedEntries, droppedIds[], hadContribution }.
8
12
  function purgeJar({ jarPath, id }) {
9
13
  if (!fs.existsSync(jarPath)) {
10
14
  throw new Error(`Jar not found: ${jarPath}`);
11
15
  }
12
16
 
13
- const contribRoot = `META-INF/federation/${id}/`;
17
+ const federationRoot = `META-INF/federation/`;
18
+ const targetPrefix = id ? `${federationRoot}${id}/` : federationRoot;
19
+
14
20
  const original = new AdmZip(jarPath);
15
21
  const out = new AdmZip();
16
22
 
17
23
  let dropped = 0;
24
+ const idsHit = new Set();
18
25
  for (const entry of original.getEntries()) {
19
- if (entry.entryName.startsWith(contribRoot)) {
26
+ if (entry.entryName.startsWith(targetPrefix)) {
20
27
  dropped++;
28
+ const rest = entry.entryName.slice(federationRoot.length);
29
+ const slash = rest.indexOf("/");
30
+ if (slash > 0) idsHit.add(rest.slice(0, slash));
21
31
  continue;
22
32
  }
23
33
  if (entry.isDirectory) {
@@ -33,7 +43,12 @@ function purgeJar({ jarPath, id }) {
33
43
  }
34
44
 
35
45
  if (dropped === 0) {
36
- return { jarPath, droppedEntries: 0, hadContribution: false };
46
+ return {
47
+ jarPath,
48
+ droppedEntries: 0,
49
+ droppedIds: [],
50
+ hadContribution: false,
51
+ };
37
52
  }
38
53
 
39
54
  const dir = path.dirname(jarPath);
@@ -44,7 +59,12 @@ function purgeJar({ jarPath, id }) {
44
59
  out.writeZip(tmp);
45
60
  fs.renameSync(tmp, jarPath);
46
61
 
47
- return { jarPath, droppedEntries: dropped, hadContribution: true };
62
+ return {
63
+ jarPath,
64
+ droppedEntries: dropped,
65
+ droppedIds: [...idsHit].sort(),
66
+ hadContribution: true,
67
+ };
48
68
  }
49
69
 
50
70
  module.exports = { purgeJar };
@@ -3,118 +3,244 @@ const path = require("path");
3
3
  const { purgeJar } = require("./purge-jar");
4
4
  const { loadOrder, partitionByKind } = require("./build-modules");
5
5
  const { loadDeployContext, resolveTarget } = require("./target-resolution");
6
+ const { syncFederationWorkspace } = require("./federation-peers");
6
7
 
7
- // Purge a single remote's deployed footprint:
8
- // - jar target: drop META-INF/federation/<id>/ entries from the jar
9
- // - ext target: remove <wpmRoot>/scripts/ext-admin-remotes/<id>/
10
- // - always: remove host's <adminDir>/.federation/<id>/ types dir
11
- //
12
- // Uses the exact same target resolution as deploy — wsconfig override >
13
- // project package.json (target=ext | pom-derived artifactId). No part of the
14
- // jar location is implied from the project id; same abuse-resistance as deploy.
15
- function purgeOne({
16
- workspaceDir,
17
- id,
18
- wpmRoot,
19
- modulesFolder,
20
- adminDir,
21
- wsconfig,
22
- }) {
23
- const hostFederationDir = path.join(adminDir, ".federation", id);
8
+ // Per-remote purge action. Returns a structured action record.
9
+ function purgeOne({ workspaceDir, id, ctx }) {
10
+ const hostFederationDir = path.join(ctx.adminDir, ".federation", id);
24
11
 
25
12
  const target = resolveTarget({
26
13
  workspaceDir,
27
14
  projectName: id,
28
- wsconfig,
29
- wpmRoot,
30
- modulesFolder,
15
+ wsconfig: ctx.wsconfig,
16
+ wpmRoot: ctx.wpmRoot,
17
+ modulesFolder: ctx.modulesFolder,
31
18
  });
32
19
 
33
- let artifactReport;
20
+ let artifact;
34
21
  if (target.kind === "jar") {
35
- const result = purgeJar({ jarPath: target.jarPath, id });
36
- artifactReport = {
22
+ const r = purgeJar({ jarPath: target.jarPath, id });
23
+ artifact = {
37
24
  kind: "jar",
38
25
  jarPath: target.jarPath,
39
26
  pomPath: target.pomPath,
40
27
  artifactId: target.artifactId,
41
- ...result,
28
+ droppedEntries: r.droppedEntries,
29
+ hadContribution: r.hadContribution,
42
30
  };
43
31
  } else {
44
- let removed = false;
45
- if (fs.existsSync(target.extDir)) {
46
- fs.rmSync(target.extDir, { recursive: true, force: true });
47
- removed = true;
48
- }
49
- artifactReport = { kind: "ext", extDir: target.extDir, removedExtDir: removed };
32
+ const removed = fs.existsSync(target.extDir);
33
+ if (removed) fs.rmSync(target.extDir, { recursive: true, force: true });
34
+ artifact = { kind: "ext", extDir: target.extDir, removedExtDir: removed };
50
35
  }
51
36
 
52
- let typesRemoved = false;
53
- if (fs.existsSync(hostFederationDir)) {
37
+ const typesRemoved = fs.existsSync(hostFederationDir);
38
+ if (typesRemoved) {
54
39
  fs.rmSync(hostFederationDir, { recursive: true, force: true });
55
- typesRemoved = true;
56
40
  }
57
41
 
58
- return { ...artifactReport, hostFederationDir, typesRemoved };
42
+ return { id, kind: target.kind, ...artifact, hostFederationDir, typesRemoved };
59
43
  }
60
44
 
61
- function purgeRemotes({ workspaceDir, restrictTo = [], withDeps = false }) {
45
+ // Purge across an admin-modules workspace.
46
+ //
47
+ // modeFilter: null | "jar" | "ext"
48
+ // null → each remote is purged according to its own resolved target
49
+ // "jar" → only act on jar-targeted remotes (skip ext)
50
+ // "ext" → only act on ext-targeted remotes (skip jar)
51
+ //
52
+ // strict: when true and modeFilter is set with explicit restrictTo names,
53
+ // mismatches throw with an actionable error (suggests `ws-purge jar <path>`
54
+ // for jar-mismatch). When false (modeFilter without restrictTo), mismatches
55
+ // are silently filtered.
56
+ function purgeRemotes({
57
+ workspaceDir,
58
+ restrictTo = [],
59
+ withDeps = false,
60
+ modeFilter = null,
61
+ strict = false,
62
+ }) {
62
63
  const ctx = loadDeployContext(workspaceDir);
63
-
64
64
  const { order } = loadOrder({ workspaceDir, restrictTo, withDeps });
65
65
  const { remotes, libraries } = partitionByKind(workspaceDir, order);
66
66
 
67
- console.log(`\n=== Purge order ===`);
68
- remotes.forEach((n, i) => console.log(` ${i + 1}. ${n}`));
67
+ console.log(`=== Purge ===`);
68
+ console.log(` mode: ${modeFilter || "auto (each by own target)"}`);
69
+ console.log(
70
+ ` scope: ${restrictTo.length ? restrictTo.join(", ") : "all remotes"}`
71
+ );
69
72
  if (libraries.length) {
70
73
  console.log(
71
- ` (skipping libraries: ${libraries.join(", ")} wsmodules.kind=library)`
74
+ ` skipped libraries: ${libraries.join(", ")} (wsmodules.kind=library)`
72
75
  );
73
76
  }
77
+ console.log("");
78
+
79
+ const actions = [];
80
+ const skipped = [];
81
+ const errors = [];
74
82
 
75
- const results = [];
76
83
  for (const id of remotes) {
77
- console.log(`\n=== purge ${id} ===`);
84
+ let target;
78
85
  try {
79
- const r = purgeOne({
86
+ target = resolveTarget({
80
87
  workspaceDir,
81
- id,
88
+ projectName: id,
89
+ wsconfig: ctx.wsconfig,
82
90
  wpmRoot: ctx.wpmRoot,
83
91
  modulesFolder: ctx.modulesFolder,
84
- adminDir: ctx.adminDir,
85
- wsconfig: ctx.wsconfig,
86
92
  });
87
- if (r.kind === "jar") {
88
- console.log(` target: jar`);
89
- console.log(` jar: ${r.jarPath}`);
90
- if (r.artifactId) console.log(` artifactId: ${r.artifactId}`);
91
- if (r.pomPath) console.log(` pom: ${r.pomPath}`);
92
- if (r.hadContribution) {
93
- console.log(` dropped: ${r.droppedEntries} entries`);
94
- } else {
95
- console.log(` dropped: (nothing to purge)`);
96
- }
93
+ } catch (e) {
94
+ errors.push({ id, stage: "resolve", message: e.message });
95
+ continue;
96
+ }
97
+
98
+ if (modeFilter && target.kind !== modeFilter) {
99
+ if (strict) {
100
+ const hint =
101
+ modeFilter === "jar"
102
+ ? `Use \`ws-purge jar <pathtojar>\` to purge a specific jar by path.`
103
+ : `Use \`ws-purge\` (no mode) to purge by each module's own target.`;
104
+ errors.push({
105
+ id,
106
+ stage: "filter",
107
+ message: `${id} is ${target.kind}-targeted, not ${modeFilter}-targeted. ${hint}`,
108
+ });
109
+ continue;
110
+ }
111
+ skipped.push({ id, reason: `target is ${target.kind}, filter is ${modeFilter}` });
112
+ continue;
113
+ }
114
+
115
+ try {
116
+ const action = purgeOne({ workspaceDir, id, ctx });
117
+ actions.push(action);
118
+ } catch (e) {
119
+ errors.push({ id, stage: "purge", message: e.message });
120
+ }
121
+ }
122
+
123
+ reportActions({ actions, skipped, errors });
124
+
125
+ // Removed federation type dirs ⇒ workspaces list is stale; resync so
126
+ // the remaining libs stay correctly installed (and orphaned ones stop
127
+ // claiming workspace slots).
128
+ if (actions.some((a) => a.typesRemoved)) {
129
+ try {
130
+ syncFederationWorkspace(ctx.adminDir);
131
+ } catch (e) {
132
+ console.warn(
133
+ `\x1b[33mWarning:\x1b[0m federation peer resync failed: ${e.message}`
134
+ );
135
+ }
136
+ }
137
+
138
+ return { actions, skipped, errors };
139
+ }
140
+
141
+ // Escape hatch: purge a specific jar by path, dropping every
142
+ // META-INF/federation/<*>/ entry regardless of which workspace owns it.
143
+ function purgeJarPath(jarPath) {
144
+ console.log(`=== Purge ===`);
145
+ console.log(` mode: jar (specific path)`);
146
+ console.log(` jar: ${jarPath}`);
147
+ console.log("");
148
+
149
+ const actions = [];
150
+ const errors = [];
151
+ try {
152
+ const r = purgeJar({ jarPath });
153
+ actions.push({
154
+ id: null,
155
+ kind: "jar",
156
+ jarPath,
157
+ droppedEntries: r.droppedEntries,
158
+ droppedIds: r.droppedIds,
159
+ hadContribution: r.hadContribution,
160
+ hostFederationDir: null,
161
+ typesRemoved: false,
162
+ });
163
+ } catch (e) {
164
+ errors.push({ id: jarPath, stage: "purge", message: e.message });
165
+ }
166
+
167
+ reportActions({ actions, skipped: [], errors });
168
+ return { actions, skipped: [], errors };
169
+ }
170
+
171
+ function reportActions({ actions, skipped, errors }) {
172
+ console.log(`=== Actions ===`);
173
+ if (!actions.length) {
174
+ console.log(` (nothing purged)`);
175
+ }
176
+ for (const a of actions) {
177
+ const label = a.id ? a.id : "(by path)";
178
+ if (a.kind === "jar") {
179
+ if (a.hadContribution) {
180
+ const ids = a.droppedIds && a.droppedIds.length
181
+ ? ` (ids: ${a.droppedIds.join(", ")})`
182
+ : "";
183
+ console.log(
184
+ ` ${label} jar purged ${a.droppedEntries} entries from ${a.jarPath}${ids}`
185
+ );
97
186
  } else {
98
- console.log(` target: ext`);
99
- console.log(` ext dir: ${r.extDir}`);
100
- console.log(` ext dir: ${r.removedExtDir ? "removed" : "(absent)"}`);
187
+ console.log(
188
+ ` ${label} jar no contribution to purge in ${a.jarPath}`
189
+ );
101
190
  }
191
+ } else {
192
+ console.log(
193
+ ` ${label} ext ${a.removedExtDir ? "removed" : "(absent)"} ${a.extDir}`
194
+ );
195
+ }
196
+ if (a.hostFederationDir) {
102
197
  console.log(
103
- ` host types: ${r.typesRemoved ? "removed" : "(absent)"} (${r.hostFederationDir})`
198
+ ` host types ${a.typesRemoved ? "removed" : "(absent)"}: ${a.hostFederationDir}`
104
199
  );
105
- results.push({ id, ok: true, ...r });
106
- } catch (err) {
107
- console.error(` \x1b[31mfailed:\x1b[0m ${err.message}`);
108
- results.push({ id, ok: false, error: err.message });
109
200
  }
110
201
  }
111
202
 
112
- const ok = results.filter((r) => r.ok).length;
113
- const failed = results.length - ok;
203
+ if (skipped.length) {
204
+ console.log(`\n=== Skipped ===`);
205
+ for (const s of skipped) console.log(` ${s.id}: ${s.reason}`);
206
+ }
207
+
208
+ if (errors.length) {
209
+ console.log(`\n=== Errors ===`);
210
+ for (const e of errors)
211
+ console.log(` \x1b[31m${e.id}\x1b[0m [${e.stage}] ${e.message}`);
212
+ }
213
+
214
+ const jars = actions.filter((a) => a.kind === "jar");
215
+ const exts = actions.filter((a) => a.kind === "ext");
216
+ const typesRemoved = actions.filter((a) => a.typesRemoved).length;
217
+ const noopJars = jars.filter((a) => !a.hadContribution).length;
218
+ const removedExt = exts.filter((a) => a.removedExtDir).length;
219
+
220
+ console.log(`\n=== Summary ===`);
114
221
  console.log(
115
- `\nPurged ${ok} module(s)${failed ? `, ${failed} failed` : ""}.`
222
+ ` acted on ${actions.length} target(s): ${jars.length} jar, ${exts.length} ext`
116
223
  );
117
- return { results };
224
+ if (jars.length) {
225
+ const totalEntries = jars.reduce((s, a) => s + (a.droppedEntries || 0), 0);
226
+ console.log(
227
+ ` jar: ${totalEntries} entries dropped` +
228
+ (noopJars ? `; ${noopJars} jar(s) had no contribution` : "")
229
+ );
230
+ }
231
+ if (exts.length) {
232
+ console.log(
233
+ ` ext: ${removedExt} dir(s) removed` +
234
+ (exts.length - removedExt
235
+ ? `; ${exts.length - removedExt} already absent`
236
+ : "")
237
+ );
238
+ }
239
+ if (typesRemoved) {
240
+ console.log(` host: ${typesRemoved} .federation/<id>/ dir(s) removed`);
241
+ }
242
+ if (skipped.length) console.log(` skipped: ${skipped.length}`);
243
+ if (errors.length) console.log(` \x1b[31merrors: ${errors.length}\x1b[0m`);
118
244
  }
119
245
 
120
- module.exports = { purgeRemotes, purgeOne };
246
+ module.exports = { purgeRemotes, purgeOne, purgeJarPath };
@@ -1,5 +1,7 @@
1
+ const fs = require("fs");
1
2
  const path = require("path");
2
3
  const Module = require("module");
4
+ const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
3
5
 
4
6
  // Build the in-memory webpack config used to produce a federated remote
5
7
  // from a single synthetic entry that re-exports the lib's public API.
@@ -30,6 +32,19 @@ function buildWebpackConfig({
30
32
 
31
33
  const remoteOutDir = path.join(outDir, id, "remote");
32
34
 
35
+ // tsconfig.federation.json is the SSOT for cross-workspace paths
36
+ // (`@ws-remote/*` → `<adminDir>/.federation/*`, sibling lib aliases,
37
+ // etc.). Without this plugin webpack ignores tsconfig `paths` and
38
+ // walks node_modules — which fails for cross-workspace federation
39
+ // siblings that only live in the host's federation dir.
40
+ const federationTsconfig = path.join(workspaceDir, "tsconfig.federation.json");
41
+ const resolvePlugins = [];
42
+ if (fs.existsSync(federationTsconfig)) {
43
+ resolvePlugins.push(
44
+ new TsconfigPathsPlugin({ configFile: federationTsconfig })
45
+ );
46
+ }
47
+
33
48
  return {
34
49
  mode: "production",
35
50
  entry: syntheticEntry,
@@ -56,6 +71,7 @@ function buildWebpackConfig({
56
71
  },
57
72
  resolve: {
58
73
  alias: aliasMap,
74
+ plugins: resolvePlugins,
59
75
  extensions: [".ts", ".js", ".mjs", ".json"],
60
76
  // ngcc rewrites Angular packages in-place but leaves the
61
77
  // originals next to them, exposing both via package.json fields
package/lib/wire-host.js CHANGED
@@ -53,6 +53,16 @@ function rebuildFederationTsconfig(workspaceDir) {
53
53
  federationTarget = path.join(hostDir, ".federation", "*");
54
54
  paths["@ws-remote/*"] = [federationTarget];
55
55
  }
56
+ // Catch-all fallback: any unresolved import resolves against this
57
+ // workspace's own node_modules. Required for cross-workspace federation
58
+ // siblings (e.g. @ws-remote/ws-framework) whose .d.ts files reference
59
+ // transitive deps (@angular/material/form-field, etc.) — TS walks up
60
+ // from the .d.ts file's location to find node_modules, but federation
61
+ // remotes live under <wpmRoot>/admin/.federation/ outside any consumer
62
+ // workspace's tree. This catch-all lets TS fall back to the consumer's
63
+ // node_modules regardless of where the .d.ts lives. Must come AFTER
64
+ // specific paths so they win on prefix match.
65
+ paths["*"] = ["./node_modules/*"];
56
66
  // tsconfig.federation.json is the leaf of the extends chain. Its single
57
67
  // responsibility now is to combine the workspace-local `paths` with the
58
68
  // canonical compiler options from @ws-test-realm/devkit/tsconfig.base.json.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ws-test-realm/admin-kit",
3
- "version": "0.1.8",
3
+ "version": "0.1.11",
4
4
  "description": "Workflow CLI + scaffolding for Wiresphere admin-modules workspaces. Ships `ws-init-workspace` (init/merge into a project, stamps @wiresphere/shared's `getOverrides()` into the workspace's npm overrides block), `ws-modules` (build/pack/deploy driver), `ws-generate-module`/`ws-drop-module`, `ws-wire-host`, `ws-wire-pom`, `ws-sync-paths`, and `ws-pack-remote`. Depends on @wiresphere/devkit (toolchain BOM, peerDeps); pairs with @wiresphere/shared (runtime BOM + MF share map + overrides manifest). Requires npm 9+ for overrides + peerDep resolution to coexist cleanly.",
5
5
  "license": "Artistic-2.0",
6
6
  "publishConfig": {
@@ -14,6 +14,7 @@
14
14
  "ws-generate-module": "./bin/ws-generate-module.js",
15
15
  "ws-drop-module": "./bin/ws-drop-module.js",
16
16
  "ws-modules": "./bin/ws-modules.js",
17
+ "ws-purge": "./bin/ws-purge.js",
17
18
  "ws-sync-paths": "./bin/ws-sync-paths.js"
18
19
  },
19
20
  "main": "lib/index.js",
@@ -28,6 +29,7 @@
28
29
  "@ws-test-realm/devkit": "^0.3.0",
29
30
  "@ws-test-realm/shared": "^0.3.0",
30
31
  "adm-zip": "^0.5.10",
31
- "fast-xml-parser": "^4.3.0"
32
+ "fast-xml-parser": "^4.3.0",
33
+ "tsconfig-paths-webpack-plugin": "^4.1.0"
32
34
  }
33
35
  }
@@ -12,7 +12,7 @@
12
12
  "build": "ws-modules --build",
13
13
  "pack": "ws-modules --pack",
14
14
  "deploy": "ws-modules --deploy",
15
- "purge": "ws-modules --purge",
15
+ "purge": "ws-purge",
16
16
  "wire": "ws-modules --build --pack --deploy"
17
17
  },
18
18
  "prettier": "@ws-test-realm/devkit/prettier.config.js",