@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
@@ -0,0 +1,249 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { packIntoJar } = require("./pack-into-jar");
4
+ const { loadOrder, partitionByKind } = require("./build-modules");
5
+ const { resolveProjectPom, readArtifactId } = require("./pom");
6
+ const { resolveOne, findMatches } = require("./jar-glob");
7
+
8
+ function readWsconfig(workspaceDir) {
9
+ const file = path.join(workspaceDir, "wsconfig.json");
10
+ if (!fs.existsSync(file)) {
11
+ throw new Error(
12
+ `No wsconfig.json in ${workspaceDir}. Run \`ws-wire-host <wpm-root>\` first.`
13
+ );
14
+ }
15
+ return JSON.parse(fs.readFileSync(file, "utf8"));
16
+ }
17
+
18
+ function readModulesJson(wpmRoot) {
19
+ const file = path.join(wpmRoot, "modules.json");
20
+ if (!fs.existsSync(file)) {
21
+ throw new Error(`No modules.json at ${wpmRoot}.`);
22
+ }
23
+ return JSON.parse(fs.readFileSync(file, "utf8"));
24
+ }
25
+
26
+ function readProjectPkg(workspaceDir, name) {
27
+ const pkgPath = path.join(workspaceDir, "projects", name, "package.json");
28
+ if (!fs.existsSync(pkgPath)) return {};
29
+ try {
30
+ return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
31
+ } catch {
32
+ return {};
33
+ }
34
+ }
35
+
36
+ // Decide jar vs ext for a project, honouring wsconfig override > package.json.
37
+ // Returns { kind: "jar", jarPath } or { kind: "ext", extDir }.
38
+ function resolveTarget({ workspaceDir, projectName, wsconfig, wpmRoot, modulesFolder }) {
39
+ const modulesDir = path.resolve(wpmRoot, modulesFolder);
40
+ const extDir = path.join(
41
+ wpmRoot,
42
+ "scripts",
43
+ "ext-admin-remotes",
44
+ projectName
45
+ );
46
+
47
+ // 1. wsconfig override
48
+ const override =
49
+ wsconfig.deployOverrides && wsconfig.deployOverrides[projectName];
50
+ if (override) {
51
+ if (override.jar && override.ext) {
52
+ throw new Error(
53
+ `wsconfig.deployOverrides.${projectName}: set exactly one of 'jar' or 'ext', not both`
54
+ );
55
+ }
56
+ if (override.ext === true) {
57
+ return { kind: "ext", extDir };
58
+ }
59
+ if (override.jar) {
60
+ const jarPath = resolveOne(modulesDir, override.jar, projectName);
61
+ return { kind: "jar", jarPath };
62
+ }
63
+ }
64
+
65
+ // 2. project package.json
66
+ const pkg = readProjectPkg(workspaceDir, projectName);
67
+ const target = (pkg.wsmodules && pkg.wsmodules.target) || "jar";
68
+ if (target === "ext") {
69
+ return { kind: "ext", extDir };
70
+ }
71
+ if (target !== "jar") {
72
+ throw new Error(
73
+ `Invalid wsmodules.target for ${projectName}: ${JSON.stringify(target)} (expected 'jar' or 'ext')`
74
+ );
75
+ }
76
+
77
+ // 3. pom-derived artifactId
78
+ const pomField = pkg.wsmodules && pkg.wsmodules.pom;
79
+ const pomPath = resolveProjectPom(workspaceDir, projectName, pomField);
80
+ const artifactId = readArtifactId(pomPath);
81
+ const jarPath = resolveOne(modulesDir, `${artifactId}-*.jar`, projectName);
82
+ return { kind: "jar", jarPath, pomPath, artifactId };
83
+ }
84
+
85
+ function copyTypes(distLib, hostFederationDir) {
86
+ if (fs.existsSync(hostFederationDir)) {
87
+ fs.rmSync(hostFederationDir, { recursive: true, force: true });
88
+ }
89
+ fs.mkdirSync(hostFederationDir, { recursive: true });
90
+ fs.cpSync(distLib, hostFederationDir, { recursive: true });
91
+ }
92
+
93
+ // Copy the federation artifact (dist/admin-remotes/<id>/) to the ext target
94
+ // dir (replace-existing semantics). Also warn if a same-id-named jar exists
95
+ // in the modules folder — possible name-collision abuse signal.
96
+ function deployExt({ workspaceDir, id, modulesDir, extDir }) {
97
+ // Collision check: same constrained-glob rules as everywhere else.
98
+ const collisions = findMatches(modulesDir, `${id}-*.jar`);
99
+ if (collisions.length) {
100
+ const list = collisions.map((c) => path.join(modulesDir, c)).join(", ");
101
+ console.warn(
102
+ `\x1b[33mWarning:\x1b[0m a jar matching '${id}-*.jar' exists at ${list}; this project is targeting ext, but the name collision may indicate abuse.`
103
+ );
104
+ }
105
+ const srcDir = path.join(workspaceDir, "dist", "admin-remotes", id);
106
+ if (!fs.existsSync(srcDir)) {
107
+ throw new Error(
108
+ `Federation artifact missing: ${srcDir}. Run \`pack:remote\` first.`
109
+ );
110
+ }
111
+ if (fs.existsSync(extDir)) {
112
+ fs.rmSync(extDir, { recursive: true, force: true });
113
+ }
114
+ fs.mkdirSync(extDir, { recursive: true });
115
+ fs.cpSync(srcDir, extDir, { recursive: true });
116
+ // Count files for the report.
117
+ let fileCount = 0;
118
+ const walk = (d) => {
119
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
120
+ const p = path.join(d, e.name);
121
+ if (e.isDirectory()) walk(p);
122
+ else fileCount += 1;
123
+ }
124
+ };
125
+ walk(extDir);
126
+ return { extDir, copiedFiles: fileCount };
127
+ }
128
+
129
+ function deployOne({
130
+ workspaceDir,
131
+ id,
132
+ wpmRoot,
133
+ modulesFolder,
134
+ adminBuildFolder,
135
+ wsconfig,
136
+ }) {
137
+ const distRemoteDir = path.join(workspaceDir, "dist", "admin-remotes", id);
138
+ const distLibDir = path.join(workspaceDir, "dist", id);
139
+
140
+ if (!fs.existsSync(distRemoteDir)) {
141
+ throw new Error(
142
+ `Federation artifact missing: ${distRemoteDir}. Run \`pack:remote\` first.`
143
+ );
144
+ }
145
+ if (!fs.existsSync(distLibDir)) {
146
+ throw new Error(`Lib dist missing: ${distLibDir}. Run \`build\` first.`);
147
+ }
148
+
149
+ const modulesDir = path.resolve(wpmRoot, modulesFolder);
150
+ const adminDir = path.resolve(wpmRoot, adminBuildFolder);
151
+ const hostFederationDir = path.join(adminDir, ".federation", id);
152
+
153
+ const target = resolveTarget({
154
+ workspaceDir,
155
+ projectName: id,
156
+ wsconfig,
157
+ wpmRoot,
158
+ modulesFolder,
159
+ });
160
+
161
+ // Perform the artifact deploy first; only on success do we touch host types.
162
+ let artifactReport;
163
+ if (target.kind === "jar") {
164
+ const packResult = packIntoJar({
165
+ jarPath: target.jarPath,
166
+ sourceDir: distRemoteDir,
167
+ id,
168
+ });
169
+ artifactReport = {
170
+ kind: "jar",
171
+ jarPath: target.jarPath,
172
+ pomPath: target.pomPath,
173
+ artifactId: target.artifactId,
174
+ ...packResult,
175
+ };
176
+ } else {
177
+ const extResult = deployExt({
178
+ workspaceDir,
179
+ id,
180
+ modulesDir,
181
+ extDir: target.extDir,
182
+ });
183
+ artifactReport = { kind: "ext", ...extResult };
184
+ }
185
+
186
+ // Artifact deploy succeeded — now publish types.
187
+ copyTypes(distLibDir, hostFederationDir);
188
+
189
+ return { ...artifactReport, hostFederationDir };
190
+ }
191
+
192
+ function deployRemotes({ workspaceDir, restrictTo = [], withDeps = false }) {
193
+ const wsconfig = readWsconfig(workspaceDir);
194
+ if (!wsconfig.wpmRoot) {
195
+ throw new Error(
196
+ "wsconfig.json missing wpmRoot. Run `ws-wire-host <wpm-root>`."
197
+ );
198
+ }
199
+ const modulesJson = readModulesJson(wsconfig.wpmRoot);
200
+ const modulesFolder = modulesJson.modulesFolder || "./modules";
201
+ const adminBuildFolder = modulesJson.adminBuildFolder || "./admin";
202
+
203
+ const { order } = loadOrder({ workspaceDir, restrictTo, withDeps });
204
+ const { remotes, libraries } = partitionByKind(workspaceDir, order);
205
+
206
+ console.log(`\n=== Deploy order ===`);
207
+ remotes.forEach((n, i) => console.log(` ${i + 1}. ${n}`));
208
+ if (libraries.length) {
209
+ console.log(
210
+ ` (skipping libraries: ${libraries.join(", ")} — wsmodules.kind=library)`
211
+ );
212
+ }
213
+
214
+ const results = [];
215
+ for (const id of remotes) {
216
+ console.log(`\n=== deploy ${id} ===`);
217
+ const r = deployOne({
218
+ workspaceDir,
219
+ id,
220
+ wpmRoot: wsconfig.wpmRoot,
221
+ modulesFolder,
222
+ adminBuildFolder,
223
+ wsconfig,
224
+ });
225
+ if (r.kind === "jar") {
226
+ console.log(` target: jar`);
227
+ console.log(` jar: ${r.jarPath}`);
228
+ if (r.artifactId) console.log(` artifactId: ${r.artifactId}`);
229
+ if (r.pomPath) console.log(` pom: ${r.pomPath}`);
230
+ if (r.addedFiles !== undefined) {
231
+ console.log(` packed files: ${r.addedFiles}`);
232
+ }
233
+ if (r.droppedEntries) {
234
+ console.log(` dropped stale: ${r.droppedEntries}`);
235
+ }
236
+ } else {
237
+ console.log(` target: ext`);
238
+ console.log(` ext dir: ${r.extDir}`);
239
+ console.log(` copied files: ${r.copiedFiles}`);
240
+ }
241
+ console.log(` host types: ${r.hostFederationDir}`);
242
+ results.push({ id, ...r });
243
+ }
244
+
245
+ console.log(`\nDeployed ${results.length} module(s).`);
246
+ return { results };
247
+ }
248
+
249
+ module.exports = { deployRemotes };
@@ -0,0 +1,71 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { rebuildFederationTsconfig } = require("./wire-host");
4
+
5
+ function dropModule({ workspaceDir, name }) {
6
+ if (!fs.existsSync(path.join(workspaceDir, "angular.json"))) {
7
+ throw new Error(`No angular.json in ${workspaceDir} (not a workspace).`);
8
+ }
9
+
10
+ const projectDir = path.join(workspaceDir, "projects", name);
11
+ const distDir = path.join(workspaceDir, "dist", name);
12
+ const distRemoteDir = path.join(workspaceDir, "dist", "admin-remotes", name);
13
+
14
+ const projectExisted = fs.existsSync(projectDir);
15
+
16
+ if (projectExisted) {
17
+ fs.rmSync(projectDir, { recursive: true, force: true });
18
+ }
19
+
20
+ const angularJsonPath = path.join(workspaceDir, "angular.json");
21
+ const angular = JSON.parse(fs.readFileSync(angularJsonPath, "utf8"));
22
+ let angularChanged = false;
23
+ if (angular.projects && angular.projects[name]) {
24
+ delete angular.projects[name];
25
+ angularChanged = true;
26
+ }
27
+ if (angularChanged) {
28
+ fs.writeFileSync(
29
+ angularJsonPath,
30
+ JSON.stringify(angular, null, "\t") + "\n"
31
+ );
32
+ }
33
+
34
+ const fed = rebuildFederationTsconfig(workspaceDir);
35
+ const federationTsconfigUpdated = !(name in fed.paths);
36
+
37
+ let distCleaned = false;
38
+ if (fs.existsSync(distDir)) {
39
+ fs.rmSync(distDir, { recursive: true, force: true });
40
+ distCleaned = true;
41
+ }
42
+ let distRemoteCleaned = false;
43
+ if (fs.existsSync(distRemoteDir)) {
44
+ fs.rmSync(distRemoteDir, { recursive: true, force: true });
45
+ distRemoteCleaned = true;
46
+ }
47
+
48
+ // Hook point: when type publishing to the host lands, clean the host-side
49
+ // typings for this module here. See TODO.md.
50
+ const hostTypesCleaned = false;
51
+
52
+ if (
53
+ !projectExisted &&
54
+ !angularChanged &&
55
+ !distCleaned &&
56
+ !distRemoteCleaned
57
+ ) {
58
+ throw new Error(`No traces of '${name}' found to drop.`);
59
+ }
60
+
61
+ return {
62
+ projectExisted,
63
+ angularChanged,
64
+ federationTsconfigUpdated,
65
+ distCleaned,
66
+ distRemoteCleaned,
67
+ hostTypesCleaned,
68
+ };
69
+ }
70
+
71
+ module.exports = { dropModule };
@@ -0,0 +1,31 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ // Write <out>/<id>/federation.json — the descriptor the admin federation
5
+ // controller reads (both jar-side and ext-side).
6
+ function emitDescriptor({
7
+ outDir,
8
+ id,
9
+ hostId,
10
+ remoteName,
11
+ exposes,
12
+ displayName,
13
+ }) {
14
+ const dir = path.join(outDir, id);
15
+ fs.mkdirSync(dir, { recursive: true });
16
+ const file = path.join(dir, "federation.json");
17
+ const descriptor = {
18
+ hostId,
19
+ contributionId: id,
20
+ manifestEntry: {
21
+ id,
22
+ name: displayName,
23
+ remoteName,
24
+ exposedModule: exposes,
25
+ },
26
+ };
27
+ fs.writeFileSync(file, JSON.stringify(descriptor, null, 2) + "\n", "utf8");
28
+ return file;
29
+ }
30
+
31
+ module.exports = { emitDescriptor };
@@ -0,0 +1,105 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { rebuildFederationTsconfig } = require("./wire-host");
4
+
5
+ function pascalCase(s) {
6
+ return s
7
+ .split(/[-_]/g)
8
+ .map((p) => (p ? p.charAt(0).toUpperCase() + p.slice(1) : p))
9
+ .join("");
10
+ }
11
+
12
+ function camelCase(s) {
13
+ return s
14
+ .split(/[-_]/g)
15
+ .map((p, i) => {
16
+ if (!p) return p;
17
+ return i === 0 ? p : p.charAt(0).toUpperCase() + p.slice(1);
18
+ })
19
+ .join("");
20
+ }
21
+
22
+ function buildTokens(name) {
23
+ return {
24
+ name,
25
+ className: pascalCase(name),
26
+ camelName: camelCase(name),
27
+ selector: `ws-${name}`,
28
+ };
29
+ }
30
+
31
+ function substitute(s, tokens) {
32
+ return s
33
+ .replace(/__className__/g, tokens.className)
34
+ .replace(/__camelName__/g, tokens.camelName)
35
+ .replace(/__selector__/g, tokens.selector)
36
+ .replace(/__name__/g, tokens.name);
37
+ }
38
+
39
+ function copyAndSubstitute(src, dest, tokens) {
40
+ fs.mkdirSync(dest, { recursive: true });
41
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
42
+ const newName = substitute(entry.name, tokens);
43
+ const srcPath = path.join(src, entry.name);
44
+ const destPath = path.join(dest, newName);
45
+ if (entry.isDirectory()) {
46
+ copyAndSubstitute(srcPath, destPath, tokens);
47
+ } else {
48
+ const content = fs.readFileSync(srcPath, "utf8");
49
+ fs.writeFileSync(destPath, substitute(content, tokens));
50
+ }
51
+ }
52
+ }
53
+
54
+ function registerInAngularJson(workspaceDir, name) {
55
+ const angularJsonPath = path.join(workspaceDir, "angular.json");
56
+ const angular = JSON.parse(fs.readFileSync(angularJsonPath, "utf8"));
57
+ angular.projects = angular.projects || {};
58
+ angular.projects[name] = {
59
+ projectType: "library",
60
+ root: `projects/${name}`,
61
+ sourceRoot: `projects/${name}/src`,
62
+ prefix: "ws",
63
+ architect: {
64
+ build: {
65
+ builder: "@angular-devkit/build-angular:ng-packagr",
66
+ options: { project: `projects/${name}/ng-package.json` },
67
+ configurations: {
68
+ production: {
69
+ tsConfig: `projects/${name}/tsconfig.lib.prod.json`,
70
+ },
71
+ development: { tsConfig: `projects/${name}/tsconfig.lib.json` },
72
+ },
73
+ defaultConfiguration: "production",
74
+ },
75
+ },
76
+ };
77
+ fs.writeFileSync(
78
+ angularJsonPath,
79
+ JSON.stringify(angular, null, "\t") + "\n"
80
+ );
81
+ }
82
+
83
+ function generateModule({ workspaceDir, name, templateModuleDir }) {
84
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
85
+ throw new Error(
86
+ `Invalid module name: ${name}. Use lowercase, hyphen-separated (e.g. db-login).`
87
+ );
88
+ }
89
+ if (!fs.existsSync(path.join(workspaceDir, "angular.json"))) {
90
+ throw new Error(`No angular.json in ${workspaceDir} (not a workspace).`);
91
+ }
92
+ const projectDir = path.join(workspaceDir, "projects", name);
93
+ if (fs.existsSync(projectDir)) {
94
+ throw new Error(`projects/${name}/ already exists.`);
95
+ }
96
+
97
+ const tokens = buildTokens(name);
98
+ copyAndSubstitute(templateModuleDir, projectDir, tokens);
99
+ registerInAngularJson(workspaceDir, name);
100
+ rebuildFederationTsconfig(workspaceDir);
101
+
102
+ return { projectDir, tokens };
103
+ }
104
+
105
+ module.exports = { generateModule, buildTokens };
@@ -0,0 +1,49 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ function camelCase(s) {
5
+ return s
6
+ .split(/[-_]/g)
7
+ .map((part, i) =>
8
+ i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)
9
+ )
10
+ .join("");
11
+ }
12
+
13
+ function loadPackConfig(projectDir, explicitPath) {
14
+ const file = explicitPath
15
+ ? path.resolve(explicitPath)
16
+ : path.join(projectDir, "pack.config.json");
17
+ if (!fs.existsSync(file)) return {};
18
+ try {
19
+ return JSON.parse(fs.readFileSync(file, "utf8"));
20
+ } catch (e) {
21
+ throw new Error(`Failed to parse ${file}: ${e.message}`);
22
+ }
23
+ }
24
+
25
+ function resolveIdentity(args) {
26
+ const projectDir = path.resolve(args.project);
27
+ const dirName = path.basename(projectDir);
28
+ const fromConfig = loadPackConfig(projectDir, args.config);
29
+
30
+ const id = args.id || fromConfig.id || dirName;
31
+ const hostId = args.host || fromConfig.hostId || "admin";
32
+ const remoteName =
33
+ args.remoteName || fromConfig.remoteName || `${camelCase(id)}Module`;
34
+ const exposes = args.exposes || fromConfig.exposes || "./Module";
35
+ const displayName = args.displayName || fromConfig.displayName || id;
36
+ const sharedExtras = fromConfig.sharedExtras || {};
37
+
38
+ return {
39
+ id,
40
+ hostId,
41
+ remoteName,
42
+ exposes,
43
+ displayName,
44
+ sharedExtras,
45
+ projectDir,
46
+ };
47
+ }
48
+
49
+ module.exports = { resolveIdentity, camelCase };
@@ -0,0 +1,55 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ // Constrained glob matcher for jar lookups.
5
+ //
6
+ // Pattern syntax: literal characters + optional `*`. Each `*` expands to the
7
+ // regex character class `[0-9.]+` — i.e. only digits and dots, the legitimate
8
+ // character set of maven version strings. Non-version `*` matches are rejected
9
+ // to prevent name-collision attacks (e.g. `wiresphere-crm-malicious.jar`
10
+ // satisfying a `wiresphere-crm-*.jar` glob).
11
+ //
12
+ // `findMatches(modulesDir, pattern)` returns an alpha-sorted array of matching
13
+ // filenames in `modulesDir`. Throws if modulesDir is missing.
14
+
15
+ const VERSION_CHAR_CLASS = "[0-9.]+";
16
+
17
+ function patternToRegex(pattern) {
18
+ // Escape regex metacharacters, then replace the placeholder `\*` (from
19
+ // escaping the user's literal `*`) with the version char class.
20
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
21
+ const expanded = escaped.replace(/\*/g, VERSION_CHAR_CLASS);
22
+ return new RegExp(`^${expanded}$`);
23
+ }
24
+
25
+ function findMatches(modulesDir, pattern) {
26
+ if (!fs.existsSync(modulesDir)) {
27
+ throw new Error(`Modules dir not found: ${modulesDir}`);
28
+ }
29
+ const re = patternToRegex(pattern);
30
+ return fs
31
+ .readdirSync(modulesDir)
32
+ .filter((f) => re.test(f))
33
+ .sort();
34
+ }
35
+
36
+ // Resolve a single match given a pattern. Errors on no match. Warns + returns
37
+ // first on multiple matches. Returns the absolute jar path.
38
+ function resolveOne(modulesDir, pattern, label) {
39
+ const matches = findMatches(modulesDir, pattern);
40
+ if (!matches.length) {
41
+ throw new Error(
42
+ `No jar matching '${pattern}' in ${modulesDir}` +
43
+ (label ? ` (for ${label})` : "")
44
+ );
45
+ }
46
+ if (matches.length > 1) {
47
+ console.warn(
48
+ `Multiple jars match '${pattern}' in ${modulesDir}; using ${matches[0]}` +
49
+ (label ? ` (for ${label})` : "")
50
+ );
51
+ }
52
+ return path.join(modulesDir, matches[0]);
53
+ }
54
+
55
+ module.exports = { findMatches, resolveOne, patternToRegex };
@@ -0,0 +1,74 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const AdmZip = require("adm-zip");
4
+
5
+ function walk(dir, base = dir) {
6
+ const out = [];
7
+ for (const name of fs.readdirSync(dir)) {
8
+ const full = path.join(dir, name);
9
+ const stat = fs.statSync(full);
10
+ if (stat.isDirectory()) {
11
+ out.push(...walk(full, base));
12
+ } else if (stat.isFile()) {
13
+ out.push({
14
+ abs: full,
15
+ rel: path.relative(base, full).split(path.sep).join("/"),
16
+ });
17
+ }
18
+ }
19
+ return out;
20
+ }
21
+
22
+ // Repack a jar to carry a federated remote contribution under
23
+ // META-INF/federation/<id>/. Drops any pre-existing entries under that
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 (which already contains both
27
+ // federation.json and remote/* per ws-pack-remote's emit).
28
+ function packIntoJar({ jarPath, sourceDir, id }) {
29
+ if (!fs.existsSync(jarPath)) {
30
+ throw new Error(`Jar not found: ${jarPath}`);
31
+ }
32
+ if (!fs.existsSync(sourceDir) || !fs.statSync(sourceDir).isDirectory()) {
33
+ throw new Error(`Source dir not found or not a directory: ${sourceDir}`);
34
+ }
35
+
36
+ const contribRoot = `META-INF/federation/${id}/`;
37
+ const original = new AdmZip(jarPath);
38
+ const out = new AdmZip();
39
+
40
+ let dropped = 0;
41
+ for (const entry of original.getEntries()) {
42
+ if (entry.entryName.startsWith(contribRoot)) {
43
+ dropped++;
44
+ continue;
45
+ }
46
+ if (entry.isDirectory) {
47
+ out.addFile(entry.entryName, Buffer.alloc(0), "", entry.attr);
48
+ } else {
49
+ out.addFile(
50
+ entry.entryName,
51
+ entry.getData(),
52
+ entry.comment,
53
+ entry.attr
54
+ );
55
+ }
56
+ }
57
+
58
+ const files = walk(sourceDir);
59
+ for (const f of files) {
60
+ out.addFile(contribRoot + f.rel, fs.readFileSync(f.abs));
61
+ }
62
+
63
+ const dir = path.dirname(jarPath);
64
+ const tmp = path.join(
65
+ dir,
66
+ `.${path.basename(jarPath)}.repack.${process.pid}.tmp`
67
+ );
68
+ out.writeZip(tmp);
69
+ fs.renameSync(tmp, jarPath);
70
+
71
+ return { jarPath, droppedEntries: dropped, addedFiles: files.length };
72
+ }
73
+
74
+ module.exports = { packIntoJar };