@ws-test-realm/admin-kit 0.1.13 → 0.2.1-ng16
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-purge.js +0 -0
- package/lib/build-modules.js +10 -14
- package/lib/cli-args.js +2 -2
- package/lib/deploy-remotes.js +55 -76
- package/lib/emit-descriptor.js +8 -14
- package/lib/generate-module.js +19 -9
- package/lib/identity.js +19 -1
- package/lib/pack-into-jar.js +3 -3
- package/lib/purge-remotes.js +0 -14
- package/package.json +6 -8
- package/template/package.json +6 -6
- package/template/tsconfig.json +1 -1
- package/template-module/federation.config.js +11 -0
- package/template-module/package.json +9 -6
- package/template-module/src/index.html +8 -0
- package/template-module/src/main.ts +6 -0
- package/template-module/src/polyfills.ts +1 -0
- package/template-module/tsconfig.app.json +9 -0
- package/bin/ws-pack-remote.js +0 -196
- package/hooks/ngcc.js +0 -41
- package/lib/alias-discovery.js +0 -40
- package/lib/federation-peers.js +0 -184
- package/lib/shared-deps.js +0 -90
- package/lib/synthetic-entry.js +0 -27
- package/lib/webpack-factory.js +0 -120
- package/template-module/ng-package.json +0 -8
- package/template-module/tsconfig.lib.json +0 -13
- package/template-module/tsconfig.lib.prod.json +0 -9
package/bin/ws-purge.js
CHANGED
|
File without changes
|
package/lib/build-modules.js
CHANGED
|
@@ -16,8 +16,8 @@ function loadOrder({ workspaceDir, restrictTo, withDeps = false }) {
|
|
|
16
16
|
|
|
17
17
|
// A project declares itself a library (not a federated remote) by setting
|
|
18
18
|
// `wsmodules.kind = "library"` in its own package.json. Default kind is
|
|
19
|
-
// "remote": build/
|
|
20
|
-
// is consumed by sibling projects) but skip
|
|
19
|
+
// "remote": build/deploy both run. Libraries are still built (their dist
|
|
20
|
+
// is consumed by sibling projects) but skip deploy.
|
|
21
21
|
function isLibrary(workspaceDir, name) {
|
|
22
22
|
const pkgPath = path.join(workspaceDir, "projects", name, "package.json");
|
|
23
23
|
if (!fs.existsSync(pkgPath)) return false;
|
|
@@ -53,31 +53,27 @@ function buildLibs({ workspaceDir, restrictTo = [], withDeps = false }) {
|
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
console.log(`\nBuilt ${order.length}
|
|
56
|
+
console.log(`\nBuilt ${order.length} project(s).`);
|
|
57
57
|
return { order };
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// Under native-federation the artifact (`remoteEntry.json` + chunks) is
|
|
61
|
+
// produced directly by `ng build` via the @angular-architects/native-federation
|
|
62
|
+
// builder. There is no separate webpack pack step. Kept as a no-op so the
|
|
63
|
+
// `npm run wire = ws-modules --build --pack --deploy` convention keeps
|
|
64
|
+
// working without churn.
|
|
60
65
|
function packRemotes({ workspaceDir, restrictTo = [], withDeps = false }) {
|
|
61
66
|
const { order } = loadOrder({ workspaceDir, restrictTo, withDeps });
|
|
62
67
|
const { remotes, libraries } = partitionByKind(workspaceDir, order);
|
|
63
68
|
|
|
64
|
-
console.log(`\n=== Pack
|
|
65
|
-
remotes.forEach((n, i) => console.log(` ${i + 1}. ${n}`));
|
|
69
|
+
console.log(`\n=== Pack (no-op under native-federation) ===`);
|
|
70
|
+
remotes.forEach((n, i) => console.log(` ${i + 1}. ${n} (artifact already produced by ng build)`));
|
|
66
71
|
if (libraries.length) {
|
|
67
72
|
console.log(
|
|
68
73
|
` (skipping libraries: ${libraries.join(", ")} — wsmodules.kind=library)`
|
|
69
74
|
);
|
|
70
75
|
}
|
|
71
76
|
|
|
72
|
-
for (const name of remotes) {
|
|
73
|
-
console.log(`\n=== ws-pack-remote ${name} ===`);
|
|
74
|
-
execSync(
|
|
75
|
-
`ws-pack-remote --project projects/${name} --workspace .`,
|
|
76
|
-
{ cwd: workspaceDir, stdio: "inherit" }
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
console.log(`\nPacked ${remotes.length} remote(s).`);
|
|
81
77
|
return { order: remotes, libraries };
|
|
82
78
|
}
|
|
83
79
|
|
package/lib/cli-args.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Shared CLI arg parsing + selection-mode resolution for ws-
|
|
2
|
-
//
|
|
1
|
+
// Shared CLI arg parsing + selection-mode resolution for the ws-modules
|
|
2
|
+
// driver (build + deploy across an admin-modules workspace).
|
|
3
3
|
//
|
|
4
4
|
// Selection modes:
|
|
5
5
|
// inert — no positional args; print usage, do nothing
|
package/lib/deploy-remotes.js
CHANGED
|
@@ -4,37 +4,47 @@ 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 {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
fs.rmSync(hostFederationDir, { recursive: true, force: true });
|
|
7
|
+
const { emitDescriptor } = require("./emit-descriptor");
|
|
8
|
+
const { readIdentity } = require("./identity");
|
|
9
|
+
|
|
10
|
+
// Resolve the build output for a native-federation remote. The Angular
|
|
11
|
+
// application builder (esbuild) emits to `dist/<id>/browser/` by default;
|
|
12
|
+
// older builder configurations may still write to `dist/<id>/`. Probe both
|
|
13
|
+
// and return whichever contains `remoteEntry.json`.
|
|
14
|
+
function resolveBuildOutput(workspaceDir, id) {
|
|
15
|
+
const candidates = [
|
|
16
|
+
path.join(workspaceDir, "dist", id, "browser"),
|
|
17
|
+
path.join(workspaceDir, "dist", id),
|
|
18
|
+
];
|
|
19
|
+
for (const c of candidates) {
|
|
20
|
+
if (fs.existsSync(path.join(c, "remoteEntry.json"))) return c;
|
|
22
21
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Native-federation artifact not found for '${id}'. Expected remoteEntry.json under one of: ${candidates.join(
|
|
24
|
+
", "
|
|
25
|
+
)}. Did the build step run?`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Stage the build output + descriptor into a deploy-ready tree:
|
|
30
|
+
// <stagingDir>/<id>/{federation.json, remote/<all chunks + remoteEntry.json>}
|
|
31
|
+
// `remote/` mirrors the path the federation controller serves contributions
|
|
32
|
+
// under, so packing-into-jar and ext-copy are uniform.
|
|
33
|
+
function stageContribution({ workspaceDir, id, remoteName, hostId }) {
|
|
34
|
+
const buildDir = resolveBuildOutput(workspaceDir, id);
|
|
35
|
+
const stagingRoot = path.join(workspaceDir, "dist", "admin-remotes");
|
|
36
|
+
const idDir = path.join(stagingRoot, id);
|
|
37
|
+
if (fs.existsSync(idDir)) {
|
|
38
|
+
fs.rmSync(idDir, { recursive: true, force: true });
|
|
27
39
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
const remoteDir = path.join(idDir, "remote");
|
|
41
|
+
fs.mkdirSync(remoteDir, { recursive: true });
|
|
42
|
+
fs.cpSync(buildDir, remoteDir, { recursive: true });
|
|
43
|
+
emitDescriptor({ outDir: stagingRoot, id, hostId, remoteName });
|
|
44
|
+
return idDir;
|
|
32
45
|
}
|
|
33
46
|
|
|
34
|
-
|
|
35
|
-
// dir (replace-existing semantics). Also warn if a same-id-named jar exists
|
|
36
|
-
// in the modules folder — possible name-collision abuse signal.
|
|
37
|
-
function deployExt({ workspaceDir, id, modulesDir, extDir }) {
|
|
47
|
+
function deployExt({ workspaceDir, id, modulesDir, extDir, stagedDir }) {
|
|
38
48
|
const collisions = findMatches(modulesDir, `${id}-*.jar`);
|
|
39
49
|
if (collisions.length) {
|
|
40
50
|
const list = collisions.map((c) => path.join(modulesDir, c)).join(", ");
|
|
@@ -42,17 +52,11 @@ function deployExt({ workspaceDir, id, modulesDir, extDir }) {
|
|
|
42
52
|
`\x1b[33mWarning:\x1b[0m a jar matching '${id}-*.jar' exists at ${list}; this project is targeting ext, but the name collision may indicate abuse.`
|
|
43
53
|
);
|
|
44
54
|
}
|
|
45
|
-
const srcDir = path.join(workspaceDir, "dist", "admin-remotes", id);
|
|
46
|
-
if (!fs.existsSync(srcDir)) {
|
|
47
|
-
throw new Error(
|
|
48
|
-
`Federation artifact missing: ${srcDir}. Run \`pack:remote\` first.`
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
55
|
if (fs.existsSync(extDir)) {
|
|
52
56
|
fs.rmSync(extDir, { recursive: true, force: true });
|
|
53
57
|
}
|
|
54
58
|
fs.mkdirSync(extDir, { recursive: true });
|
|
55
|
-
fs.cpSync(
|
|
59
|
+
fs.cpSync(stagedDir, extDir, { recursive: true });
|
|
56
60
|
let fileCount = 0;
|
|
57
61
|
const walk = (d) => {
|
|
58
62
|
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
@@ -74,19 +78,13 @@ function deployOne({
|
|
|
74
78
|
modulesDir,
|
|
75
79
|
wsconfig,
|
|
76
80
|
}) {
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
if (!fs.existsSync(distLibDir)) {
|
|
86
|
-
throw new Error(`Lib dist missing: ${distLibDir}. Run \`build\` first.`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const hostFederationDir = path.join(adminDir, ".federation", id);
|
|
81
|
+
const identity = readIdentity(workspaceDir, id);
|
|
82
|
+
const stagedDir = stageContribution({
|
|
83
|
+
workspaceDir,
|
|
84
|
+
id,
|
|
85
|
+
remoteName: identity.remoteName,
|
|
86
|
+
hostId: identity.hostId,
|
|
87
|
+
});
|
|
90
88
|
|
|
91
89
|
const target = resolveTarget({
|
|
92
90
|
workspaceDir,
|
|
@@ -96,33 +94,29 @@ function deployOne({
|
|
|
96
94
|
modulesFolder,
|
|
97
95
|
});
|
|
98
96
|
|
|
99
|
-
let artifactReport;
|
|
100
97
|
if (target.kind === "jar") {
|
|
101
98
|
const packResult = packIntoJar({
|
|
102
99
|
jarPath: target.jarPath,
|
|
103
|
-
sourceDir:
|
|
100
|
+
sourceDir: stagedDir,
|
|
104
101
|
id,
|
|
105
102
|
});
|
|
106
|
-
|
|
103
|
+
return {
|
|
107
104
|
kind: "jar",
|
|
108
105
|
jarPath: target.jarPath,
|
|
109
106
|
pomPath: target.pomPath,
|
|
110
107
|
artifactId: target.artifactId,
|
|
111
108
|
...packResult,
|
|
112
109
|
};
|
|
113
|
-
} else {
|
|
114
|
-
const extResult = deployExt({
|
|
115
|
-
workspaceDir,
|
|
116
|
-
id,
|
|
117
|
-
modulesDir,
|
|
118
|
-
extDir: target.extDir,
|
|
119
|
-
});
|
|
120
|
-
artifactReport = { kind: "ext", ...extResult };
|
|
121
110
|
}
|
|
122
111
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
112
|
+
const extResult = deployExt({
|
|
113
|
+
workspaceDir,
|
|
114
|
+
id,
|
|
115
|
+
modulesDir,
|
|
116
|
+
extDir: target.extDir,
|
|
117
|
+
stagedDir,
|
|
118
|
+
});
|
|
119
|
+
return { kind: "ext", ...extResult };
|
|
126
120
|
}
|
|
127
121
|
|
|
128
122
|
function deployRemotes({ workspaceDir, restrictTo = [], withDeps = false }) {
|
|
@@ -167,26 +161,11 @@ function deployRemotes({ workspaceDir, restrictTo = [], withDeps = false }) {
|
|
|
167
161
|
console.log(` ext dir: ${r.extDir}`);
|
|
168
162
|
console.log(` copied files: ${r.copiedFiles}`);
|
|
169
163
|
}
|
|
170
|
-
console.log(` host types: ${r.hostFederationDir}`);
|
|
171
164
|
results.push({ id, ...r });
|
|
172
165
|
}
|
|
173
166
|
|
|
174
167
|
console.log(`\nDeployed ${results.length} module(s).`);
|
|
175
168
|
|
|
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
|
-
|
|
190
169
|
return { results };
|
|
191
170
|
}
|
|
192
171
|
|
package/lib/emit-descriptor.js
CHANGED
|
@@ -3,26 +3,20 @@ const path = require("path");
|
|
|
3
3
|
|
|
4
4
|
// Write <out>/<id>/federation.json — the descriptor the admin federation
|
|
5
5
|
// controller reads (both jar-side and ext-side).
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}) {
|
|
6
|
+
//
|
|
7
|
+
// Under native-federation, the only field the fiddle controller needs from
|
|
8
|
+
// each descriptor to build the manifest is `remoteName`. The host loads the
|
|
9
|
+
// remote by fetching `remoteEntry.json` at the asset URL the controller
|
|
10
|
+
// returns; no separate `exposedModule` lookup is required because the
|
|
11
|
+
// import-map carries the entry mapping directly.
|
|
12
|
+
function emitDescriptor({ outDir, id, hostId, remoteName }) {
|
|
14
13
|
const dir = path.join(outDir, id);
|
|
15
14
|
fs.mkdirSync(dir, { recursive: true });
|
|
16
15
|
const file = path.join(dir, "federation.json");
|
|
17
16
|
const descriptor = {
|
|
18
17
|
hostId,
|
|
19
18
|
contributionId: id,
|
|
20
|
-
manifestEntry: {
|
|
21
|
-
id,
|
|
22
|
-
name: displayName,
|
|
23
|
-
remoteName,
|
|
24
|
-
exposedModule: exposes,
|
|
25
|
-
},
|
|
19
|
+
manifestEntry: { remoteName },
|
|
26
20
|
};
|
|
27
21
|
fs.writeFileSync(file, JSON.stringify(descriptor, null, 2) + "\n", "utf8");
|
|
28
22
|
return file;
|
package/lib/generate-module.js
CHANGED
|
@@ -51,26 +51,36 @@ function copyAndSubstitute(src, dest, tokens) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// Register the project as a native-federation application in angular.json.
|
|
55
|
+
// `build` is the @angular-architects/native-federation:build target, which
|
|
56
|
+
// post-processes the underlying esbuild app target (`esbuild`) to emit
|
|
57
|
+
// remoteEntry.json + shared/exposed chunk bundles next to the application
|
|
58
|
+
// bundle. Names match the federation.config.js `name` derivation.
|
|
54
59
|
function registerInAngularJson(workspaceDir, name) {
|
|
55
60
|
const angularJsonPath = path.join(workspaceDir, "angular.json");
|
|
56
61
|
const angular = JSON.parse(fs.readFileSync(angularJsonPath, "utf8"));
|
|
57
62
|
angular.projects = angular.projects || {};
|
|
58
63
|
angular.projects[name] = {
|
|
59
|
-
projectType: "
|
|
64
|
+
projectType: "application",
|
|
65
|
+
schematics: {},
|
|
60
66
|
root: `projects/${name}`,
|
|
61
67
|
sourceRoot: `projects/${name}/src`,
|
|
62
68
|
prefix: "ws",
|
|
63
69
|
architect: {
|
|
64
70
|
build: {
|
|
65
|
-
builder: "@angular-
|
|
66
|
-
options: {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
builder: "@angular-architects/native-federation:build",
|
|
72
|
+
options: { target: `${name}:esbuild` },
|
|
73
|
+
},
|
|
74
|
+
esbuild: {
|
|
75
|
+
builder: "@angular-devkit/build-angular:application",
|
|
76
|
+
options: {
|
|
77
|
+
outputPath: `dist/${name}`,
|
|
78
|
+
index: `projects/${name}/src/index.html`,
|
|
79
|
+
browser: `projects/${name}/src/main.ts`,
|
|
80
|
+
polyfills: [`projects/${name}/src/polyfills.ts`],
|
|
81
|
+
tsConfig: `projects/${name}/tsconfig.app.json`,
|
|
82
|
+
assets: [`projects/${name}/src/assets`],
|
|
72
83
|
},
|
|
73
|
-
defaultConfiguration: "production",
|
|
74
84
|
},
|
|
75
85
|
},
|
|
76
86
|
};
|
package/lib/identity.js
CHANGED
|
@@ -46,4 +46,22 @@ function resolveIdentity(args) {
|
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
// Lightweight identity lookup used by deploy-remotes.js. Derives the
|
|
50
|
+
// federated remote name from the project directory name (camelCased) and
|
|
51
|
+
// picks up the host id from the workspace's wsconfig.json (default "admin").
|
|
52
|
+
// The project's own `federation.config.js` declares the same `name` value;
|
|
53
|
+
// keeping the derivation here means deploy doesn't have to evaluate that
|
|
54
|
+
// config file at deploy time.
|
|
55
|
+
function readIdentity(workspaceDir, id) {
|
|
56
|
+
let hostId = "admin";
|
|
57
|
+
try {
|
|
58
|
+
const wsconfigPath = path.join(workspaceDir, "wsconfig.json");
|
|
59
|
+
if (fs.existsSync(wsconfigPath)) {
|
|
60
|
+
const ws = JSON.parse(fs.readFileSync(wsconfigPath, "utf8"));
|
|
61
|
+
if (ws.hostId) hostId = ws.hostId;
|
|
62
|
+
}
|
|
63
|
+
} catch {}
|
|
64
|
+
return { id, hostId, remoteName: camelCase(id) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { resolveIdentity, camelCase, readIdentity };
|
package/lib/pack-into-jar.js
CHANGED
|
@@ -22,9 +22,9 @@ function walk(dir, base = dir) {
|
|
|
22
22
|
// Repack a jar to carry a federated remote contribution under
|
|
23
23
|
// META-INF/federation/<id>/. Drops any pre-existing entries under that
|
|
24
24
|
// prefix (so re-deploys are clean), then adds every file under sourceDir
|
|
25
|
-
// at <prefix>/<relpath>. sourceDir is expected to be the
|
|
26
|
-
// dist/admin-remotes/<id>/ tree (
|
|
27
|
-
// federation.json
|
|
25
|
+
// at <prefix>/<relpath>. sourceDir is expected to be the staged
|
|
26
|
+
// dist/admin-remotes/<id>/ tree (federation.json + remote/* with the
|
|
27
|
+
// native-federation `remoteEntry.json` + chunks).
|
|
28
28
|
function packIntoJar({ jarPath, sourceDir, id }) {
|
|
29
29
|
if (!fs.existsSync(jarPath)) {
|
|
30
30
|
throw new Error(`Jar not found: ${jarPath}`);
|
package/lib/purge-remotes.js
CHANGED
|
@@ -3,7 +3,6 @@ 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");
|
|
7
6
|
|
|
8
7
|
// Per-remote purge action. Returns a structured action record.
|
|
9
8
|
function purgeOne({ workspaceDir, id, ctx }) {
|
|
@@ -122,19 +121,6 @@ function purgeRemotes({
|
|
|
122
121
|
|
|
123
122
|
reportActions({ actions, skipped, errors });
|
|
124
123
|
|
|
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
|
-
|
|
138
124
|
return { actions, skipped, errors };
|
|
139
125
|
}
|
|
140
126
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ws-test-realm/admin-kit",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Workflow CLI + scaffolding for Wiresphere admin-modules workspaces. Ships `ws-init-workspace
|
|
3
|
+
"version": "0.2.1-ng16",
|
|
4
|
+
"description": "Workflow CLI + scaffolding for Wiresphere admin-modules workspaces (Angular 16 + native-federation line). Ships `ws-init-workspace`, `ws-modules` (build+deploy driver), `ws-generate-module`/`ws-drop-module`, `ws-wire-host`, `ws-wire-pom`, `ws-sync-paths`, and `ws-purge`. Depends on @ws-test-realm/devkit (toolchain BOM) + @ws-test-realm/shared (runtime BOM + native-federation share map).",
|
|
5
5
|
"license": "Artistic-2.0",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
9
9
|
"bin": {
|
|
10
|
-
"ws-pack-remote": "./bin/ws-pack-remote.js",
|
|
11
10
|
"ws-init-workspace": "./bin/ws-init-workspace.js",
|
|
12
11
|
"ws-wire-host": "./bin/ws-wire-host.js",
|
|
13
12
|
"ws-wire-pom": "./bin/ws-wire-pom.js",
|
|
@@ -21,16 +20,15 @@
|
|
|
21
20
|
"files": [
|
|
22
21
|
"bin",
|
|
23
22
|
"lib",
|
|
24
|
-
"hooks",
|
|
25
23
|
"template",
|
|
26
24
|
"template-module",
|
|
27
25
|
"README.md"
|
|
28
26
|
],
|
|
29
27
|
"dependencies": {
|
|
30
|
-
"@
|
|
31
|
-
"@ws-test-realm/
|
|
28
|
+
"@angular-architects/native-federation": "^16.0.0",
|
|
29
|
+
"@ws-test-realm/devkit": "file:/Users/ph/projects/ws-admin-aux/devkit",
|
|
30
|
+
"@ws-test-realm/shared": "file:/Users/ph/projects/ws-admin-aux/shared",
|
|
32
31
|
"adm-zip": "^0.5.10",
|
|
33
|
-
"fast-xml-parser": "^4.3.0"
|
|
34
|
-
"tsconfig-paths-webpack-plugin": "^4.1.0"
|
|
32
|
+
"fast-xml-parser": "^4.3.0"
|
|
35
33
|
}
|
|
36
34
|
}
|
package/template/package.json
CHANGED
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
"wire": "ws-modules --build --pack --deploy"
|
|
17
17
|
},
|
|
18
18
|
"prettier": "@ws-test-realm/devkit/prettier.config.js",
|
|
19
|
-
"husky": {
|
|
20
|
-
"hooks": {
|
|
21
|
-
"pre-commit": "ws-pre-commit"
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
19
|
"dependencies": {
|
|
25
|
-
"@ws-test-realm/shared": "^0.
|
|
20
|
+
"@ws-test-realm/shared": "^0.4.0-ng16",
|
|
21
|
+
"@ws-test-realm/ws-core": "^0.1.0-ng16"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@ws-test-realm/admin-kit": "^0.2.0-ng16",
|
|
25
|
+
"@ws-test-realm/devkit": "^0.4.0-ng16"
|
|
26
26
|
}
|
|
27
27
|
}
|
package/template/tsconfig.json
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const { withNativeFederation } = require('@angular-architects/native-federation/config');
|
|
2
|
+
const { sharedDescriptors } = require('@ws-test-realm/shared');
|
|
3
|
+
|
|
4
|
+
module.exports = withNativeFederation({
|
|
5
|
+
name: '__camelName__Module',
|
|
6
|
+
exposes: {
|
|
7
|
+
'./Module': './src/public-api.ts',
|
|
8
|
+
},
|
|
9
|
+
shared: sharedDescriptors(),
|
|
10
|
+
skip: ['rxjs/ajax', 'rxjs/fetch', 'rxjs/testing', 'rxjs/webSocket'],
|
|
11
|
+
});
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
"name": "__name__",
|
|
3
3
|
"version": "0.0.1",
|
|
4
4
|
"peerDependencies": {
|
|
5
|
-
"@angular/common": "
|
|
6
|
-
"@angular/core": "
|
|
7
|
-
"@angular/forms": "
|
|
8
|
-
"@angular/router": "
|
|
9
|
-
"rxjs": "
|
|
5
|
+
"@angular/common": "16.2.12",
|
|
6
|
+
"@angular/core": "16.2.12",
|
|
7
|
+
"@angular/forms": "16.2.12",
|
|
8
|
+
"@angular/router": "16.2.12",
|
|
9
|
+
"rxjs": "~7.8.0"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"tslib": "^2.
|
|
12
|
+
"tslib": "^2.5.0"
|
|
13
|
+
},
|
|
14
|
+
"wsmodules": {
|
|
15
|
+
"target": "jar"
|
|
13
16
|
}
|
|
14
17
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Federated remote entry — this remote is consumed by the host via
|
|
2
|
+
// `loadRemoteModule('__camelName__Module', './Module')`. main.ts exists
|
|
3
|
+
// because the native-federation builder wraps an `application` builder
|
|
4
|
+
// target, which requires an entry. The actual exposed surface is
|
|
5
|
+
// `./src/public-api.ts` (see federation.config.js).
|
|
6
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import 'zone.js';
|
package/bin/ws-pack-remote.js
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// ws-pack-remote — pack a built lib's dist into a deploy-ready
|
|
3
|
-
// federated remote artifact. See README.md for usage.
|
|
4
|
-
|
|
5
|
-
const fs = require("fs");
|
|
6
|
-
const path = require("path");
|
|
7
|
-
const Module = require("module");
|
|
8
|
-
|
|
9
|
-
const { resolveIdentity } = require("../lib/identity");
|
|
10
|
-
const { discoverWorkspaceAliases } = require("../lib/alias-discovery");
|
|
11
|
-
const { writeSyntheticEntry, cleanup } = require("../lib/synthetic-entry");
|
|
12
|
-
const { buildSharedMap } = require("../lib/shared-deps");
|
|
13
|
-
const { buildWebpackConfig } = require("../lib/webpack-factory");
|
|
14
|
-
const { emitDescriptor } = require("../lib/emit-descriptor");
|
|
15
|
-
|
|
16
|
-
function parseArgs(argv) {
|
|
17
|
-
const aliases = {
|
|
18
|
-
project: "project",
|
|
19
|
-
workspace: "workspace",
|
|
20
|
-
out: "out",
|
|
21
|
-
id: "id",
|
|
22
|
-
host: "host",
|
|
23
|
-
"remote-name": "remoteName",
|
|
24
|
-
exposes: "exposes",
|
|
25
|
-
"display-name": "displayName",
|
|
26
|
-
config: "config",
|
|
27
|
-
};
|
|
28
|
-
const args = {};
|
|
29
|
-
for (let i = 2; i < argv.length; i++) {
|
|
30
|
-
const a = argv[i];
|
|
31
|
-
if (!a.startsWith("--")) continue;
|
|
32
|
-
const key = a.slice(2);
|
|
33
|
-
const mapped = aliases[key];
|
|
34
|
-
if (!mapped) {
|
|
35
|
-
console.error(`Unknown flag: --${key}`);
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
|
-
const val = argv[i + 1];
|
|
39
|
-
if (val === undefined || val.startsWith("--")) {
|
|
40
|
-
args[mapped] = true;
|
|
41
|
-
} else {
|
|
42
|
-
args[mapped] = val;
|
|
43
|
-
i++;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return args;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function require_(args, key) {
|
|
50
|
-
if (!args[key]) {
|
|
51
|
-
console.error(`Missing required flag: --${kebab(key)}`);
|
|
52
|
-
process.exit(1);
|
|
53
|
-
}
|
|
54
|
-
return args[key];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function kebab(s) {
|
|
58
|
-
return s.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function main() {
|
|
62
|
-
const args = parseArgs(process.argv);
|
|
63
|
-
require_(args, "project");
|
|
64
|
-
require_(args, "workspace");
|
|
65
|
-
|
|
66
|
-
const workspaceDir = path.resolve(args.workspace);
|
|
67
|
-
const identity = resolveIdentity(args);
|
|
68
|
-
const outDir = args.out
|
|
69
|
-
? path.resolve(args.out)
|
|
70
|
-
: path.join(workspaceDir, "dist", "admin-remotes");
|
|
71
|
-
|
|
72
|
-
const libDist = path.join(workspaceDir, "dist", identity.id);
|
|
73
|
-
if (!fs.existsSync(libDist)) {
|
|
74
|
-
console.error(
|
|
75
|
-
`Lib dist not found at ${libDist}. Run \`ng build ${identity.id}\` first.`
|
|
76
|
-
);
|
|
77
|
-
process.exit(1);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
console.log(
|
|
81
|
-
`ws-pack-remote: id=${identity.id} remoteName=${
|
|
82
|
-
identity.remoteName
|
|
83
|
-
} → ${path.join(outDir, identity.id)}/`
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
const aliasMap = discoverWorkspaceAliases(workspaceDir);
|
|
87
|
-
if (!aliasMap[identity.id]) {
|
|
88
|
-
console.error(
|
|
89
|
-
`Lib '${identity.id}' not recognized as ng-package output (missing module/main in dist/${identity.id}/package.json).`
|
|
90
|
-
);
|
|
91
|
-
process.exit(1);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const synthetic = writeSyntheticEntry(identity.id);
|
|
95
|
-
|
|
96
|
-
let webpack;
|
|
97
|
-
try {
|
|
98
|
-
const requireFromWorkspace = Module.createRequire(
|
|
99
|
-
path.join(workspaceDir, "package.json")
|
|
100
|
-
);
|
|
101
|
-
webpack = requireFromWorkspace("webpack");
|
|
102
|
-
} catch (e) {
|
|
103
|
-
console.error(
|
|
104
|
-
`Failed to load webpack from workspace ${workspaceDir}: ${e.message}`
|
|
105
|
-
);
|
|
106
|
-
cleanup(synthetic.dir);
|
|
107
|
-
process.exit(1);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
const sharedMap = buildSharedMap({
|
|
112
|
-
workspaceDir,
|
|
113
|
-
aliasMap,
|
|
114
|
-
selfId: identity.id,
|
|
115
|
-
sharedExtras: identity.sharedExtras,
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
const config = buildWebpackConfig({
|
|
119
|
-
workspaceDir,
|
|
120
|
-
syntheticEntry: synthetic.file,
|
|
121
|
-
outDir,
|
|
122
|
-
id: identity.id,
|
|
123
|
-
remoteName: identity.remoteName,
|
|
124
|
-
exposes: identity.exposes,
|
|
125
|
-
aliasMap,
|
|
126
|
-
sharedMap,
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
await runWebpack(webpack, config);
|
|
130
|
-
|
|
131
|
-
// Copy the lib's static assets (i18n JSON, etc.) into the remote
|
|
132
|
-
// dir so the controller serves them at the same URL space as the
|
|
133
|
-
// chunks. ng-packagr emits these to `dist/<id>/assets/` per the
|
|
134
|
-
// lib's ng-package.json `assets` field; the old *-remote projects
|
|
135
|
-
// mirrored them via angular.json `assets` globs — same effect.
|
|
136
|
-
const libAssets = path.join(workspaceDir, "dist", identity.id, "assets");
|
|
137
|
-
if (fs.existsSync(libAssets)) {
|
|
138
|
-
const targetAssets = path.join(outDir, identity.id, "remote", "assets");
|
|
139
|
-
fs.cpSync(libAssets, targetAssets, { recursive: true });
|
|
140
|
-
console.log(` + assets ${targetAssets}`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const descriptorPath = emitDescriptor({
|
|
144
|
-
outDir,
|
|
145
|
-
id: identity.id,
|
|
146
|
-
hostId: identity.hostId,
|
|
147
|
-
remoteName: identity.remoteName,
|
|
148
|
-
exposes: identity.exposes,
|
|
149
|
-
displayName: identity.displayName,
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
console.log(` + descriptor ${descriptorPath}`);
|
|
153
|
-
} finally {
|
|
154
|
-
cleanup(synthetic.dir);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function runWebpack(webpack, config) {
|
|
159
|
-
return new Promise((resolve, reject) => {
|
|
160
|
-
webpack(config, (err, stats) => {
|
|
161
|
-
if (err) {
|
|
162
|
-
reject(err);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
const info = stats.toJson({ errors: true, warnings: true });
|
|
166
|
-
if (stats.hasErrors()) {
|
|
167
|
-
for (const e of info.errors) {
|
|
168
|
-
console.error(e.message || e);
|
|
169
|
-
}
|
|
170
|
-
reject(new Error("webpack build failed"));
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
if (stats.hasWarnings()) {
|
|
174
|
-
for (const w of info.warnings) {
|
|
175
|
-
console.warn(w.message || w);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
console.log(
|
|
179
|
-
stats.toString({
|
|
180
|
-
colors: true,
|
|
181
|
-
modules: false,
|
|
182
|
-
children: false,
|
|
183
|
-
chunks: false,
|
|
184
|
-
chunkModules: false,
|
|
185
|
-
entrypoints: false,
|
|
186
|
-
})
|
|
187
|
-
);
|
|
188
|
-
resolve();
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
main().catch((e) => {
|
|
194
|
-
console.error(e.stack || e.message || e);
|
|
195
|
-
process.exit(1);
|
|
196
|
-
});
|
package/hooks/ngcc.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
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
|
-
};
|
package/lib/alias-discovery.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
|
|
4
|
-
// Scan <workspace>/dist/* for ng-package-style outputs and return a map
|
|
5
|
-
// {<libName>: <absolute path to dist/<libName>>}. An ng-package output is
|
|
6
|
-
// recognized by having a package.json that declares either "module" or
|
|
7
|
-
// "main" — that distinguishes it from the *-remote outputs (which we want
|
|
8
|
-
// to ignore — they are webpack bundles, not consumable libs).
|
|
9
|
-
function discoverWorkspaceAliases(workspaceDir) {
|
|
10
|
-
const distDir = path.join(workspaceDir, "dist");
|
|
11
|
-
if (!fs.existsSync(distDir)) return {};
|
|
12
|
-
|
|
13
|
-
const aliases = {};
|
|
14
|
-
for (const name of fs.readdirSync(distDir)) {
|
|
15
|
-
const dir = path.join(distDir, name);
|
|
16
|
-
let stat;
|
|
17
|
-
try {
|
|
18
|
-
stat = fs.statSync(dir);
|
|
19
|
-
} catch (e) {
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
if (!stat.isDirectory()) continue;
|
|
23
|
-
|
|
24
|
-
const pkgPath = path.join(dir, "package.json");
|
|
25
|
-
if (!fs.existsSync(pkgPath)) continue;
|
|
26
|
-
|
|
27
|
-
let pkg;
|
|
28
|
-
try {
|
|
29
|
-
pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
30
|
-
} catch (e) {
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
if (!pkg.module && !pkg.main) continue;
|
|
34
|
-
|
|
35
|
-
aliases[name] = dir;
|
|
36
|
-
}
|
|
37
|
-
return aliases;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
module.exports = { discoverWorkspaceAliases };
|
package/lib/federation-peers.js
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
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
|
-
};
|
package/lib/shared-deps.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
// Build the `shared` map for ModuleFederationPlugin.
|
|
2
|
-
//
|
|
3
|
-
// Layers (later wins):
|
|
4
|
-
// 1. sharedDescriptors() from @wiresphere/shared (host baseline:
|
|
5
|
-
// @angular/*, rxjs, etc.).
|
|
6
|
-
// 2. Workspace-discovered libs (from alias map) as WORKSPACE_LIBS shape.
|
|
7
|
-
// 3. Self-lib forced to import: false so the container does not load
|
|
8
|
-
// itself back through the shared scope.
|
|
9
|
-
// 4. sharedExtras from pack.config.json (project overrides).
|
|
10
|
-
//
|
|
11
|
-
// Then for any entry left with `requiredVersion: 'auto'` we resolve the
|
|
12
|
-
// version from <workspaceDir>/node_modules/<pkg>/package.json BEFORE handing
|
|
13
|
-
// the map to share(). share()'s built-in auto-probe reads the workspace's
|
|
14
|
-
// own package.json, which no longer lists Angular/rxjs/etc. directly — those
|
|
15
|
-
// are pulled in transitively via @wiresphere/shared. Probing node_modules
|
|
16
|
-
// closes that gap.
|
|
17
|
-
const fs = require("fs");
|
|
18
|
-
const path = require("path");
|
|
19
|
-
|
|
20
|
-
function packageRootOf(key) {
|
|
21
|
-
if (key.startsWith("@")) {
|
|
22
|
-
const parts = key.split("/");
|
|
23
|
-
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : key;
|
|
24
|
-
}
|
|
25
|
-
return key.split("/")[0];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function tryReadVersion(workspaceDir, pkgRoot) {
|
|
29
|
-
const pkgJsonPath = path.join(
|
|
30
|
-
workspaceDir,
|
|
31
|
-
"node_modules",
|
|
32
|
-
pkgRoot,
|
|
33
|
-
"package.json"
|
|
34
|
-
);
|
|
35
|
-
try {
|
|
36
|
-
return JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")).version || null;
|
|
37
|
-
} catch {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function resolveAutoVersions(map, workspaceDir) {
|
|
43
|
-
const out = {};
|
|
44
|
-
for (const [key, val] of Object.entries(map)) {
|
|
45
|
-
if (val && val.requiredVersion === "auto") {
|
|
46
|
-
const version = tryReadVersion(workspaceDir, packageRootOf(key));
|
|
47
|
-
out[key] = Object.assign({}, val, {
|
|
48
|
-
requiredVersion: version || false,
|
|
49
|
-
});
|
|
50
|
-
} else {
|
|
51
|
-
out[key] = val;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return out;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function buildSharedMap({ workspaceDir, aliasMap, selfId, sharedExtras }) {
|
|
58
|
-
const wsShared = require("@ws-test-realm/shared");
|
|
59
|
-
const { share } = require("@angular-architects/module-federation/webpack");
|
|
60
|
-
|
|
61
|
-
const WORKSPACE_LIBS = wsShared.WORKSPACE_LIBS || {
|
|
62
|
-
singleton: true,
|
|
63
|
-
strictVersion: false,
|
|
64
|
-
requiredVersion: false,
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const map = Object.assign({}, wsShared.sharedDescriptors());
|
|
68
|
-
|
|
69
|
-
for (const libName of Object.keys(aliasMap)) {
|
|
70
|
-
if (libName.endsWith("-remote")) continue;
|
|
71
|
-
if (libName === selfId) {
|
|
72
|
-
if (!map[libName]) {
|
|
73
|
-
map[libName] = Object.assign({}, WORKSPACE_LIBS);
|
|
74
|
-
}
|
|
75
|
-
} else {
|
|
76
|
-
map[libName] = Object.assign({}, WORKSPACE_LIBS, {
|
|
77
|
-
import: false,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
for (const [k, v] of Object.entries(sharedExtras || {})) {
|
|
83
|
-
map[k] = Object.assign({}, map[k] || {}, v);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const resolved = resolveAutoVersions(map, workspaceDir);
|
|
87
|
-
return share(resolved, workspaceDir);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
module.exports = { buildSharedMap };
|
package/lib/synthetic-entry.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
const os = require("os");
|
|
3
|
-
const path = require("path");
|
|
4
|
-
const crypto = require("crypto");
|
|
5
|
-
|
|
6
|
-
// Writes a temp directory containing a single `public-api.ts` that
|
|
7
|
-
// re-exports everything from the lib's import path. Webpack's MF
|
|
8
|
-
// `exposes['./Module']` points at this file; the import resolves
|
|
9
|
-
// through the workspace alias for `<id>` (set up in webpack.resolve).
|
|
10
|
-
function writeSyntheticEntry(id) {
|
|
11
|
-
const hash = crypto.randomBytes(4).toString("hex");
|
|
12
|
-
const dir = path.join(os.tmpdir(), "ws-pack-remote", `${id}-${hash}`);
|
|
13
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
14
|
-
const file = path.join(dir, "public-api.ts");
|
|
15
|
-
fs.writeFileSync(file, `export * from '${id}';\n`, "utf8");
|
|
16
|
-
return { dir, file };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function cleanup(dir) {
|
|
20
|
-
try {
|
|
21
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
22
|
-
} catch (e) {
|
|
23
|
-
// best-effort cleanup
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
module.exports = { writeSyntheticEntry, cleanup };
|
package/lib/webpack-factory.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
const Module = require("module");
|
|
4
|
-
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
|
5
|
-
|
|
6
|
-
// Build the in-memory webpack config used to produce a federated remote
|
|
7
|
-
// from a single synthetic entry that re-exports the lib's public API.
|
|
8
|
-
//
|
|
9
|
-
// We resolve the workspace's webpack + ModuleFederationPlugin so the
|
|
10
|
-
// build runs against the same versions used by the workspace's own
|
|
11
|
-
// dependency graph (avoids shared-scope ABI surprises).
|
|
12
|
-
function buildWebpackConfig({
|
|
13
|
-
workspaceDir,
|
|
14
|
-
syntheticEntry,
|
|
15
|
-
outDir,
|
|
16
|
-
id,
|
|
17
|
-
remoteName,
|
|
18
|
-
exposes,
|
|
19
|
-
aliasMap,
|
|
20
|
-
sharedMap,
|
|
21
|
-
}) {
|
|
22
|
-
const requireFromWorkspace = Module.createRequire(
|
|
23
|
-
path.join(workspaceDir, "package.json")
|
|
24
|
-
);
|
|
25
|
-
const ModuleFederationPlugin = requireFromWorkspace(
|
|
26
|
-
"webpack/lib/container/ModuleFederationPlugin"
|
|
27
|
-
);
|
|
28
|
-
const babelLoader = requireFromWorkspace.resolve("babel-loader");
|
|
29
|
-
const linkerPlugin = requireFromWorkspace.resolve(
|
|
30
|
-
"@angular/compiler-cli/linker/babel"
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
const remoteOutDir = path.join(outDir, id, "remote");
|
|
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
|
-
|
|
48
|
-
return {
|
|
49
|
-
mode: "production",
|
|
50
|
-
entry: syntheticEntry,
|
|
51
|
-
context: workspaceDir,
|
|
52
|
-
// Terser's default class-name mangling strips identifying info
|
|
53
|
-
// Angular's runtime relies on (you get `'_' is neither
|
|
54
|
-
// ComponentType nor DirectiveType` errors). Angular CLI's builder
|
|
55
|
-
// configures Terser with Angular-aware options; we don't, so keep
|
|
56
|
-
// minimization off until that config is ported. Output is larger
|
|
57
|
-
// but functionally correct.
|
|
58
|
-
optimization: {
|
|
59
|
-
minimize: false,
|
|
60
|
-
},
|
|
61
|
-
output: {
|
|
62
|
-
path: remoteOutDir,
|
|
63
|
-
publicPath: "auto",
|
|
64
|
-
uniqueName: remoteName,
|
|
65
|
-
// Content-hash all chunks so the controller can serve them
|
|
66
|
-
// with immutable caching. The MF plugin overrides this to
|
|
67
|
-
// "remoteEntry.js" for the entry only — that one stays at a
|
|
68
|
-
// stable URL and is revalidated each load.
|
|
69
|
-
filename: "[name].[contenthash:10].js",
|
|
70
|
-
chunkFilename: "[name].[contenthash:10].js",
|
|
71
|
-
},
|
|
72
|
-
resolve: {
|
|
73
|
-
alias: aliasMap,
|
|
74
|
-
plugins: resolvePlugins,
|
|
75
|
-
extensions: [".ts", ".js", ".mjs", ".json"],
|
|
76
|
-
// ngcc rewrites Angular packages in-place but leaves the
|
|
77
|
-
// originals next to them, exposing both via package.json fields
|
|
78
|
-
// (`module` for the partial-Ivy original, `module_ivy_ngcc` for
|
|
79
|
-
// the full-Ivy processed copy). Webpack's default mainFields
|
|
80
|
-
// doesn't know about the latter — it resolves to the partial
|
|
81
|
-
// version and components show up as `'MatInput' is neither
|
|
82
|
-
// ComponentType nor DirectiveType` because their `ɵdir` static
|
|
83
|
-
// is missing. Angular CLI's builder injects this same ordering.
|
|
84
|
-
mainFields: ["module_ivy_ngcc", "browser", "module", "main"],
|
|
85
|
-
},
|
|
86
|
-
module: {
|
|
87
|
-
rules: [
|
|
88
|
-
// Run the Angular partial-Ivy linker over every JS module.
|
|
89
|
-
// ng-packagr emits libs in compilationMode "partial" by
|
|
90
|
-
// default since v12; the runtime can't always process
|
|
91
|
-
// partial declarations directly (we hit
|
|
92
|
-
// `meta.deps.map is not a function` in ɵɵngDeclareFactory).
|
|
93
|
-
// Babel-with-linker rewrites the partial declarations to
|
|
94
|
-
// full Ivy at bundle time — same step that Angular CLI's
|
|
95
|
-
// custom-webpack:browser builder does implicitly.
|
|
96
|
-
{
|
|
97
|
-
test: /\.m?js$/,
|
|
98
|
-
loader: babelLoader,
|
|
99
|
-
options: {
|
|
100
|
-
compact: false,
|
|
101
|
-
cacheDirectory: true,
|
|
102
|
-
plugins: [linkerPlugin],
|
|
103
|
-
},
|
|
104
|
-
resolve: { fullySpecified: false },
|
|
105
|
-
},
|
|
106
|
-
],
|
|
107
|
-
},
|
|
108
|
-
plugins: [
|
|
109
|
-
new ModuleFederationPlugin({
|
|
110
|
-
name: remoteName,
|
|
111
|
-
library: { type: "window", name: remoteName },
|
|
112
|
-
filename: "remoteEntry.js",
|
|
113
|
-
exposes: { [exposes]: syntheticEntry },
|
|
114
|
-
shared: sharedMap,
|
|
115
|
-
}),
|
|
116
|
-
],
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
module.exports = { buildWebpackConfig };
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"outDir": "../../out-tsc/lib",
|
|
5
|
-
"target": "es2015",
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"declarationMap": true,
|
|
8
|
-
"inlineSources": true,
|
|
9
|
-
"types": [],
|
|
10
|
-
"lib": ["dom", "es2018"]
|
|
11
|
-
},
|
|
12
|
-
"exclude": ["**/*.spec.ts"]
|
|
13
|
-
}
|