@ws-test-realm/admin-kit 0.6.1-ng20 → 0.6.3-ng20

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.
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // ws-generate-module — scaffold a federated admin module project under
3
- // projects/<name>/ in the current workspace. Updates angular.json and
4
- // tsconfig.json paths.
3
+ // projects/<name>/ in the current workspace. Delegates the Angular-blessed
4
+ // scaffolding to `ng generate library`, then overlays the federation files
5
+ // + BOM-only peerDep shape + workspace registration + npm install. After
6
+ // this runs, the dev can immediately `npm run wire`.
5
7
  //
6
8
  // Usage:
7
9
  // ws-generate-module <name>
@@ -18,16 +20,11 @@ function main() {
18
20
  }
19
21
 
20
22
  const workspaceDir = process.cwd();
21
- const templateModuleDir = path.join(__dirname, "..", "template-module");
22
23
 
23
24
  try {
24
- const { projectDir } = generateModule({
25
- workspaceDir,
26
- name,
27
- templateModuleDir,
28
- });
25
+ const { projectDir } = generateModule({ workspaceDir, name });
29
26
  console.log(`Generated ${path.relative(workspaceDir, projectDir)}/`);
30
- console.log(`Updated angular.json + tsconfig.json with the new project.`);
27
+ console.log(`Module ready run \`npm run wire\` to build and deploy.`);
31
28
  } catch (e) {
32
29
  console.error(e.message);
33
30
  process.exit(1);
@@ -0,0 +1,138 @@
1
+ // Federation-shaped JSON artifacts that ws-generate-module stamps over
2
+ // the output of `ng generate library`. Hardcoded here (rather than in
3
+ // template-module/) because the @ws-test-realm/shared peer pin is derived
4
+ // from admin-kit's own dependency, and the JSON shapes are small enough
5
+ // that token substitution against a template file would just be ceremony.
6
+ //
7
+ // File-shaped overlay artifacts (federation.config.js, main.ts, etc.) live
8
+ // in admin-kit/template-module/ and are copied with token substitution by
9
+ // the generator — those are large enough that a real file is clearer than
10
+ // an embedded string.
11
+
12
+ const adminKitPkg = require("../package.json");
13
+
14
+ // The single peer that workspace libs declare. Pulls from admin-kit's own
15
+ // `dependencies` so bumping admin-kit's shared ref propagates automatically
16
+ // to every freshly-generated module.
17
+ const SHARED_VERSION_PIN =
18
+ (adminKitPkg.dependencies || {})["@ws-test-realm/shared"];
19
+
20
+ // Module's package.json shape. Lib declares ONLY the BOM as a peer. All
21
+ // @angular/*, @ngx-translate/core, rxjs, zone.js etc. flow transitively
22
+ // from `@ws-test-realm/shared`. ng-packagr's `allowedNonPeerDependencies`
23
+ // (in ng-package.json) permits the bare imports without per-package peer
24
+ // entries. See the convention pinned at /Users/ph/projects/ws-admin/admin/PINNED.md.
25
+ function bomOnlyPackageJson(name) {
26
+ return {
27
+ name,
28
+ version: "0.0.1",
29
+ module: `../../dist/${name}/fesm2022/${name}.mjs`,
30
+ sideEffects: true,
31
+ peerDependencies: {
32
+ "@ws-test-realm/shared": SHARED_VERSION_PIN,
33
+ },
34
+ dependencies: {
35
+ tslib: "^2.5.0",
36
+ },
37
+ wsmodules: {
38
+ target: "ext",
39
+ },
40
+ };
41
+ }
42
+
43
+ // Module's ng-package.json. The `allowedNonPeerDependencies` regex array
44
+ // is the load-bearing companion to the BOM-only peer pattern: it tells
45
+ // ng-packagr that imports from these packages are OK even though they
46
+ // aren't in peerDependencies (because the BOM owns them).
47
+ function federationNgPackageJson(name) {
48
+ return {
49
+ $schema: "../../node_modules/ng-packagr/ng-package.schema.json",
50
+ dest: `../../dist/${name}`,
51
+ assets: ["./assets"],
52
+ lib: {
53
+ entryFile: "src/public-api.ts",
54
+ },
55
+ allowedNonPeerDependencies: [
56
+ "@angular/.*",
57
+ "@ngx-translate/core",
58
+ "rxjs",
59
+ "ws-framework",
60
+ "shop-common",
61
+ "@ws-test-realm/ws-core",
62
+ ],
63
+ };
64
+ }
65
+
66
+ // Uppercase + underscore form for path/route constant names. user-management
67
+ // → USER_MANAGEMENT.
68
+ function constantBase(name) {
69
+ return name.toUpperCase().replace(/-/g, "_");
70
+ }
71
+
72
+ // Pascal-case form for the generated class name. user-management →
73
+ // UserManagement.
74
+ function classBase(name) {
75
+ return name
76
+ .split(/[-_]/g)
77
+ .map((p) => (p ? p.charAt(0).toUpperCase() + p.slice(1) : p))
78
+ .join("");
79
+ }
80
+
81
+ // Camel-case form for the federation remote name. user-management →
82
+ // userManagement (then suffixed with "Module" downstream).
83
+ function camelBase(name) {
84
+ return name
85
+ .split(/[-_]/g)
86
+ .map((p, i) => {
87
+ if (!p) return p;
88
+ return i === 0 ? p : p.charAt(0).toUpperCase() + p.slice(1);
89
+ })
90
+ .join("");
91
+ }
92
+
93
+ // The NgModule file we stamp over what `ng g library` emits. Brings in
94
+ // SubModuleRouting + adminSettings + the path/route constant convention.
95
+ // Devs typically re-point `parent: adminSettings` to whatever main module
96
+ // makes sense (crm/admin-settings/...) after generation.
97
+ function moduleSource(name) {
98
+ const className = classBase(name);
99
+ const constName = constantBase(name);
100
+ return `import { NgModule } from '@angular/core';
101
+ import { CommonModule } from '@angular/common';
102
+ import { SubModuleRouting, adminSettings } from '@ws-test-realm/ws-core';
103
+
104
+ import { ${className}Component } from './${name}/${name}.component';
105
+
106
+ export const ${constName}_PATH = '${name}';
107
+ export const ${constName}_ROUTE = '/' + ${constName}_PATH;
108
+
109
+ @SubModuleRouting({
110
+ \tparent: adminSettings,
111
+ \tpath: ${constName}_PATH,
112
+ \tlabel: '_${name}.${name}',
113
+ \tsort: 99,
114
+ \ticonPath: 'assets/svg/module.svg',
115
+ })
116
+ @NgModule({
117
+ \tdeclarations: [${className}Component],
118
+ \timports: [CommonModule],
119
+ \texports: [${className}Component],
120
+ })
121
+ export class ${className}Module {}
122
+ `;
123
+ }
124
+
125
+ function publicApiSource(name) {
126
+ return `export * from './lib/${name}.module';\n`;
127
+ }
128
+
129
+ module.exports = {
130
+ SHARED_VERSION_PIN,
131
+ bomOnlyPackageJson,
132
+ federationNgPackageJson,
133
+ moduleSource,
134
+ publicApiSource,
135
+ classBase,
136
+ camelBase,
137
+ constantBase,
138
+ };
@@ -10,6 +10,7 @@ function dropModule({ workspaceDir, name }) {
10
10
  const projectDir = path.join(workspaceDir, "projects", name);
11
11
  const distDir = path.join(workspaceDir, "dist", name);
12
12
  const distRemoteDir = path.join(workspaceDir, "dist", "admin-remotes", name);
13
+ const distRemoteAltDir = path.join(workspaceDir, "dist", `${name}-remote`);
13
14
 
14
15
  const projectExisted = fs.existsSync(projectDir);
15
16
 
@@ -31,6 +32,35 @@ function dropModule({ workspaceDir, name }) {
31
32
  );
32
33
  }
33
34
 
35
+ // Inverse of registerInWorkspaces — drop the projects/<name> entry from
36
+ // package.json#workspaces if present. Subsequent `npm install` removes
37
+ // the node_modules/<name> symlink.
38
+ const pkgPath = path.join(workspaceDir, "package.json");
39
+ let workspacesChanged = false;
40
+ if (fs.existsSync(pkgPath)) {
41
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
42
+ const entry = `projects/${name}`;
43
+ if (Array.isArray(pkg.workspaces)) {
44
+ const idx = pkg.workspaces.indexOf(entry);
45
+ if (idx >= 0) {
46
+ pkg.workspaces.splice(idx, 1);
47
+ workspacesChanged = true;
48
+ }
49
+ } else if (
50
+ pkg.workspaces &&
51
+ Array.isArray(pkg.workspaces.packages)
52
+ ) {
53
+ const idx = pkg.workspaces.packages.indexOf(entry);
54
+ if (idx >= 0) {
55
+ pkg.workspaces.packages.splice(idx, 1);
56
+ workspacesChanged = true;
57
+ }
58
+ }
59
+ if (workspacesChanged) {
60
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, "\t") + "\n");
61
+ }
62
+ }
63
+
34
64
  const fed = rebuildFederationTsconfig(workspaceDir);
