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

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/hooks/ngcc.js ADDED
@@ -0,0 +1,41 @@
1
+ // Built-in federation postInstall hook: run ngcc against the federation
2
+ // node_modules tree so every @angular package gets `__processed_by_ivy_ngcc__`
3
+ // markers and class definitions rewritten in place (ɵcmp/ɵdir/etc).
4
+ //
5
+ // Producers reference this hook from their lib's package.json:
6
+ //
7
+ // "wsmodules": {
8
+ // "federationHooks": { "postInstall": "@ws-test-realm/admin-kit/hooks/ngcc" }
9
+ // }
10
+ //
11
+ // The hook resolves `@angular/compiler-cli/ngcc` from the PRODUCER workspace
12
+ // (where compiler-cli is installed as a dev dep, alongside ng-cli, etc.).
13
+ // The federation tree itself only holds runtime peer deps and won't have
14
+ // compiler-cli unless a lib explicitly declares it.
15
+
16
+ const Module = require("module");
17
+ const path = require("path");
18
+
19
+ module.exports = function ngccHook({ federationDir, producerWorkspaceDir, libName }) {
20
+ const requireFromProducer = Module.createRequire(
21
+ path.join(producerWorkspaceDir, "package.json")
22
+ );
23
+ let ngcc;
24
+ try {
25
+ ngcc = requireFromProducer("@angular/compiler-cli/ngcc");
26
+ } catch (e) {
27
+ console.warn(
28
+ `\x1b[33mWarning:\x1b[0m ngcc hook (${libName}): couldn't load @angular/compiler-cli/ngcc from ${producerWorkspaceDir}: ${e.message}`
29
+ );
30
+ return;
31
+ }
32
+
33
+ const basePath = path.join(federationDir, "node_modules");
34
+ console.log(` ngcc: rewriting ${basePath}`);
35
+ ngcc.process({
36
+ basePath,
37
+ propertiesToConsider: ["module_ivy_ngcc", "browser", "module", "main"],
38
+ compileAllFormats: false,
39
+ async: false,
40
+ });
41
+ };
@@ -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, workspaceDir);
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,184 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const Module = require("module");
4
+ const { execSync } = require("child_process");
5
+
6
+ // Mirror a federation lib's peerDependencies into its dependencies in the
7
+ // STAGED package.json under <adminDir>/.federation/<id>/. ng-packagr emits
8
+ // peerDependencies — the canonical contract for a published library — but
9
+ // for our publish-less federation we want npm to actually INSTALL those
10
+ // peers (so consuming workspaces' TypeScript can resolve their types via
11
+ // walk-up from .federation/<id>/). Promoting peer → dep in the staged copy
12
+ // only doesn't touch the producer's source.
13
+ function promotePeerDeps(libDir) {
14
+ const pkgPath = path.join(libDir, "package.json");
15
+ if (!fs.existsSync(pkgPath)) return false;
16
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
17
+ if (!pkg.peerDependencies || !Object.keys(pkg.peerDependencies).length) {
18
+ return false;
19
+ }
20
+ pkg.dependencies = Object.assign({}, pkg.dependencies, pkg.peerDependencies);
21
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
22
+ return true;
23
+ }
24
+
25
+ // Reconcile the entire .federation/ dir as an npm workspace root. Lists
26
+ // every lib subdir as a workspace, runs `npm install` (npm hoists shared
27
+ // peer versions, nests only on per-lib disagreement), then runs each
28
+ // lib's declared postInstall hooks against the freshly installed tree.
29
+ //
30
+ // Hooks let a federation lib ship arbitrary prep work that consumers run
31
+ // automatically — e.g. ws-framework declares
32
+ // `wsmodules.federationHooks.postInstall = "@ws-test-realm/admin-kit/hooks/ngcc"`
33
+ // so the consumer's install pipeline Ivy-rewrites the Angular tree in
34
+ // place. admin-kit knows nothing about Angular here; the producer's
35
+ // declaration drives what runs.
36
+ //
37
+ // Idempotent. Re-runs at the end of every deploy.
38
+ function syncFederationWorkspace(adminDir, producerWorkspaceDir) {
39
+ const federationDir = path.join(adminDir, ".federation");
40
+ if (!fs.existsSync(federationDir)) return { installed: false };
41
+
42
+ const libs = listFederationLibs(federationDir);
43
+ if (!libs.length) return { installed: false };
44
+
45
+ writeWorkspaceManifest(federationDir, libs);
46
+
47
+ console.log(
48
+ `\n=== federation peers: install (${libs.length} workspace${libs.length === 1 ? "" : "s"}) ===`
49
+ );
50
+ console.log(` at ${federationDir}`);
51
+ execSync("npm install --no-package-lock --no-audit --no-fund", {
52
+ cwd: federationDir,
53
+ stdio: "inherit",
54
+ });
55
+
56
+ runFederationHooks({ federationDir, libs, producerWorkspaceDir });
57
+
58
+ return { installed: true, libs };
59
+ }
60
+
61
+ // Walk each federation lib's package.json for declared postInstall hooks
62
+ // (`wsmodules.federationHooks.postInstall`, string or array). Dedupe by
63
+ // hook reference — multiple libs declaring the same hook (e.g. all
64
+ // Angular libs referencing the built-in ngcc hook) run it once. Each hook
65
+ // is a CommonJS module exporting a default function with signature
66
+ // `({ federationDir, libDir, libName, producerWorkspaceDir }) => void`.
67
+ //
68
+ // Hook reference resolution:
69
+ // - "./..." or "../..." → relative to the lib's federation dir
70
+ // - any other specifier → node `require` resolution, first trying
71
+ // admin-kit's own neighborhood (built-in hooks
72
+ // like @ws-test-realm/admin-kit/hooks/ngcc),
73
+ // then the producer workspace (third-party
74
+ // hook packages).
75
+ function runFederationHooks({ federationDir, libs, producerWorkspaceDir }) {
76
+ const queue = [];
77
+ const seen = new Set();
78
+ for (const libName of libs) {
79
+ const libDir = path.join(federationDir, libName);
80
+ const pkg = safeReadJson(path.join(libDir, "package.json"));
81
+ const refs = collectHookRefs(pkg);
82
+ for (const ref of refs) {
83
+ if (seen.has(ref)) continue;
84
+ seen.add(ref);
85
+ queue.push({ ref, libDir, libName });
86
+ }
87
+ }
88
+ if (!queue.length) return;
89
+
90
+ console.log(
91
+ `\n=== federation peers: postInstall hooks (${queue.length}) ===`
92
+ );
93
+ for (const { ref, libDir, libName } of queue) {
94
+ console.log(` ${libName} → ${ref}`);
95
+ let hookFn;
96
+ try {
97
+ hookFn = resolveHook({ ref, libDir, producerWorkspaceDir });
98
+ } catch (e) {
99
+ console.warn(
100
+ ` \x1b[33mWarning:\x1b[0m couldn't load hook (${e.message})`
101
+ );
102
+ continue;
103
+ }
104
+ if (typeof hookFn !== "function") {
105
+ console.warn(
106
+ ` \x1b[33mWarning:\x1b[0m hook ${ref} did not export a function`
107
+ );
108
+ continue;
109
+ }
110
+ try {
111
+ hookFn({ federationDir, libDir, libName, producerWorkspaceDir });
112
+ } catch (e) {
113
+ console.warn(
114
+ ` \x1b[33mWarning:\x1b[0m hook ${ref} threw: ${e.message}`
115
+ );
116
+ }
117
+ }
118
+ }
119
+
120
+ function collectHookRefs(pkg) {
121
+ if (!pkg || !pkg.wsmodules || !pkg.wsmodules.federationHooks) return [];
122
+ const v = pkg.wsmodules.federationHooks.postInstall;
123
+ if (!v) return [];
124
+ return Array.isArray(v) ? v : [v];
125
+ }
126
+
127
+ function resolveHook({ ref, libDir, producerWorkspaceDir }) {
128
+ let resolvedPath;
129
+ if (ref.startsWith("./") || ref.startsWith("../")) {
130
+ resolvedPath = path.resolve(libDir, ref);
131
+ } else {
132
+ const adminKitRequire = Module.createRequire(
133
+ path.join(__dirname, "..", "package.json")
134
+ );
135
+ try {
136
+ resolvedPath = adminKitRequire.resolve(ref);
137
+ } catch {
138
+ const producerRequire = Module.createRequire(
139
+ path.join(producerWorkspaceDir, "package.json")
140
+ );
141
+ resolvedPath = producerRequire.resolve(ref);
142
+ }
143
+ }
144
+ return require(resolvedPath);
145
+ }
146
+
147
+ function listFederationLibs(federationDir) {
148
+ return fs
149
+ .readdirSync(federationDir, { withFileTypes: true })
150
+ .filter((e) => e.isDirectory())
151
+ .map((e) => e.name)
152
+ .filter((name) => {
153
+ if (name === "node_modules") return false;
154
+ return fs.existsSync(path.join(federationDir, name, "package.json"));
155
+ })
156
+ .sort();
157
+ }
158
+
159
+ function writeWorkspaceManifest(federationDir, libs) {
160
+ const pkg = {
161
+ name: "ws-federation-host",
162
+ private: true,
163
+ version: "0.0.0",
164
+ workspaces: libs.map((id) => `./${id}`),
165
+ };
166
+ fs.writeFileSync(
167
+ path.join(federationDir, "package.json"),
168
+ JSON.stringify(pkg, null, 2) + "\n"
169
+ );
170
+ }
171
+
172
+ function safeReadJson(file) {
173
+ try {
174
+ return JSON.parse(fs.readFileSync(file, "utf8"));
175
+ } catch {
176
+ return null;
177
+ }
178
+ }
179
+
180
+ module.exports = {
181
+ promotePeerDeps,
182
+ syncFederationWorkspace,
183
+ listFederationLibs,
184
+ };
@@ -3,6 +3,7 @@ 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
8
  // Per-remote purge action. Returns a structured action record.
8
9
  function purgeOne({ workspaceDir, id, ctx }) {
@@ -120,6 +121,20 @@ function purgeRemotes({
120
121
  }
121
122
 
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, workspaceDir);
131
+ } catch (e) {
132
+ console.warn(
133
+ `\x1b[33mWarning:\x1b[0m federation peer resync failed: ${e.message}`
134
+ );
135
+ }
136
+ }
137
+
123
138
  return { actions, skipped, errors };
124
139
  }
125
140
 
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.10",
3
+ "version": "0.1.13",
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": {
@@ -21,6 +21,7 @@
21
21
  "files": [
22
22
  "bin",
23
23
  "lib",
24
+ "hooks",
24
25
  "template",
25
26
  "template-module",
26
27
  "README.md"