@ws-test-realm/admin-kit 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -0
- package/bin/ws-drop-module.js +40 -0
- package/bin/ws-generate-module.js +37 -0
- package/bin/ws-init-workspace.js +190 -0
- package/bin/ws-modules.js +179 -0
- package/bin/ws-pack-remote.js +196 -0
- package/bin/ws-sync-paths.js +35 -0
- package/bin/ws-wire-host.js +67 -0
- package/bin/ws-wire-pom.js +132 -0
- package/lib/alias-discovery.js +40 -0
- package/lib/build-modules.js +84 -0
- package/lib/cli-args.js +55 -0
- package/lib/deploy-remotes.js +249 -0
- package/lib/drop-module.js +71 -0
- package/lib/emit-descriptor.js +31 -0
- package/lib/generate-module.js +105 -0
- package/lib/identity.js +49 -0
- package/lib/jar-glob.js +55 -0
- package/lib/pack-into-jar.js +74 -0
- package/lib/pom.js +84 -0
- package/lib/shared-deps.js +90 -0
- package/lib/synthetic-entry.js +27 -0
- package/lib/template-copy.js +136 -0
- package/lib/topo-sort.js +134 -0
- package/lib/webpack-factory.js +104 -0
- package/lib/wire-host.js +96 -0
- package/package.json +33 -0
- package/template/angular.json +14 -0
- package/template/package.json +26 -0
- package/template/tsconfig.json +7 -0
- package/template/wsconfig.json.example +4 -0
- package/template-module/assets/i18n/de.json +1 -0
- package/template-module/assets/i18n/en.json +1 -0
- package/template-module/assets/i18n/fr.json +1 -0
- package/template-module/assets/i18n/general/de.json +1 -0
- package/template-module/assets/i18n/general/en.json +1 -0
- package/template-module/assets/i18n/general/fr.json +1 -0
- package/template-module/ng-package.json +8 -0
- package/template-module/package.json +14 -0
- package/template-module/src/lib/__name__.module.ts +11 -0
- package/template-module/src/lib/components/__name__/__name__.component.html +1 -0
- package/template-module/src/lib/components/__name__/__name__.component.scss +0 -0
- package/template-module/src/lib/components/__name__/__name__.component.ts +8 -0
- package/template-module/src/public-api.ts +6 -0
- package/template-module/tsconfig.lib.json +13 -0
- package/template-module/tsconfig.lib.prod.json +9 -0
package/lib/pom.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
// Pom parsing + module-scope path helpers.
|
|
5
|
+
//
|
|
6
|
+
// Scope rule: for a workspace at <workspace>, the module scope root is
|
|
7
|
+
// path.dirname(workspace). All resolved pom paths must stay within that
|
|
8
|
+
// directory. Validation runs at both wire time (ws-wire-pom) and deploy time.
|
|
9
|
+
|
|
10
|
+
function getScopeRoot(workspaceDir) {
|
|
11
|
+
return path.dirname(path.resolve(workspaceDir));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isWithinScope(workspaceDir, absPath) {
|
|
15
|
+
const scope = getScopeRoot(workspaceDir);
|
|
16
|
+
const rel = path.relative(scope, absPath);
|
|
17
|
+
return rel.length > 0 && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validatePomPath(workspaceDir, absPomPath) {
|
|
21
|
+
if (!fs.existsSync(absPomPath)) {
|
|
22
|
+
throw new Error(`Pom not found: ${absPomPath}`);
|
|
23
|
+
}
|
|
24
|
+
if (!fs.statSync(absPomPath).isFile()) {
|
|
25
|
+
throw new Error(`Pom path is not a file: ${absPomPath}`);
|
|
26
|
+
}
|
|
27
|
+
if (!isWithinScope(workspaceDir, absPomPath)) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Pom path is outside module scope (${getScopeRoot(workspaceDir)}): ${absPomPath}`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Resolve the absolute pom path for a project. If `pomField` is set in the
|
|
35
|
+
// project's package.json (`wsmodules.pom`), it's a path relative to the
|
|
36
|
+
// project dir — resolve + validate. Otherwise fall back to the scope-root
|
|
37
|
+
// pom.xml. Throws if neither resolves to an existing pom.
|
|
38
|
+
function resolveProjectPom(workspaceDir, projectName, pomField) {
|
|
39
|
+
if (pomField) {
|
|
40
|
+
const projectDir = path.join(workspaceDir, "projects", projectName);
|
|
41
|
+
const abs = path.resolve(projectDir, pomField);
|
|
42
|
+
validatePomPath(workspaceDir, abs);
|
|
43
|
+
return abs;
|
|
44
|
+
}
|
|
45
|
+
const fallback = path.join(getScopeRoot(workspaceDir), "pom.xml");
|
|
46
|
+
if (!fs.existsSync(fallback)) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`No pom found for ${projectName}: set wsmodules.pom in projects/${projectName}/package.json, place pom.xml at ${getScopeRoot(workspaceDir)}, or set wsmodules.target: "ext" to bypass jar deploy.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return fallback;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Read the project's own artifactId from a pom. fast-xml-parser yields a tree
|
|
55
|
+
// where parent.artifactId is nested under `project.parent` and
|
|
56
|
+
// dependency/plugin artifactIds are under their respective sections — so
|
|
57
|
+
// `result.project.artifactId` is the project's own.
|
|
58
|
+
function readArtifactId(absPomPath) {
|
|
59
|
+
const { XMLParser } = require("fast-xml-parser");
|
|
60
|
+
const xml = fs.readFileSync(absPomPath, "utf8");
|
|
61
|
+
const parser = new XMLParser({ ignoreAttributes: true });
|
|
62
|
+
const tree = parser.parse(xml);
|
|
63
|
+
const artifactId = tree && tree.project && tree.project.artifactId;
|
|
64
|
+
if (!artifactId || typeof artifactId !== "string") {
|
|
65
|
+
throw new Error(`Pom missing top-level <artifactId>: ${absPomPath}`);
|
|
66
|
+
}
|
|
67
|
+
return artifactId.trim();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Compute the relative path to store in wsmodules.pom — relative from the
|
|
71
|
+
// project dir, so it travels with the project on rename/move.
|
|
72
|
+
function relativeFromProject(workspaceDir, projectName, absPomPath) {
|
|
73
|
+
const projectDir = path.join(workspaceDir, "projects", projectName);
|
|
74
|
+
return path.relative(projectDir, absPomPath);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
getScopeRoot,
|
|
79
|
+
isWithinScope,
|
|
80
|
+
validatePomPath,
|
|
81
|
+
resolveProjectPom,
|
|
82
|
+
readArtifactId,
|
|
83
|
+
relativeFromProject,
|
|
84
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
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 };
|
|
@@ -0,0 +1,27 @@
|
|
|
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 };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
// Copy non-package.json template files into destDir, skipping any path that
|
|
5
|
+
// already exists. package.json gets a structured merge instead (see
|
|
6
|
+
// mergePackageJson). Returns { copied: [...], skipped: [...] }.
|
|
7
|
+
function copyTemplate(templateDir, destDir) {
|
|
8
|
+
if (!fs.existsSync(templateDir)) {
|
|
9
|
+
throw new Error(`Template not found: ${templateDir}`);
|
|
10
|
+
}
|
|
11
|
+
const copied = [];
|
|
12
|
+
const skipped = [];
|
|
13
|
+
for (const entry of fs.readdirSync(templateDir, { withFileTypes: true })) {
|
|
14
|
+
if (entry.name === "package.json") continue;
|
|
15
|
+
const src = path.join(templateDir, entry.name);
|
|
16
|
+
const dst = path.join(destDir, entry.name);
|
|
17
|
+
if (fs.existsSync(dst)) {
|
|
18
|
+
skipped.push(entry.name);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
fs.cpSync(src, dst, { recursive: true });
|
|
22
|
+
copied.push(entry.name);
|
|
23
|
+
}
|
|
24
|
+
return { copied, skipped };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Merge template package.json fields into existing package.json. On conflict
|
|
28
|
+
// (key already present with a different shape/value), refuse and collect into
|
|
29
|
+
// `conflicts`. Returns { conflicts: [{path, existing, incoming}], merged }
|
|
30
|
+
// where `merged` is the resulting object (only valid if conflicts is empty).
|
|
31
|
+
//
|
|
32
|
+
// Merge rules per field:
|
|
33
|
+
// scripts object: per-key add; conflict on different value
|
|
34
|
+
// dependencies object: per-key add; conflict on different value
|
|
35
|
+
// devDependencies same
|
|
36
|
+
// peerDependencies same
|
|
37
|
+
// workspaces array OR object: must equal incoming; conflict otherwise
|
|
38
|
+
// husky object deep-merge; conflict on differing leaf values
|
|
39
|
+
// prettier string or object: must equal incoming; conflict otherwise
|
|
40
|
+
// private scalar: if missing or matches, set; conflict otherwise
|
|
41
|
+
// <anything else> leave existing untouched (don't try to be clever)
|
|
42
|
+
function mergePackageJson(existingPath, templatePath) {
|
|
43
|
+
const existing = JSON.parse(fs.readFileSync(existingPath, "utf8"));
|
|
44
|
+
const incoming = JSON.parse(fs.readFileSync(templatePath, "utf8"));
|
|
45
|
+
const conflicts = [];
|
|
46
|
+
const merged = { ...existing };
|
|
47
|
+
|
|
48
|
+
const mergeMap = (key) => {
|
|
49
|
+
if (!incoming[key]) return;
|
|
50
|
+
merged[key] = merged[key] || {};
|
|
51
|
+
for (const [k, v] of Object.entries(incoming[key])) {
|
|
52
|
+
if (k in merged[key]) {
|
|
53
|
+
if (JSON.stringify(merged[key][k]) !== JSON.stringify(v)) {
|
|
54
|
+
conflicts.push({
|
|
55
|
+
path: `${key}.${k}`,
|
|
56
|
+
existing: merged[key][k],
|
|
57
|
+
incoming: v,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
merged[key][k] = v;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
mergeMap("scripts");
|
|
67
|
+
mergeMap("dependencies");
|
|
68
|
+
mergeMap("devDependencies");
|
|
69
|
+
mergeMap("peerDependencies");
|
|
70
|
+
|
|
71
|
+
// husky deep-merge (object of objects)
|
|
72
|
+
if (incoming.husky) {
|
|
73
|
+
const eh = merged.husky || {};
|
|
74
|
+
const ih = incoming.husky;
|
|
75
|
+
const mh = { ...eh };
|
|
76
|
+
if (ih.hooks) {
|
|
77
|
+
mh.hooks = { ...(eh.hooks || {}) };
|
|
78
|
+
for (const [k, v] of Object.entries(ih.hooks)) {
|
|
79
|
+
if (k in mh.hooks && mh.hooks[k] !== v) {
|
|
80
|
+
conflicts.push({
|
|
81
|
+
path: `husky.hooks.${k}`,
|
|
82
|
+
existing: mh.hooks[k],
|
|
83
|
+
incoming: v,
|
|
84
|
+
});
|
|
85
|
+
} else {
|
|
86
|
+
mh.hooks[k] = v;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
merged.husky = mh;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const scalar of ["prettier", "private"]) {
|
|
94
|
+
if (!(scalar in incoming)) continue;
|
|
95
|
+
if (scalar in existing) {
|
|
96
|
+
if (JSON.stringify(existing[scalar]) !== JSON.stringify(incoming[scalar])) {
|
|
97
|
+
conflicts.push({
|
|
98
|
+
path: scalar,
|
|
99
|
+
existing: existing[scalar],
|
|
100
|
+
incoming: incoming[scalar],
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
merged[scalar] = incoming[scalar];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// workspaces: equality check (don't try to merge arrays meaningfully)
|
|
109
|
+
if (incoming.workspaces) {
|
|
110
|
+
if ("workspaces" in existing) {
|
|
111
|
+
if (
|
|
112
|
+
JSON.stringify(existing.workspaces) !==
|
|
113
|
+
JSON.stringify(incoming.workspaces)
|
|
114
|
+
) {
|
|
115
|
+
conflicts.push({
|
|
116
|
+
path: "workspaces",
|
|
117
|
+
existing: existing.workspaces,
|
|
118
|
+
incoming: incoming.workspaces,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
merged.workspaces = incoming.workspaces;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { conflicts, merged };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function writePackageJson(destPath, obj) {
|
|
130
|
+
// Match what npm writes: 2-space indent + trailing newline. Conservative;
|
|
131
|
+
// users with tabs/4-space style will see a one-time reformat but it's stable
|
|
132
|
+
// after the first prettier run.
|
|
133
|
+
fs.writeFileSync(destPath, JSON.stringify(obj, null, 2) + "\n");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = { copyTemplate, mergePackageJson, writePackageJson };
|
package/lib/topo-sort.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
function getProjectDependencies(workspaceDir, projectName, allProjects, angularJson) {
|
|
5
|
+
const projectConfig = angularJson.projects[projectName];
|
|
6
|
+
if (!projectConfig) return [];
|
|
7
|
+
const projectRoot = projectConfig.root || `projects/${projectName}`;
|
|
8
|
+
const deps = new Set();
|
|
9
|
+
|
|
10
|
+
const considerName = (s) => {
|
|
11
|
+
if (!s) return;
|
|
12
|
+
for (const p of allProjects) {
|
|
13
|
+
if (p === projectName) continue;
|
|
14
|
+
if (s === p || s.startsWith(p + "/")) deps.add(p);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const tsConfigPath = projectConfig?.architect?.build?.options?.tsConfig;
|
|
19
|
+
if (tsConfigPath) {
|
|
20
|
+
const abs = path.join(workspaceDir, tsConfigPath);
|
|
21
|
+
if (fs.existsSync(abs)) {
|
|
22
|
+
try {
|
|
23
|
+
const tsc = JSON.parse(fs.readFileSync(abs, "utf8"));
|
|
24
|
+
const paths = tsc.compilerOptions?.paths || {};
|
|
25
|
+
for (const k of Object.keys(paths)) considerName(k);
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const pkgPath = path.join(workspaceDir, projectRoot, "package.json");
|
|
31
|
+
if (fs.existsSync(pkgPath)) {
|
|
32
|
+
try {
|
|
33
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
34
|
+
for (const section of [
|
|
35
|
+
"dependencies",
|
|
36
|
+
"devDependencies",
|
|
37
|
+
"peerDependencies",
|
|
38
|
+
]) {
|
|
39
|
+
for (const k of Object.keys(pkg[section] || {})) considerName(k);
|
|
40
|
+
}
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const srcRoot = path.join(workspaceDir, projectRoot, "src");
|
|
45
|
+
if (fs.existsSync(srcRoot)) {
|
|
46
|
+
const importRe = /import\s.*from\s['"]([^'"]+)['"]/g;
|
|
47
|
+
const walk = (dir) => {
|
|
48
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
49
|
+
const full = path.join(dir, entry.name);
|
|
50
|
+
if (entry.isDirectory()) {
|
|
51
|
+
walk(full);
|
|
52
|
+
} else if (entry.name.endsWith(".ts")) {
|
|
53
|
+
try {
|
|
54
|
+
const content = fs.readFileSync(full, "utf8");
|
|
55
|
+
let m;
|
|
56
|
+
while ((m = importRe.exec(content))) considerName(m[1]);
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
walk(srcRoot);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return Array.from(deps);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function topologicalSort(graph) {
|
|
68
|
+
const sorted = [];
|
|
69
|
+
const visited = {};
|
|
70
|
+
const inStack = {};
|
|
71
|
+
|
|
72
|
+
const visit = (node, stack = []) => {
|
|
73
|
+
if (visited[node]) return;
|
|
74
|
+
if (inStack[node]) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Circular dependency detected: ${[...stack, node].join(" → ")}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
inStack[node] = true;
|
|
80
|
+
for (const dep of graph[node] || []) {
|
|
81
|
+
visit(dep, [...stack, node]);
|
|
82
|
+
}
|
|
83
|
+
inStack[node] = false;
|
|
84
|
+
visited[node] = true;
|
|
85
|
+
sorted.push(node);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
for (const node of Object.keys(graph)) visit(node);
|
|
89
|
+
return sorted;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// restrictTo: array of project names. Empty/undefined ⇒ keep full graph.
|
|
93
|
+
// opts.withDeps: when true and restrictTo is non-empty, expand the set to
|
|
94
|
+
// include every transitive workspace dep (legacy behavior). When false
|
|
95
|
+
// (default), the set is exactly the names in restrictTo; the returned graph
|
|
96
|
+
// keeps only edges within that set so the sort respects ordering among them.
|
|
97
|
+
function buildDependencyGraph(workspaceDir, angularJson, restrictTo, opts = {}) {
|
|
98
|
+
const { withDeps = false } = opts;
|
|
99
|
+
const allProjects = Object.keys(angularJson.projects || {});
|
|
100
|
+
const graph = {};
|
|
101
|
+
for (const name of allProjects) {
|
|
102
|
+
graph[name] = getProjectDependencies(
|
|
103
|
+
workspaceDir,
|
|
104
|
+
name,
|
|
105
|
+
allProjects,
|
|
106
|
+
angularJson
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (!restrictTo || !restrictTo.length) return { graph, allProjects };
|
|
110
|
+
|
|
111
|
+
for (const n of restrictTo) {
|
|
112
|
+
if (!graph[n]) {
|
|
113
|
+
throw new Error(`Unknown project: ${n}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const allowed = new Set();
|
|
118
|
+
if (withDeps) {
|
|
119
|
+
const collect = (n) => {
|
|
120
|
+
if (allowed.has(n)) return;
|
|
121
|
+
allowed.add(n);
|
|
122
|
+
for (const d of graph[n] || []) collect(d);
|
|
123
|
+
};
|
|
124
|
+
for (const n of restrictTo) collect(n);
|
|
125
|
+
} else {
|
|
126
|
+
for (const n of restrictTo) allowed.add(n);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const restrictedGraph = {};
|
|
130
|
+
for (const n of allowed) restrictedGraph[n] = graph[n].filter((d) => allowed.has(d));
|
|
131
|
+
return { graph: restrictedGraph, allProjects: Array.from(allowed) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { getProjectDependencies, topologicalSort, buildDependencyGraph };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const Module = require("module");
|
|
3
|
+
|
|
4
|
+
// Build the in-memory webpack config used to produce a federated remote
|
|
5
|
+
// from a single synthetic entry that re-exports the lib's public API.
|
|
6
|
+
//
|
|
7
|
+
// We resolve the workspace's webpack + ModuleFederationPlugin so the
|
|
8
|
+
// build runs against the same versions used by the workspace's own
|
|
9
|
+
// dependency graph (avoids shared-scope ABI surprises).
|
|
10
|
+
function buildWebpackConfig({
|
|
11
|
+
workspaceDir,
|
|
12
|
+
syntheticEntry,
|
|
13
|
+
outDir,
|
|
14
|
+
id,
|
|
15
|
+
remoteName,
|
|
16
|
+
exposes,
|
|
17
|
+
aliasMap,
|
|
18
|
+
sharedMap,
|
|
19
|
+
}) {
|
|
20
|
+
const requireFromWorkspace = Module.createRequire(
|
|
21
|
+
path.join(workspaceDir, "package.json")
|
|
22
|
+
);
|
|
23
|
+
const ModuleFederationPlugin = requireFromWorkspace(
|
|
24
|
+
"webpack/lib/container/ModuleFederationPlugin"
|
|
25
|
+
);
|
|
26
|
+
const babelLoader = requireFromWorkspace.resolve("babel-loader");
|
|
27
|
+
const linkerPlugin = requireFromWorkspace.resolve(
|
|
28
|
+
"@angular/compiler-cli/linker/babel"
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const remoteOutDir = path.join(outDir, id, "remote");
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
mode: "production",
|
|
35
|
+
entry: syntheticEntry,
|
|
36
|
+
context: workspaceDir,
|
|
37
|
+
// Terser's default class-name mangling strips identifying info
|
|
38
|
+
// Angular's runtime relies on (you get `'_' is neither
|
|
39
|
+
// ComponentType nor DirectiveType` errors). Angular CLI's builder
|
|
40
|
+
// configures Terser with Angular-aware options; we don't, so keep
|
|
41
|
+
// minimization off until that config is ported. Output is larger
|
|
42
|
+
// but functionally correct.
|
|
43
|
+
optimization: {
|
|
44
|
+
minimize: false,
|
|
45
|
+
},
|
|
46
|
+
output: {
|
|
47
|
+
path: remoteOutDir,
|
|
48
|
+
publicPath: "auto",
|
|
49
|
+
uniqueName: remoteName,
|
|
50
|
+
// Content-hash all chunks so the controller can serve them
|
|
51
|
+
// with immutable caching. The MF plugin overrides this to
|
|
52
|
+
// "remoteEntry.js" for the entry only — that one stays at a
|
|
53
|
+
// stable URL and is revalidated each load.
|
|
54
|
+
filename: "[name].[contenthash:10].js",
|
|
55
|
+
chunkFilename: "[name].[contenthash:10].js",
|
|
56
|
+
},
|
|
57
|
+
resolve: {
|
|
58
|
+
alias: aliasMap,
|
|
59
|
+
extensions: [".ts", ".js", ".mjs", ".json"],
|
|
60
|
+
// ngcc rewrites Angular packages in-place but leaves the
|
|
61
|
+
// originals next to them, exposing both via package.json fields
|
|
62
|
+
// (`module` for the partial-Ivy original, `module_ivy_ngcc` for
|
|
63
|
+
// the full-Ivy processed copy). Webpack's default mainFields
|
|
64
|
+
// doesn't know about the latter — it resolves to the partial
|
|
65
|
+
// version and components show up as `'MatInput' is neither
|
|
66
|
+
// ComponentType nor DirectiveType` because their `ɵdir` static
|
|
67
|
+
// is missing. Angular CLI's builder injects this same ordering.
|
|
68
|
+
mainFields: ["module_ivy_ngcc", "browser", "module", "main"],
|
|
69
|
+
},
|
|
70
|
+
module: {
|
|
71
|
+
rules: [
|
|
72
|
+
// Run the Angular partial-Ivy linker over every JS module.
|
|
73
|
+
// ng-packagr emits libs in compilationMode "partial" by
|
|
74
|
+
// default since v12; the runtime can't always process
|
|
75
|
+
// partial declarations directly (we hit
|
|
76
|
+
// `meta.deps.map is not a function` in ɵɵngDeclareFactory).
|
|
77
|
+
// Babel-with-linker rewrites the partial declarations to
|
|
78
|
+
// full Ivy at bundle time — same step that Angular CLI's
|
|
79
|
+
// custom-webpack:browser builder does implicitly.
|
|
80
|
+
{
|
|
81
|
+
test: /\.m?js$/,
|
|
82
|
+
loader: babelLoader,
|
|
83
|
+
options: {
|
|
84
|
+
compact: false,
|
|
85
|
+
cacheDirectory: true,
|
|
86
|
+
plugins: [linkerPlugin],
|
|
87
|
+
},
|
|
88
|
+
resolve: { fullySpecified: false },
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
plugins: [
|
|
93
|
+
new ModuleFederationPlugin({
|
|
94
|
+
name: remoteName,
|
|
95
|
+
library: { type: "window", name: remoteName },
|
|
96
|
+
filename: "remoteEntry.js",
|
|
97
|
+
exposes: { [exposes]: syntheticEntry },
|
|
98
|
+
shared: sharedMap,
|
|
99
|
+
}),
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { buildWebpackConfig };
|
package/lib/wire-host.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
function wsconfigPath(workspaceDir) {
|
|
5
|
+
return path.join(workspaceDir, "wsconfig.json");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function readWsconfig(workspaceDir) {
|
|
9
|
+
const file = wsconfigPath(workspaceDir);
|
|
10
|
+
if (!fs.existsSync(file)) return {};
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
13
|
+
} catch (e) {
|
|
14
|
+
throw new Error(`Failed to parse ${file}: ${e.message}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeWsconfig(workspaceDir, config) {
|
|
19
|
+
fs.writeFileSync(
|
|
20
|
+
wsconfigPath(workspaceDir),
|
|
21
|
+
JSON.stringify(config, null, 2) + "\n"
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readModulesJson(wpmRoot) {
|
|
26
|
+
const file = path.join(wpmRoot, "modules.json");
|
|
27
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveHostDir(wpmRoot) {
|
|
31
|
+
const modulesJson = readModulesJson(wpmRoot);
|
|
32
|
+
const adminBuildFolder = modulesJson.adminBuildFolder || "./admin";
|
|
33
|
+
return path.resolve(wpmRoot, adminBuildFolder);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function collectSiblingPaths(workspaceDir) {
|
|
37
|
+
const angularJsonPath = path.join(workspaceDir, "angular.json");
|
|
38
|
+
if (!fs.existsSync(angularJsonPath)) return {};
|
|
39
|
+
const angular = JSON.parse(fs.readFileSync(angularJsonPath, "utf8"));
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const name of Object.keys(angular.projects || {})) {
|
|
42
|
+
out[name] = [`dist/${name}/${name}`, `dist/${name}`];
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function rebuildFederationTsconfig(workspaceDir) {
|
|
48
|
+
const paths = collectSiblingPaths(workspaceDir);
|
|
49
|
+
const wsconfig = readWsconfig(workspaceDir);
|
|
50
|
+
let federationTarget = null;
|
|
51
|
+
if (wsconfig.wpmRoot) {
|
|
52
|
+
const hostDir = resolveHostDir(wsconfig.wpmRoot);
|
|
53
|
+
federationTarget = path.join(hostDir, ".federation", "*");
|
|
54
|
+
paths["@ws-remote/*"] = [federationTarget];
|
|
55
|
+
}
|
|
56
|
+
// tsconfig.federation.json is the leaf of the extends chain. Its single
|
|
57
|
+
// responsibility now is to combine the workspace-local `paths` with the
|
|
58
|
+
// canonical compiler options from @ws-test-realm/devkit/tsconfig.base.json.
|
|
59
|
+
// Workspace tsconfig.json sits on top and extends THIS file.
|
|
60
|
+
const config = {
|
|
61
|
+
extends: "@ws-test-realm/devkit/tsconfig.base.json",
|
|
62
|
+
compilerOptions: { paths },
|
|
63
|
+
};
|
|
64
|
+
const file = path.join(workspaceDir, "tsconfig.federation.json");
|
|
65
|
+
fs.writeFileSync(file, JSON.stringify(config, null, "\t") + "\n");
|
|
66
|
+
return { file, paths, federationTarget };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function applyWpmRoot(workspaceDir, wpmRoot, hostId) {
|
|
70
|
+
if (!wpmRoot) {
|
|
71
|
+
throw new Error("wpmRoot is required");
|
|
72
|
+
}
|
|
73
|
+
const abs = path.resolve(wpmRoot);
|
|
74
|
+
if (!fs.existsSync(path.join(abs, "modules.json"))) {
|
|
75
|
+
throw new Error(`Not a wpm project root (no modules.json): ${abs}`);
|
|
76
|
+
}
|
|
77
|
+
const config = readWsconfig(workspaceDir);
|
|
78
|
+
config.wpmRoot = abs;
|
|
79
|
+
config.hostId = hostId || "admin";
|
|
80
|
+
writeWsconfig(workspaceDir, config);
|
|
81
|
+
|
|
82
|
+
const fed = rebuildFederationTsconfig(workspaceDir);
|
|
83
|
+
return {
|
|
84
|
+
wpmRoot: abs,
|
|
85
|
+
hostId: config.hostId,
|
|
86
|
+
federationTsconfig: fed.file,
|
|
87
|
+
federationTarget: fed.federationTarget,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
readWsconfig,
|
|
93
|
+
writeWsconfig,
|
|
94
|
+
applyWpmRoot,
|
|
95
|
+
rebuildFederationTsconfig,
|
|
96
|
+
};
|