35
65
  const federationTsconfigUpdated = !(name in fed.paths);
36
66
 
@@ -44,6 +74,10 @@ function dropModule({ workspaceDir, name }) {
44
74
  fs.rmSync(distRemoteDir, { recursive: true, force: true });
45
75
  distRemoteCleaned = true;
46
76
  }
77
+ if (fs.existsSync(distRemoteAltDir)) {
78
+ fs.rmSync(distRemoteAltDir, { recursive: true, force: true });
79
+ distRemoteCleaned = true;
80
+ }
47
81
 
48
82
  // Hook point: when type publishing to the host lands, clean the host-side
49
83
  // typings for this module here. See TODO.md.
@@ -52,6 +86,7 @@ function dropModule({ workspaceDir, name }) {
52
86
  if (
53
87
  !projectExisted &&
54
88
  !angularChanged &&
89
+ !workspacesChanged &&
55
90
  !distCleaned &&
56
91
  !distRemoteCleaned
57
92
  ) {
@@ -61,6 +96,7 @@ function dropModule({ workspaceDir, name }) {
61
96
  return {
62
97
  projectExisted,
63
98
  angularChanged,
99
+ workspacesChanged,
64
100
  federationTsconfigUpdated,
65
101
  distCleaned,
66
102
  distRemoteCleaned,
@@ -1,81 +1,149 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
- const { rebuildFederationTsconfig } = require("./wire-host");
3
+ const { ngGenerateLibrary, npmInstall } = require("./ng-shell");
4
+ const {
5
+ bomOnlyPackageJson,
6
+ federationNgPackageJson,
7
+ moduleSource,
8
+ publicApiSource,
9
+ camelBase,
10
+ } = require("./bom-shape");
4
11
 
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
- }
12
+ const TEMPLATE_MODULE_DIR = path.join(__dirname, "..", "template-module");
11
13
 
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
- }
14
+ // Pipeline orchestrator. Generates a federated admin module project by:
15
+ //
16
+ // 1. shelling out to `ng generate library` for the Angular-blessed source
17
+ // scaffold (component, module file, ng-package.json, tsconfig.lib*,
18
+ // package.json, public-api.ts, plus angular.json entry and root
19
+ // tsconfig path);
20
+ // 2. overwriting the schematic's package.json + ng-package.json with our
21
+ // BOM-only / allowedNonPeerDependencies shapes (see bom-shape.js);
22
+ // 3. overwriting the schematic's module file with our @SubModuleRouting
23
+ // stamp + the <NAME>_PATH/_ROUTE constants convention;
24
+ // 4. copying federation-overlay files from admin-kit/template-module/
25
+ // with token substitution (federation.config.js, src/main.ts,
26
+ // src/exposed-module.ts, src/index.html, src/polyfills.ts,
27
+ // tsconfig.app.json, assets/i18n/*);
28
+ // 5. replacing the angular.json block (schematic wrote a library-type
29
+ // block with 2 targets; we need application-type with 3 targets:
30
+ // build-lib + build + esbuild);
31
+ // 6. cleaning up the duplicate `paths` entry the schematic put into the
32
+ // root tsconfig.json (tsconfig.federation.json is our single source
33
+ // of truth for federation paths);
34
+ // 7. registering the new project in root package.json#workspaces;
35
+ // 8. running `npm install` — npm workspaces machinery materializes the
36
+ // `node_modules/<name>` symlink, and the workspace's postinstall hook
37
+ // (ws-sync-paths) rebuilds tsconfig.federation.json with the new path
38
+ // entry.
39
+ //
40
+ // End state: the dev can immediately run `npm run wire` and the new module
41
+ // builds and deploys alongside the rest.
42
+ function generateModule({ workspaceDir, name }) {
43
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
44
+ throw new Error(
45
+ `Invalid module name: ${name}. Use lowercase, hyphen-separated (e.g. db-login).`
46
+ );
47
+ }
48
+ if (!fs.existsSync(path.join(workspaceDir, "angular.json"))) {
49
+ throw new Error(`No angular.json in ${workspaceDir} (not a workspace).`);
50
+ }
51
+ const projectDir = path.join(workspaceDir, "projects", name);
52
+ if (fs.existsSync(projectDir)) {
53
+ throw new Error(`projects/${name}/ already exists.`);
54
+ }
21
55
 
22
- function buildTokens(name) {
23
- return {
24
- name,
25
- className: pascalCase(name),
26
- camelName: camelCase(name),
27
- selector: `ws-${name}`,
28
- };
56
+ // 1. Angular schematic: scaffolds projects/<name>/ + updates angular.json
57
+ // + adds tsconfig.json paths entry. Adds tsconfig.lib(.prod).json,
58
+ // tsconfig.spec.json, ng-package.json, package.json, README.md,
59
+ // src/public-api.ts, src/lib/<name>.module.ts, src/lib/<name>.component.*.
60
+ ngGenerateLibrary({ workspaceDir, name });
61
+
62
+ // 2. Overwrite the schematic's package.json with the BOM-only shape.
63
+ writeJson(path.join(projectDir, "package.json"), bomOnlyPackageJson(name));
64
+
65
+ // 3. Overwrite the schematic's ng-package.json with our shape (different
66
+ // `dest`, plus `allowedNonPeerDependencies`).
67
+ writeJson(
68
+ path.join(projectDir, "ng-package.json"),
69
+ federationNgPackageJson(name)
70
+ );
71
+
72
+ // 4. Overwrite the schematic's module file with our @SubModuleRouting
73
+ // stamp + the <NAME>_PATH/_ROUTE constants convention.
74
+ fs.writeFileSync(
75
+ path.join(projectDir, "src", "lib", `${name}.module.ts`),
76
+ moduleSource(name)
77
+ );
78
+
79
+ // 5. Replace public-api.ts: the schematic re-exports both module and
80
+ // component; for federation we only need the module export. (Decorated
81
+ // components requiring DCE protection get added by the dev later, per
82
+ // the @ExpansionEntry public-api rule.)
83
+ fs.writeFileSync(
84
+ path.join(projectDir, "src", "public-api.ts"),
85
+ publicApiSource(name)
86
+ );
87
+
88
+ // 6. Copy federation overlay files from template-module/ with token
89
+ // substitution. The schematic doesn't produce any of these.
90
+ copyOverlayWithSubstitution(projectDir, name);
91
+
92
+ // 7. Replace the angular.json block. ng schematic wrote a library-type
93
+ // block with build (ng-packagr) + test (karma) targets. We need
94
+ // application-type with build-lib + build + esbuild.
95
+ replaceAngularJsonBlock(workspaceDir, name);
96
+
97
+ // 8. Clean the duplicate tsconfig.json paths entry the schematic added.
98
+ cleanTsconfigPathsEntry(workspaceDir, name);
99
+
100
+ // 9. Add to package.json#workspaces (npm-workspaces machinery uses this
101
+ // to materialize the node_modules/<name> symlink).
102
+ registerInWorkspaces(workspaceDir, name);
103
+
104
+ // 10. npm install — symlinks materialize; postinstall (ws-sync-paths)
105
+ // rebuilds tsconfig.federation.json with the new path entry.
106
+ npmInstall({ workspaceDir });
107
+
108
+ return { projectDir };
29
109
  }
30
110
 
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);
111
+ function writeJson(filePath, obj) {
112
+ fs.writeFileSync(filePath, JSON.stringify(obj, null, "\t") + "\n");
37
113
  }
38
114
 
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);
115
+ // Recursively copy admin-kit/template-module/ into projects/<name>/,
116
+ // substituting __name__ <name> and __camelName__ → camelCase(<name>)
117
+ // in both file contents AND filenames. Existing destination files are
118
+ // overwritten the previous schematic+overwrites have already populated
119
+ // the directory and these copies don't conflict.
120
+ function copyOverlayWithSubstitution(destDir, name) {
121
+ const camelName = camelBase(name);
122
+ const substitute = (s) =>
123
+ s.replace(/__camelName__/g, camelName).replace(/__name__/g, name);
124
+
125
+ function copyRecursive(src, dst) {
126
+ if (!fs.existsSync(src)) return;
127
+ const stat = fs.statSync(src);
128
+ if (stat.isDirectory()) {
129
+ fs.mkdirSync(dst, { recursive: true });
130
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
131
+ copyRecursive(
132
+ path.join(src, entry.name),
133
+ path.join(dst, substitute(entry.name))
134
+ );
135
+ }
47
136
  } else {
48
- const content = fs.readFileSync(srcPath, "utf8");
49
- fs.writeFileSync(destPath, substitute(content, tokens));
137
+ fs.writeFileSync(dst, substitute(fs.readFileSync(src, "utf8")));
50
138
  }
51
139
  }
