@ws-test-realm/admin-kit 0.1.7 → 0.1.10

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
@@ -3,58 +3,76 @@
3
3
  // workspace.
4
4
  //
5
5
  // Usage:
6
- // ws-modules <names...|all> [--build] [--pack] [--deploy] [--with-deps]
6
+ // ws-modules <names...|all> [--build] [--pack] [--deploy] [--purge] [--with-deps]
7
7
  //
8
8
  // <names...> one or more project names from angular.json
9
9
  // all expand to every project in angular.json
10
10
  // --build ng build each lib (topo-ordered)
11
11
  // --pack federation-pack each lib into dist/admin-remotes/<id>/
12
12
  // --deploy push jar + types into <wpmRoot>/<modulesFolder>/<id>-*.jar
13
+ // --purge strip prior federation contribution from the jar (or ext
14
+ // dir) and host's .federation/<id>/ types dir. Resolves the
15
+ // target the same way --deploy does (wsconfig override >
16
+ // project package.json's wsmodules.target / pom).
13
17
  // --with-deps expand selection to include each name's in-workspace deps
14
18
  //
15
19
  // Behaviour:
16
20
  // - No args at all → print usage, exit 1
17
21
  // - Positional + step flag(s) → run exactly those steps, non-interactive
18
22
  // - Positional, no step flags → prompt y/n for each of build/pack/deploy
19
- // (and --with-deps); run the selected set
23
+ // (and --with-deps); run the selected set.
24
+ // Purge is NOT prompted (destructive +
25
+ // not part of the normal wire cycle).
20
26
  // - Step flag(s) but no positional → error (need to know what to operate on)
21
27
  //
28
+ // Step ordering when multiple are passed: purge → build → pack → deploy.
29
+ //
22
30
  // Examples:
23
31
  // ws-modules all # interactive
24
32
  // ws-modules db-login # interactive, just this project
25
33
  // ws-modules all --build --pack --deploy # wire the whole stack
26
34
  // ws-modules db-login --build --with-deps # build db-login + its deps
27
35
  // ws-modules crm shop-common --pack # pack two specific projects
36
+ // ws-modules all --purge # strip stale contributions
37
+ // ws-modules all --purge --build --pack --deploy # nuke + rewire
28
38
 
29
39
  const readline = require("readline");
30
40
  const { buildLibs, packRemotes } = require("../lib/build-modules");
31
41
  const { deployRemotes } = require("../lib/deploy-remotes");
42
+ const { purgeRemotes } = require("../lib/purge-remotes");
32
43
 
33
- const STEP_FLAGS = ["build", "pack", "deploy"];
44
+ const STEP_FLAGS = ["purge", "build", "pack", "deploy"];
34
45
  const PASSIVE_FLAGS = ["with-deps"];
35
46
  const KNOWN_FLAGS = new Set([...STEP_FLAGS, ...PASSIVE_FLAGS]);
36
47
 
37
- const USAGE = `ws-modules <names...|all> [--build] [--pack] [--deploy] [--with-deps]
48
+ const USAGE = `ws-modules <names...|all> [--build] [--pack] [--deploy] [--purge] [--with-deps]
38
49
 
39
- Drive build / pack / deploy across an admin-modules workspace.
50
+ Drive build / pack / deploy / purge across an admin-modules workspace.
40
51
 
41
52
  Positional:
42
53
  <names...> one or more project names from angular.json
43
54
  all expand to every project in angular.json
44
55
 
