@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.
Files changed (46) hide show
  1. package/README.md +89 -0
  2. package/bin/ws-drop-module.js +40 -0
  3. package/bin/ws-generate-module.js +37 -0
  4. package/bin/ws-init-workspace.js +190 -0
  5. package/bin/ws-modules.js +179 -0
  6. package/bin/ws-pack-remote.js +196 -0
  7. package/bin/ws-sync-paths.js +35 -0
  8. package/bin/ws-wire-host.js +67 -0
  9. package/bin/ws-wire-pom.js +132 -0
  10. package/lib/alias-discovery.js +40 -0
  11. package/lib/build-modules.js +84 -0
  12. package/lib/cli-args.js +55 -0
  13. package/lib/deploy-remotes.js +249 -0
  14. package/lib/drop-module.js +71 -0
  15. package/lib/emit-descriptor.js +31 -0
  16. package/lib/generate-module.js +105 -0
  17. package/lib/identity.js +49 -0
  18. package/lib/jar-glob.js +55 -0
  19. package/lib/pack-into-jar.js +74 -0
  20. package/lib/pom.js +84 -0
  21. package/lib/shared-deps.js +90 -0
  22. package/lib/synthetic-entry.js +27 -0
  23. package/lib/template-copy.js +136 -0
  24. package/lib/topo-sort.js +134 -0
  25. package/lib/webpack-factory.js +104 -0
  26. package/lib/wire-host.js +96 -0
  27. package/package.json +33 -0
  28. package/template/angular.json +14 -0
  29. package/template/package.json +26 -0
  30. package/template/tsconfig.json +7 -0
  31. package/template/wsconfig.json.example +4 -0
  32. package/template-module/assets/i18n/de.json +1 -0
  33. package/template-module/assets/i18n/en.json +1 -0
  34. package/template-module/assets/i18n/fr.json +1 -0
  35. package/template-module/assets/i18n/general/de.json +1 -0
  36. package/template-module/assets/i18n/general/en.json +1 -0
  37. package/template-module/assets/i18n/general/fr.json +1 -0
  38. package/template-module/ng-package.json +8 -0
  39. package/template-module/package.json +14 -0
  40. package/template-module/src/lib/__name__.module.ts +11 -0
  41. package/template-module/src/lib/components/__name__/__name__.component.html +1 -0
  42. package/template-module/src/lib/components/__name__/__name__.component.scss +0 -0
  43. package/template-module/src/lib/components/__name__/__name__.component.ts +8 -0
  44. package/template-module/src/public-api.ts +6 -0
  45. package/template-module/tsconfig.lib.json +13 -0
  46. 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 };
@@ -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 };
@@ -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
+ };