140
+ copyRecursive(TEMPLATE_MODULE_DIR, destDir);
52
141
  }
53
142
 
54
- // Register the project as a dual-build admin module in angular.json. Three
55
- // architect targets per project:
56
- //
57
- // build-lib → @angular-devkit/build-angular:ng-packagr. Produces the typed
58
- // library at dist/<name>/ (with .d.ts + package.json + fesm2015).
59
- // Consumed at COMPILE time by sibling projects in this workspace
60
- // (and by the host) via tsconfig.federation.json `paths`.
61
- //
62
- // build → @angular-architects/native-federation:build. Wraps the
63
- // `esbuild` target and emits remoteEntry.json + chunks at
64
- // dist/<name>-remote/. Deployed to the fiddle; consumed at
65
- // RUNTIME via the host's initFederation + import map.
66
- //
67
- // esbuild → @angular-devkit/build-angular:application. The bundle
68
- // target the native-federation builder wraps (NF 17.1+ requires
69
- // the application builder). Output dir dist/<name>-remote/ keeps
70
- // it separate from the lib output; outputPath.browser is
71
- // flattened to "" so federation chunks land at the dist root
72
- // rather than dist/<name>-remote/browser/, keeping the deploy
73
- // layout consistent with the pre-ng17 line.
74
- //
75
- // Every module is dual-build by default — any project could grow a type-level
76
- // consumer (pluggable slots, base components), and the dual invariant
77
- // guarantees the typed surface is always there.
78
- function registerInAngularJson(workspaceDir, name) {
143
+ // Replace projects.<name> in angular.json with the application-type +
144
+ // 3-target dual-build shape. We overwrite (not merge) because the schematic
145
+ // emits a library-type block that we'd need to fully restructure anyway.
146
+ function replaceAngularJsonBlock(workspaceDir, name) {
79
147
  const angularJsonPath = path.join(workspaceDir, "angular.json");
80
148
  const angular = JSON.parse(fs.readFileSync(angularJsonPath, "utf8"));
81
149
  angular.projects = angular.projects || {};
@@ -84,7 +152,7 @@ function registerInAngularJson(workspaceDir, name) {
84
152
  schematics: {},
85
153
  root: `projects/${name}`,
86
154
  sourceRoot: `projects/${name}/src`,
87
- prefix: "ws",
155
+ prefix: "lib",
88
156
  architect: {
89
157
  "build-lib": {
90
158
  builder: "@angular-devkit/build-angular:ng-packagr",
@@ -93,7 +161,9 @@ function registerInAngularJson(workspaceDir, name) {
93
161
  production: {
94
162
  tsConfig: `projects/${name}/tsconfig.lib.prod.json`,
95
163
  },
96
- development: { tsConfig: `projects/${name}/tsconfig.lib.json` },
164
+ development: {
165
+ tsConfig: `projects/${name}/tsconfig.lib.json`,
166
+ },
97
167
  },
98
168
  defaultConfiguration: "production",
99
169
  },
@@ -114,20 +184,7 @@ function registerInAngularJson(workspaceDir, name) {
114
184
  browser: `projects/${name}/src/main.ts`,
115
185
  polyfills: [],
116
186
  tsConfig: `projects/${name}/tsconfig.app.json`,
117
- // Cross-workspace federation siblings reach this workspace via
118
- // admin-kit-managed symlinks at node_modules/<name>; esbuild needs
119
- // preserveSymlinks=true to resolve peer imports out of those .d.ts/
120
- // .mjs files against THIS workspace's node_modules (where the
121
- // peers are installed) rather than walking up from the symlink
122
- // target in the fiddle.
123
187
  preserveSymlinks: true,
124
- // Copy the project's `assets/` into the federation output so the
125
- // fiddle's `/api/v1/admin-federation/asset/<id>/...` endpoint can
126
- // serve them. The endpoint resolves to
127
- // `META-INF/federation/<id>/remote/<subpath>` (jar target) or
128
- // `<ext-dir>/remote/<subpath>` (ext target); packIntoJar /
129
- // deployExt mirror whatever's in `dist/<id>-remote/` into that
130
- // `remote/` directory, so anything here becomes addressable.
131
188
  assets: [
132
189
  {
133
190
  glob: "**/*",
@@ -152,26 +209,51 @@ function registerInAngularJson(workspaceDir, name) {
152
209
  );
153
210
  }
154
211
 
155
- function generateModule({ workspaceDir, name, templateModuleDir }) {
156
- if (!/^[a-z][a-z0-9-]*$/.test(name)) {
157
- throw new Error(
158
- `Invalid module name: ${name}. Use lowercase, hyphen-separated (e.g. db-login).`
159
- );
212
+ // ng g library appends `<name>: ["./dist/<name>"]` to the root tsconfig's
213
+ // compilerOptions.paths. tsconfig.federation.json is our SSOT for federation
214
+ // paths so this duplicate would just be confusing. Strip it.
215
+ function cleanTsconfigPathsEntry(workspaceDir, name) {
216
+ const tsconfigPath = path.join(workspaceDir, "tsconfig.json");
217
+ if (!fs.existsSync(tsconfigPath)) return;
218
+ const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, "utf8"));
219
+ if (
220
+ !tsconfig.compilerOptions ||
221
+ !tsconfig.compilerOptions.paths ||
222
+ !(name in tsconfig.compilerOptions.paths)
223
+ ) {
224
+ return;
160
225
  }
161
- if (!fs.existsSync(path.join(workspaceDir, "angular.json"))) {
162
- throw new Error(`No angular.json in ${workspaceDir} (not a workspace).`);
226
+ delete tsconfig.compilerOptions.paths[name];
227
+ if (Object.keys(tsconfig.compilerOptions.paths).length === 0) {
228
+ delete tsconfig.compilerOptions.paths;
163
229
  }
164
- const projectDir = path.join(workspaceDir, "projects", name);
165
- if (fs.existsSync(projectDir)) {
166
- throw new Error(`projects/${name}/ already exists.`);
167
- }
168
-
169
- const tokens = buildTokens(name);
170
- copyAndSubstitute(templateModuleDir, projectDir, tokens);
171
- registerInAngularJson(workspaceDir, name);
172
- rebuildFederationTsconfig(workspaceDir);
230
+ fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, "\t") + "\n");
231
+ }
173
232
 
174
- return { projectDir, tokens };
233
+ // Add "projects/<name>" to package.json#workspaces if missing. This is the
234
+ // gap-closer: without this entry, npm-workspaces machinery won't symlink
235
+ // node_modules/<name> → projects/<name>, and native-federation's
236
+ // findDepPackageJson lookup fails at build time.
237
+ function registerInWorkspaces(workspaceDir, name) {
238
+ const pkgPath = path.join(workspaceDir, "package.json");
239
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
240
+ const entry = `projects/${name}`;
241
+ pkg.workspaces = pkg.workspaces || [];
242
+ if (!Array.isArray(pkg.workspaces)) {
243
+ // npm also supports { packages: [...] } shape. Handle both.
244
+ if (pkg.workspaces.packages) {
245
+ if (!pkg.workspaces.packages.includes(entry)) {
246
+ pkg.workspaces.packages.push(entry);
247
+ }
248
+ } else {
249
+ throw new Error(
250
+ "Unsupported package.json#workspaces shape; expected array or { packages: [...] }"
251
+ );
252
+ }
253
+ } else if (!pkg.workspaces.includes(entry)) {
254
+ pkg.workspaces.push(entry);
255
+ }
256
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, "\t") + "\n");
175
257
  }
176
258
 
177
- module.exports = { generateModule, buildTokens };
259
+ module.exports = { generateModule };
@@ -0,0 +1,31 @@
1
+ const { execSync } = require("child_process");
2
+
3
+ // Run `ng generate library <name>` in the workspace via npx, so we don't
4
+ // require @angular/cli to be globally installed. Every admin workspace
5
+ // already has it as a transitive dep through @angular/build. Defaults:
6
+ // --prefix=lib (matches existing crm/shop-settings/...)
7
+ // --standalone=false (NgModule mode; required by our PINNED rule)
8
+ // --skip-install (admin-kit drives the install at end of pipeline)
9
+ //
10
+ // Inherits stdio so the dev sees ng's output and any prompts. ng's exit code
11
+ // is checked: on failure, throws so the generator pipeline aborts cleanly.
12
+ function ngGenerateLibrary({ workspaceDir, name, prefix = "lib" }) {
13
+ const cmd = [
14
+ "npx",
15
+ "--no-install",
16
+ "ng",
17
+ "generate",
18
+ "library",
19
+ name,
20
+ `--prefix=${prefix}`,
21
+ "--standalone=false",
22
+ "--skip-install",
23
+ ].join(" ");
24
+ execSync(cmd, { cwd: workspaceDir, stdio: "inherit" });
25
+ }
26
+
27
+ function npmInstall({ workspaceDir }) {
28
+ execSync("npm install", { cwd: workspaceDir, stdio: "inherit" });
29
+ }
30
+
31
+ module.exports = { ngGenerateLibrary, npmInstall };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ws-test-realm/admin-kit",
3
- "version": "0.6.1-ng20",
3
+ "version": "0.6.3-ng20",
4
4
  "description": "Workflow CLI + scaffolding for Wiresphere admin-modules workspaces (Angular 20 + 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": {
@@ -28,7 +28,7 @@
28
28
  "dependencies": {
29
29
  "@angular-architects/native-federation": "^20.0.0",
30
30
  "@ws-test-realm/devkit": "^0.8.0-ng20",
31
- "@ws-test-realm/shared": "^0.8.0-ng20",
31
+ "@ws-test-realm/shared": "^0.8.1-ng20",
32
32
  "adm-zip": "^0.5.10",
33
33
  "fast-xml-parser": "^4.3.0"
34
34
  }
@@ -1,8 +0,0 @@
1
- {
2
- "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3
- "dest": "../../dist/__name__",
4
- "assets": ["./assets"],
5
- "lib": {
6
- "entryFile": "src/public-api.ts"
7
- }
8
- }
@@ -1,19 +0,0 @@
1
- {
2
- "name": "__name__",
3
- "version": "0.0.1",
4
- "module": "../../dist/__name__/fesm2022/__name__.mjs",
5
- "sideEffects": true,
6
- "peerDependencies": {
7
- "@angular/common": "^20.0.0",
8
- "@angular/core": "^20.0.0",
9
- "@angular/forms": "^20.0.0",
10
- "@angular/router": "^20.0.0",
11
- "rxjs": "~7.8.0"
12
- },
13
- "dependencies": {
14
- "tslib": "^2.5.0"
15
- },
16
- "wsmodules": {
17
- "target": "jar"
18
- }
19
- }
@@ -1,11 +0,0 @@
1
- import { NgModule } from '@angular/core';
2
- import { CommonModule } from '@angular/common';
3
-
4
- import { __className__Component } from './components/__name__/__name__.component';
5
-
6
- @NgModule({
7
- declarations: [__className__Component],
8
- imports: [CommonModule],
9
- exports: [__className__Component],
10
- })
11
- export class __className__Module {}
@@ -1 +0,0 @@
1
- <p>__name__ works!</p>
@@ -1,8 +0,0 @@
1
- import { Component } from '@angular/core';
2
-
3
- @Component({
4
- selector: '__selector__',
5
- templateUrl: './__name__.component.html',
6
- styleUrls: ['./__name__.component.scss'],
7
- })
8
- export class __className__Component {}
@@ -1,6 +0,0 @@
1
- /*
2
- * Public API Surface of __name__
3
- */
4
-
5
- export * from './lib/__name__.module';
6
- export * from './lib/components/__name__/__name__.component';
@@ -1,14 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "../../out-tsc/lib",
5
- "target": "es2022",
6
- "useDefineForClassFields": false,
7
- "declaration": true,
8
- "declarationMap": true,
9
- "inlineSources": true,
10
- "types": [],
11
- "lib": ["dom", "es2022"]
12
- },
13
- "exclude": ["src/test.ts", "**/*.spec.ts"]
14
- }
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "./tsconfig.lib.json",
3
- "compilerOptions": {
4
- "declarationMap": false
5
- },
6
- "angularCompilerOptions": {
7
- "compilationMode": "partial"
8
- }
9
- }