45
- Step flags (run exactly the steps you list — if none, you'll be prompted):
56
+ Step flags (run exactly the steps you list — if none, you'll be prompted for
57
+ build/pack/deploy; purge is never prompted):
46
58
  --build ng build each lib (topo-ordered)
47
59
  --pack federation-pack each lib into dist/admin-remotes/<id>/
48
60
  --deploy push jar + types into <wpmRoot>/<modulesFolder>/<id>-*.jar
61
+ --purge strip prior federation contribution from the jar (or ext
62
+ dir) and host's .federation/<id>/ types dir
49
63
 
50
64
  Other:
51
65
  --with-deps expand selection to include each name's in-workspace deps
52
66
 
67
+ Order when multiple steps are passed: purge → build → pack → deploy.
68
+
53
69
  Examples:
54
70
  ws-modules all
55
71
  ws-modules db-login --build
56
72
  ws-modules all --build --pack --deploy
57
73
  ws-modules crm shop-common --pack
74
+ ws-modules all --purge
75
+ ws-modules all --purge --build --pack --deploy
58
76
  `;
59
77
 
60
78
  function parseArgv(argv) {
@@ -124,10 +142,11 @@ async function main() {
124
142
  process.exit(1);
125
143
  }
126
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`.
127
148
  if (!argv.positional.length) {
128
- console.error("ws-modules: need module names or `all`.");
129
- console.error(USAGE);
130
- process.exit(1);
149
+ argv.positional.push("all");
131
150
  }
132
151
 
133
152
  let restrictTo;
@@ -143,6 +162,7 @@ async function main() {
143
162
  let withDeps;
144
163
  if (stepFlagsPresent) {
145
164
  steps = {
165
+ purge: !!argv.flags.purge,
146
166
  build: !!argv.flags.build,
147
167
  pack: !!argv.flags.pack,
148
168
  deploy: !!argv.flags.deploy,
@@ -150,11 +170,11 @@ async function main() {
150
170
  withDeps = !!argv.flags["with-deps"];
151
171
  } else {
152
172
  const ans = await promptForSteps(!!argv.flags["with-deps"]);
153
- steps = { build: ans.build, pack: ans.pack, deploy: ans.deploy };
173
+ steps = { purge: false, build: ans.build, pack: ans.pack, deploy: ans.deploy };
154
174
  withDeps = ans.withDeps;
155
175
  }
156
176
 
157
- if (!steps.build && !steps.pack && !steps.deploy) {
177
+ if (!steps.purge && !steps.build && !steps.pack && !steps.deploy) {
158
178
  console.log("No steps selected. Nothing to do.");
159
179
  return;
160
180
  }
@@ -164,10 +184,11 @@ async function main() {
164
184
 
165
185
  console.log(
166
186
  `\nws-modules: ${argv.positional.join(" ")} →` +
167
- ` build=${steps.build} pack=${steps.pack} deploy=${steps.deploy}` +
187
+ ` purge=${steps.purge} build=${steps.build} pack=${steps.pack} deploy=${steps.deploy}` +
168
188
  ` with-deps=${withDeps}`
169
189
  );
170
190
 
191
+ if (steps.purge) purgeRemotes(common);
171
192
  if (steps.build) buildLibs(common);
172
193
  if (steps.pack) packRemotes(common);
173
194
  if (steps.deploy) deployRemotes(common);
@@ -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();
@@ -2,85 +2,8 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { packIntoJar } = require("./pack-into-jar");
4
4
  const { loadOrder, partitionByKind } = require("./build-modules");
5
- const { resolveProjectPom, readArtifactId } = require("./pom");
6
- const { resolveOne, findMatches } = require("./jar-glob");
7
-
8
- function readWsconfig(workspaceDir) {
9
- const file = path.join(workspaceDir, "wsconfig.json");
10
- if (!fs.existsSync(file)) {
11
- throw new Error(
12
- `No wsconfig.json in ${workspaceDir}. Run \`ws-wire-host <wpm-root>\` first.`
13
- );
14
- }
15
- return JSON.parse(fs.readFileSync(file, "utf8"));
16
- }
17
-
18
- function readModulesJson(wpmRoot) {
19
- const file = path.join(wpmRoot, "modules.json");
20
- if (!fs.existsSync(file)) {
21
- throw new Error(`No modules.json at ${wpmRoot}.`);
22
- }
23
- return JSON.parse(fs.readFileSync(file, "utf8"));
24
- }
25
-
26
- function readProjectPkg(workspaceDir, name) {
27
- const pkgPath = path.join(workspaceDir, "projects", name, "package.json");
28
- if (!fs.existsSync(pkgPath)) return {};
29
- try {
30
- return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
31
- } catch {
32
- return {};
33
- }
34
- }
35
-
36
- // Decide jar vs ext for a project, honouring wsconfig override > package.json.
37
- // Returns { kind: "jar", jarPath } or { kind: "ext", extDir }.
38
- function resolveTarget({ workspaceDir, projectName, wsconfig, wpmRoot, modulesFolder }) {
39
- const modulesDir = path.resolve(wpmRoot, modulesFolder);
40
- const extDir = path.join(
41
- wpmRoot,
42
- "scripts",
43
- "ext-admin-remotes",
44
- projectName
45
- );
46
-
47
- // 1. wsconfig override
48
- const override =
49
- wsconfig.deployOverrides && wsconfig.deployOverrides[projectName];
50
- if (override) {
51
- if (override.jar && override.ext) {
52
- throw new Error(
53
- `wsconfig.deployOverrides.${projectName}: set exactly one of 'jar' or 'ext', not both`
54
- );
55
- }
56
- if (override.ext === true) {
57
- return { kind: "ext", extDir };
58
- }
59
- if (override.jar) {
60
- const jarPath = resolveOne(modulesDir, override.jar, projectName);
61
- return { kind: "jar", jarPath };
62
- }
63
- }
64
-
65
- // 2. project package.json
66
- const pkg = readProjectPkg(workspaceDir, projectName);
67
- const target = (pkg.wsmodules && pkg.wsmodules.target) || "jar";
68
- if (target === "ext") {
69
- return { kind: "ext", extDir };
70
- }
71
- if (target !== "jar") {
72
- throw new Error(
73
- `Invalid wsmodules.target for ${projectName}: ${JSON.stringify(target)} (expected 'jar' or 'ext')`
74
- );
75
- }
76
-
77
- // 3. pom-derived artifactId
78
- const pomField = pkg.wsmodules && pkg.wsmodules.pom;
79
- const pomPath = resolveProjectPom(workspaceDir, projectName, pomField);
80
- const artifactId = readArtifactId(pomPath);
81
- const jarPath = resolveOne(modulesDir, `${artifactId}-*.jar`, projectName);
82
- return { kind: "jar", jarPath, pomPath, artifactId };
83
- }
5
+ const { findMatches } = require("./jar-glob");
6
+ const { loadDeployContext, resolveTarget } = require("./target-resolution");
84
7
 
85
8
  function copyTypes(distLib, hostFederationDir) {
86
9
  if (fs.existsSync(hostFederationDir)) {
@@ -94,7 +17,6 @@ function copyTypes(distLib, hostFederationDir) {
94
17
  // dir (replace-existing semantics). Also warn if a same-id-named jar exists
95
18
  // in the modules folder — possible name-collision abuse signal.
96
19
  function deployExt({ workspaceDir, id, modulesDir, extDir }) {
97
- // Collision check: same constrained-glob rules as everywhere else.
98
20
  const collisions = findMatches(modulesDir, `${id}-*.jar`);
99
21
  if (collisions.length) {
100
22
  const list = collisions.map((c) => path.join(modulesDir, c)).join(", ");
@@ -113,7 +35,6 @@ function deployExt({ workspaceDir, id, modulesDir, extDir }) {
113
35
  }
114
36
  fs.mkdirSync(extDir, { recursive: true });
115
37
  fs.cpSync(srcDir, extDir, { recursive: true });
116
- // Count files for the report.
117
38
  let fileCount = 0;
118
39
  const walk = (d) => {
119
40
  for (const e of fs.readdirSync(d, { withFileTypes: true })) {
@@ -131,7 +52,8 @@ function deployOne({
131
52
  id,
132
53
  wpmRoot,
133
54
  modulesFolder,
134
- adminBuildFolder,
55
+ adminDir,
56
+ modulesDir,
135
57
  wsconfig,
136
58
  }) {
137
59
  const distRemoteDir = path.join(workspaceDir, "dist", "admin-remotes", id);
@@ -146,8 +68,6 @@ function deployOne({
146
68
  throw new Error(`Lib dist missing: ${distLibDir}. Run \`build\` first.`);
147
69
  }
148
70
 
149
- const modulesDir = path.resolve(wpmRoot, modulesFolder);
150
- const adminDir = path.resolve(wpmRoot, adminBuildFolder);
151
71
  const hostFederationDir = path.join(adminDir, ".federation", id);
152
72
 
153
73
  const target = resolveTarget({
@@ -158,7 +78,6 @@ function deployOne({
158
78
  modulesFolder,
159
79
  });
160
80
 
161
- // Perform the artifact deploy first; only on success do we touch host types.
162
81
  let artifactReport;
163
82
  if (target.kind === "jar") {
164
83
  const packResult = packIntoJar({
@@ -183,22 +102,13 @@ function deployOne({
183
102
  artifactReport = { kind: "ext", ...extResult };
184
103
  }
185
104
 
186
- // Artifact deploy succeeded — now publish types.
187
105
  copyTypes(distLibDir, hostFederationDir);
188
106
 
189
107
  return { ...artifactReport, hostFederationDir };
190
108
  }
191
109
 
192
110
  function deployRemotes({ workspaceDir, restrictTo = [], withDeps = false }) {
193
- const wsconfig = readWsconfig(workspaceDir);
194
- if (!wsconfig.wpmRoot) {
195
- throw new Error(
196
- "wsconfig.json missing wpmRoot. Run `ws-wire-host <wpm-root>`."
197
- );
198
- }
199
- const modulesJson = readModulesJson(wsconfig.wpmRoot);
200
- const modulesFolder = modulesJson.modulesFolder || "./modules";
201
- const adminBuildFolder = modulesJson.adminBuildFolder || "./admin";
111
+ const ctx = loadDeployContext(workspaceDir);
202
112
 
203
113
  const { order } = loadOrder({ workspaceDir, restrictTo, withDeps });
204
114
  const { remotes, libraries } = partitionByKind(workspaceDir, order);
@@ -217,10 +127,11 @@ function deployRemotes({ workspaceDir, restrictTo = [], withDeps = false }) {
217
127
  const r = deployOne({
218
128
  workspaceDir,
219
129
  id,
220
- wpmRoot: wsconfig.wpmRoot,
221
- modulesFolder,
222
- adminBuildFolder,
223
- wsconfig,
130
+ wpmRoot: ctx.wpmRoot,
131
+ modulesFolder: ctx.modulesFolder,
132
+ adminDir: ctx.adminDir,
133
+ modulesDir: ctx.modulesDir,
134
+ wsconfig: ctx.wsconfig,
224
135
  });
225
136
  if (r.kind === "jar") {
226
137
  console.log(` target: jar`);
@@ -0,0 +1,70 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const AdmZip = require("adm-zip");
4
+
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 }.
12
+ function purgeJar({ jarPath, id }) {
13
+ if (!fs.existsSync(jarPath)) {
14
+ throw new Error(`Jar not found: ${jarPath}`);
15
+ }
16
+
17
+ const federationRoot = `META-INF/federation/`;
18
+ const targetPrefix = id ? `${federationRoot}${id}/` : federationRoot;
19
+
20
+ const original = new AdmZip(jarPath);
21
+ const out = new AdmZip();
22
+
23
+ let dropped = 0;
24
+ const idsHit = new Set();
25
+ for (const entry of original.getEntries()) {
26
+ if (entry.entryName.startsWith(targetPrefix)) {
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));
31
+ continue;
32
+ }
33
+ if (entry.isDirectory) {
34
+ out.addFile(entry.entryName, Buffer.alloc(0), "", entry.attr);
35
+ } else {
36
+ out.addFile(
37
+ entry.entryName,
38
+ entry.getData(),
39
+ entry.comment,
40
+ entry.attr
41
+ );
42
+ }
43
+ }
44
+
45
+ if (dropped === 0) {
46
+ return {
47
+ jarPath,
48
+ droppedEntries: 0,
49
+ droppedIds: [],
50
+ hadContribution: false,
51
+ };
52
+ }
53
+
54
+ const dir = path.dirname(jarPath);
55
+ const tmp = path.join(
56
+ dir,
57
+ `.${path.basename(jarPath)}.repack.${process.pid}.tmp`
58
+ );
59
+ out.writeZip(tmp);
60
+ fs.renameSync(tmp, jarPath);
61
+
62
+ return {
63
+ jarPath,
64
+ droppedEntries: dropped,
65
+ droppedIds: [...idsHit].sort(),
66
+ hadContribution: true,
67
+ };
68
+ }
69
+
70
+ module.exports = { purgeJar };
@@ -0,0 +1,231 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { purgeJar } = require("./purge-jar");
4
+ const { loadOrder, partitionByKind } = require("./build-modules");
5
+ const { loadDeployContext, resolveTarget } = require("./target-resolution");
6
+
7
+ // Per-remote purge action. Returns a structured action record.
8
+ function purgeOne({ workspaceDir, id, ctx }) {
9
+ const hostFederationDir = path.join(ctx.adminDir, ".federation", id);
10
+
11
+ const target = resolveTarget({
12
+ workspaceDir,
13
+ projectName: id,
14
+ wsconfig: ctx.wsconfig,
15
+ wpmRoot: ctx.wpmRoot,
16
+ modulesFolder: ctx.modulesFolder,
17
+ });
18
+
19
+ let artifact;
20
+ if (target.kind === "jar") {
21
+ const r = purgeJar({ jarPath: target.jarPath, id });
22
+ artifact = {
23
+ kind: "jar",
24
+ jarPath: target.jarPath,
25
+ pomPath: target.pomPath,
26
+ artifactId: target.artifactId,
27
+ droppedEntries: r.droppedEntries,
28
+ hadContribution: r.hadContribution,
29
+ };
30
+ } else {
31
+ const removed = fs.existsSync(target.extDir);
32
+ if (removed) fs.rmSync(target.extDir, { recursive: true, force: true });
33
+ artifact = { kind: "ext", extDir: target.extDir, removedExtDir: removed };
34
+ }
35
+
36
+ const typesRemoved = fs.existsSync(hostFederationDir);
37
+ if (typesRemoved) {
38
+ fs.rmSync(hostFederationDir, { recursive: true, force: true });
39
+ }
40
+
41
+ return { id, kind: target.kind, ...artifact, hostFederationDir, typesRemoved };
42
+ }
43
+
44
+ // Purge across an admin-modules workspace.
45
+ //
46
+ // modeFilter: null | "jar" | "ext"
47
+ // null → each remote is purged according to its own resolved target
48
+ // "jar" → only act on jar-targeted remotes (skip ext)
49
+ // "ext" → only act on ext-targeted remotes (skip jar)
50
+ //
51
+ // strict: when true and modeFilter is set with explicit restrictTo names,
52
+ // mismatches throw with an actionable error (suggests `ws-purge jar <path>`
53
+ // for jar-mismatch). When false (modeFilter without restrictTo), mismatches
54
+ // are silently filtered.
55
+ function purgeRemotes({
56
+ workspaceDir,
57
+ restrictTo = [],
58
+ withDeps = false,
59
+ modeFilter = null,
60
+ strict = false,
61
+ }) {
62
+ const ctx = loadDeployContext(workspaceDir);
63
+ const { order } = loadOrder({ workspaceDir, restrictTo, withDeps });
64
+ const { remotes, libraries } = partitionByKind(workspaceDir, order);
65
+
66
+ console.log(`=== Purge ===`);
67
+ console.log(` mode: ${modeFilter || "auto (each by own target)"}`);
68
+ console.log(
69
+ ` scope: ${restrictTo.length ? restrictTo.join(", ") : "all remotes"}`
70
+ );
71
+ if (libraries.length) {
72
+ console.log(
73
+ ` skipped libraries: ${libraries.join(", ")} (wsmodules.kind=library)`
74
+ );
75
+ }
76
+ console.log("");
77
+
78
+ const actions = [];
79
+ const skipped = [];
80
+ const errors = [];
81
+
82
+ for (const id of remotes) {
83
+ let target;
84
+ try {
85
+ target = resolveTarget({
86
+ workspaceDir,
87
+ projectName: id,
88
+ wsconfig: ctx.wsconfig,
89
+ wpmRoot: ctx.wpmRoot,
90
+ modulesFolder: ctx.modulesFolder,
91
+ });
92
+ } catch (e) {
93
+ errors.push({ id, stage: "resolve", message: e.message });
94
+ continue;
95
+ }
96
+
97
+ if (modeFilter && target.kind !== modeFilter) {
98
+ if (strict) {
99
+ const hint =
100
+ modeFilter === "jar"
101
+ ? `Use \`ws-purge jar <pathtojar>\` to purge a specific jar by path.`
102
+ : `Use \`ws-purge\` (no mode) to purge by each module's own target.`;
103
+ errors.push({
104
+ id,
105
+ stage: "filter",
106
+ message: `${id} is ${target.kind}-targeted, not ${modeFilter}-targeted. ${hint}`,
107
+ });
108
+ continue;
109
+ }
110
+ skipped.push({ id, reason: `target is ${target.kind}, filter is ${modeFilter}` });
111
+ continue;
112
+ }
113
+
114
+ try {
115
+ const action = purgeOne({ workspaceDir, id, ctx });
116
+ actions.push(action);
117
+ } catch (e) {
118
+ errors.push({ id, stage: "purge", message: e.message });
119
+ }
120
+ }
121
+
122
+ reportActions({ actions, skipped, errors });
123
+ return { actions, skipped, errors };
124
+ }
125
+
126
+ // Escape hatch: purge a specific jar by path, dropping every
127
+ // META-INF/federation/<*>/ entry regardless of which workspace owns it.
128
+ function purgeJarPath(jarPath) {
129
+ console.log(`=== Purge ===`);
130
+ console.log(` mode: jar (specific path)`);
131
+ console.log(` jar: ${jarPath}`);
132
+ console.log("");
133
+
134
+ const actions = [];
135
+ const errors = [];
136
+ try {
137
+ const r = purgeJar({ jarPath });
138
+ actions.push({
139
+ id: null,
140
+ kind: "jar",
141
+ jarPath,
142
+ droppedEntries: r.droppedEntries,
143
+ droppedIds: r.droppedIds,
144
+ hadContribution: r.hadContribution,
145
+ hostFederationDir: null,
146
+ typesRemoved: false,
147
+ });
148
+ } catch (e) {
149
+ errors.push({ id: jarPath, stage: "purge", message: e.message });
150
+ }
151
+
152
+ reportActions({ actions, skipped: [], errors });
153
+ return { actions, skipped: [], errors };
154
+ }
155
+
156
+ function reportActions({ actions, skipped, errors }) {
157
+ console.log(`=== Actions ===`);
158
+ if (!actions.length) {
159
+ console.log(` (nothing purged)`);
160
+ }
161
+ for (const a of actions) {
162
+ const label = a.id ? a.id : "(by path)";
163
+ if (a.kind === "jar") {
164
+ if (a.hadContribution) {
165
+ const ids = a.droppedIds && a.droppedIds.length
166
+ ? ` (ids: ${a.droppedIds.join(", ")})`
167
+ : "";
168
+ console.log(
169
+ ` ${label} jar purged ${a.droppedEntries} entries from ${a.jarPath}${ids}`
170
+ );
171
+ } else {
172
+ console.log(
173
+ ` ${label} jar no contribution to purge in ${a.jarPath}`
174
+ );
175
+ }
176
+ } else {
177
+ console.log(
178
+ ` ${label} ext ${a.removedExtDir ? "removed" : "(absent)"} ${a.extDir}`
179
+ );
180
+ }
181
+ if (a.hostFederationDir) {
182
+ console.log(
183
+ ` host types ${a.typesRemoved ? "removed" : "(absent)"}: ${a.hostFederationDir}`
184
+ );
185
+ }
186
+ }
187
+
188
+ if (skipped.length) {
189
+ console.log(`\n=== Skipped ===`);
190
+ for (const s of skipped) console.log(` ${s.id}: ${s.reason}`);
191
+ }
192
+
193
+ if (errors.length) {
194
+ console.log(`\n=== Errors ===`);
195
+ for (const e of errors)
196
+ console.log(` \x1b[31m${e.id}\x1b[0m [${e.stage}] ${e.message}`);
197
+ }
198
+
199
+ const jars = actions.filter((a) => a.kind === "jar");
200
+ const exts = actions.filter((a) => a.kind === "ext");
201
+ const typesRemoved = actions.filter((a) => a.typesRemoved).length;
202
+ const noopJars = jars.filter((a) => !a.hadContribution).length;
203
+ const removedExt = exts.filter((a) => a.removedExtDir).length;
204
+
205
+ console.log(`\n=== Summary ===`);
206
+ console.log(
207
+ ` acted on ${actions.length} target(s): ${jars.length} jar, ${exts.length} ext`
208
+ );
209
+ if (jars.length) {
210
+ const totalEntries = jars.reduce((s, a) => s + (a.droppedEntries || 0), 0);
211
+ console.log(
212
+ ` jar: ${totalEntries} entries dropped` +
213
+ (noopJars ? `; ${noopJars} jar(s) had no contribution` : "")
214
+ );
215
+ }
216
+ if (exts.length) {
217
+ console.log(
218
+ ` ext: ${removedExt} dir(s) removed` +
219
+ (exts.length - removedExt
220
+ ? `; ${exts.length - removedExt} already absent`
221
+ : "")
222
+ );
223
+ }
224
+ if (typesRemoved) {
225
+ console.log(` host: ${typesRemoved} .federation/<id>/ dir(s) removed`);
226
+ }
227
+ if (skipped.length) console.log(` skipped: ${skipped.length}`);
228
+ if (errors.length) console.log(` \x1b[31merrors: ${errors.length}\x1b[0m`);
229
+ }
230
+
231
+ module.exports = { purgeRemotes, purgeOne, purgeJarPath };
@@ -0,0 +1,121 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { resolveProjectPom, readArtifactId } = require("./pom");
4
+ const { resolveOne } = require("./jar-glob");
5
+
6
+ function readWsconfig(workspaceDir) {
7
+ const file = path.join(workspaceDir, "wsconfig.json");
8
+ if (!fs.existsSync(file)) {
9
+ throw new Error(
10
+ `No wsconfig.json in ${workspaceDir}. Run \`ws-wire-host <wpm-root>\` first.`
11
+ );
12
+ }
13
+ return JSON.parse(fs.readFileSync(file, "utf8"));
14
+ }
15
+
16
+ function readModulesJson(wpmRoot) {
17
+ const file = path.join(wpmRoot, "modules.json");
18
+ if (!fs.existsSync(file)) {
19
+ throw new Error(`No modules.json at ${wpmRoot}.`);
20
+ }
21
+ return JSON.parse(fs.readFileSync(file, "utf8"));
22
+ }
23
+
24
+ function readProjectPkg(workspaceDir, name) {
25
+ const pkgPath = path.join(workspaceDir, "projects", name, "package.json");
26
+ if (!fs.existsSync(pkgPath)) return {};
27
+ try {
28
+ return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
29
+ } catch {
30
+ return {};
31
+ }
32
+ }
33
+
34
+ // Load the deployment context once and reuse across step runners (deploy,
35
+ // purge, ...). Returns { wsconfig, wpmRoot, modulesFolder, modulesDir,
36
+ // adminBuildFolder, adminDir }.
37
+ function loadDeployContext(workspaceDir) {
38
+ const wsconfig = readWsconfig(workspaceDir);
39
+ if (!wsconfig.wpmRoot) {
40
+ throw new Error(
41
+ "wsconfig.json missing wpmRoot. Run `ws-wire-host <wpm-root>`."
42
+ );
43
+ }
44
+ const modulesJson = readModulesJson(wsconfig.wpmRoot);
45
+ const modulesFolder = modulesJson.modulesFolder || "./modules";
46
+ const adminBuildFolder = modulesJson.adminBuildFolder || "./admin";
47
+ const modulesDir = path.resolve(wsconfig.wpmRoot, modulesFolder);
48
+ const adminDir = path.resolve(wsconfig.wpmRoot, adminBuildFolder);
49
+ return {
50
+ wsconfig,
51
+ wpmRoot: wsconfig.wpmRoot,
52
+ modulesFolder,
53
+ adminBuildFolder,
54
+ modulesDir,
55
+ adminDir,
56
+ };
57
+ }
58
+
59
+ // Decide jar vs ext for a project, honouring wsconfig override > package.json.
60
+ // Returns { kind: "jar", jarPath, pomPath?, artifactId? } or
61
+ // { kind: "ext", extDir }.
62
+ function resolveTarget({
63
+ workspaceDir,
64
+ projectName,
65
+ wsconfig,
66
+ wpmRoot,
67
+ modulesFolder,
68
+ }) {
69
+ const modulesDir = path.resolve(wpmRoot, modulesFolder);
70
+ const extDir = path.join(
71
+ wpmRoot,
72
+ "scripts",
73
+ "ext-admin-remotes",
74
+ projectName
75
+ );
76
+
77
+ // 1. wsconfig override
78
+ const override =
79
+ wsconfig.deployOverrides && wsconfig.deployOverrides[projectName];
80
+ if (override) {
81
+ if (override.jar && override.ext) {
82
+ throw new Error(
83
+ `wsconfig.deployOverrides.${projectName}: set exactly one of 'jar' or 'ext', not both`
84
+ );
85
+ }
86
+ if (override.ext === true) {
87
+ return { kind: "ext", extDir };
88
+ }
89
+ if (override.jar) {
90
+ const jarPath = resolveOne(modulesDir, override.jar, projectName);
91
+ return { kind: "jar", jarPath };
92
+ }
93
+ }
94
+
95
+ // 2. project package.json
96
+ const pkg = readProjectPkg(workspaceDir, projectName);
97
+ const target = (pkg.wsmodules && pkg.wsmodules.target) || "jar";
98
+ if (target === "ext") {
99
+ return { kind: "ext", extDir };
100
+ }
101
+ if (target !== "jar") {
102
+ throw new Error(
103
+ `Invalid wsmodules.target for ${projectName}: ${JSON.stringify(target)} (expected 'jar' or 'ext')`
104
+ );
105
+ }
106
+
107
+ // 3. pom-derived artifactId
108
+ const pomField = pkg.wsmodules && pkg.wsmodules.pom;
109
+ const pomPath = resolveProjectPom(workspaceDir, projectName, pomField);
110
+ const artifactId = readArtifactId(pomPath);
111
+ const jarPath = resolveOne(modulesDir, `${artifactId}-*.jar`, projectName);
112
+ return { kind: "jar", jarPath, pomPath, artifactId };
113
+ }
114
+
115
+ module.exports = {
116
+ readWsconfig,
117
+ readModulesJson,
118
+ readProjectPkg,
119
+ loadDeployContext,
120
+ resolveTarget,
121
+ };
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ws-test-realm/admin-kit",
3
- "version": "0.1.7",
3
+ "version": "0.1.10",
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,6 +12,7 @@
12
12
  "build": "ws-modules --build",
13
13
  "pack": "ws-modules --pack",
14
14
  "deploy": "ws-modules --deploy",
15
+ "purge": "ws-purge",
15
16
  "wire": "ws-modules --build --pack --deploy"
16
17
  },
17
18
  "prettier": "@ws-test-realm/devkit/prettier.config.js",