@ws-test-realm/admin-kit 0.6.2-ng20 → 0.6.4-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.
- package/bin/ws-generate-module.js +6 -9
- package/lib/bom-shape.js +144 -0
- package/lib/drop-module.js +36 -0
- package/lib/generate-module.js +182 -97
- package/lib/ng-shell.js +31 -0
- package/package.json +1 -1
- package/template-module/ng-package.json +0 -8
- package/template-module/package.json +0 -19
- package/template-module/src/lib/__name__.module.ts +0 -11
- package/template-module/src/lib/components/__name__/__name__.component.html +0 -1
- package/template-module/src/lib/components/__name__/__name__.component.scss +0 -0
- package/template-module/src/lib/components/__name__/__name__.component.ts +0 -8
- package/template-module/src/public-api.ts +0 -6
- package/template-module/tsconfig.lib.json +0 -14
- package/template-module/tsconfig.lib.prod.json +0 -9
|
@@ -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.
|
|
4
|
-
//
|
|
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(`
|
|
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);
|
package/lib/bom-shape.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
//
|
|
98
|
+
// Adopts the ng20+ naming convention emitted by the schematic:
|
|
99
|
+
// - file: src/lib/<name>-module.ts (dash suffix, no dot)
|
|
100
|
+
// - module cls: <Pascal>Module
|
|
101
|
+
// - component: ./<name> (flat in lib/, no .component infix)
|
|
102
|
+
// - cmp cls: <Pascal> (no Component suffix)
|
|
103
|
+
function moduleSource(name) {
|
|
104
|
+
const className = classBase(name);
|
|
105
|
+
const constName = constantBase(name);
|
|
106
|
+
return `import { NgModule } from '@angular/core';
|
|
107
|
+
import { CommonModule } from '@angular/common';
|
|
108
|
+
import { SubModuleRouting, adminSettings } from '@ws-test-realm/ws-core';
|
|
109
|
+
|
|
110
|
+
import { ${className} } from './${name}';
|
|
111
|
+
|
|
112
|
+
export const ${constName}_PATH = '${name}';
|
|
113
|
+
export const ${constName}_ROUTE = '/' + ${constName}_PATH;
|
|
114
|
+
|
|
115
|
+
@SubModuleRouting({
|
|
116
|
+
\tparent: adminSettings,
|
|
117
|
+
\tpath: ${constName}_PATH,
|
|
118
|
+
\tlabel: '_${name}.${name}',
|
|
119
|
+
\tsort: 99,
|
|
120
|
+
\ticonPath: 'assets/svg/module.svg',
|
|
121
|
+
})
|
|
122
|
+
@NgModule({
|
|
123
|
+
\tdeclarations: [${className}],
|
|
124
|
+
\timports: [CommonModule],
|
|
125
|
+
\texports: [${className}],
|
|
126
|
+
})
|
|
127
|
+
export class ${className}Module {}
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function publicApiSource(name) {
|
|
132
|
+
return `export * from './lib/${name}-module';\n`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
SHARED_VERSION_PIN,
|
|
137
|
+
bomOnlyPackageJson,
|
|
138
|
+
federationNgPackageJson,
|
|
139
|
+
moduleSource,
|
|
140
|
+
publicApiSource,
|
|
141
|
+
classBase,
|
|
142
|
+
camelBase,
|
|
143
|
+
constantBase,
|
|
144
|
+
};
|
package/lib/drop-module.js
CHANGED
|
@@ -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,
|
package/lib/generate-module.js
CHANGED
|
@@ -1,81 +1,152 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
-
const {
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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>.ts
|
|
60
|
+
// (ng20+ naming: dash suffix, no .component infix, classes are plain
|
|
61
|
+
// <Pascal> / <Pascal>Module).
|
|
62
|
+
ngGenerateLibrary({ workspaceDir, name });
|
|
63
|
+
|
|
64
|
+
// 2. Overwrite the schematic's package.json with the BOM-only shape.
|
|
65
|
+
writeJson(path.join(projectDir, "package.json"), bomOnlyPackageJson(name));
|
|
66
|
+
|
|
67
|
+
// 3. Overwrite the schematic's ng-package.json with our shape (different
|
|
68
|
+
// `dest`, plus `allowedNonPeerDependencies`).
|
|
69
|
+
writeJson(
|
|
70
|
+
path.join(projectDir, "ng-package.json"),
|
|
71
|
+
federationNgPackageJson(name)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// 4. Overwrite the schematic's module file with our @SubModuleRouting
|
|
75
|
+
// stamp + the <NAME>_PATH/_ROUTE constants convention. ng20 emits
|
|
76
|
+
// `<name>-module.ts` (dash suffix, no dot).
|
|
77
|
+
fs.writeFileSync(
|
|
78
|
+
path.join(projectDir, "src", "lib", `${name}-module.ts`),
|
|
79
|
+
moduleSource(name)
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// 5. Replace public-api.ts: the schematic re-exports both module and
|
|
83
|
+
// component; for federation we only need the module export. (Decorated
|
|
84
|
+
// components requiring DCE protection get added by the dev later, per
|
|
85
|
+
// the @ExpansionEntry public-api rule.)
|
|
86
|
+
fs.writeFileSync(
|
|
87
|
+
path.join(projectDir, "src", "public-api.ts"),
|
|
88
|
+
publicApiSource(name)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// 6. Copy federation overlay files from template-module/ with token
|
|
92
|
+
// substitution. The schematic doesn't produce any of these.
|
|
93
|
+
copyOverlayWithSubstitution(projectDir, name);
|
|
94
|
+
|
|
95
|
+
// 7. Replace the angular.json block. ng schematic wrote a library-type
|
|
96
|
+
// block with build (ng-packagr) + test (karma) targets. We need
|
|
97
|
+
// application-type with build-lib + build + esbuild.
|
|
98
|
+
replaceAngularJsonBlock(workspaceDir, name);
|
|
99
|
+
|
|
100
|
+
// 8. Clean the duplicate tsconfig.json paths entry the schematic added.
|
|
101
|
+
cleanTsconfigPathsEntry(workspaceDir, name);
|
|
102
|
+
|
|
103
|
+
// 9. Add to package.json#workspaces (npm-workspaces machinery uses this
|
|
104
|
+
// to materialize the node_modules/<name> symlink).
|
|
105
|
+
registerInWorkspaces(workspaceDir, name);
|
|
106
|
+
|
|
107
|
+
// 10. npm install — symlinks materialize; postinstall (ws-sync-paths)
|
|
108
|
+
// rebuilds tsconfig.federation.json with the new path entry.
|
|
109
|
+
npmInstall({ workspaceDir });
|
|
110
|
+
|
|
111
|
+
return { projectDir };
|
|
29
112
|
}
|
|
30
113
|
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
.replace(/__className__/g, tokens.className)
|
|
34
|
-
.replace(/__camelName__/g, tokens.camelName)
|
|
35
|
-
.replace(/__selector__/g, tokens.selector)
|
|
36
|
-
.replace(/__name__/g, tokens.name);
|
|
114
|
+
function writeJson(filePath, obj) {
|
|
115
|
+
fs.writeFileSync(filePath, JSON.stringify(obj, null, "\t") + "\n");
|
|
37
116
|
}
|
|
38
117
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
118
|
+
// Recursively copy admin-kit/template-module/ into projects/<name>/,
|
|
119
|
+
// substituting __name__ → <name> and __camelName__ → camelCase(<name>)
|
|
120
|
+
// in both file contents AND filenames. Existing destination files are
|
|
121
|
+
// overwritten — the previous schematic+overwrites have already populated
|
|
122
|
+
// the directory and these copies don't conflict.
|
|
123
|
+
function copyOverlayWithSubstitution(destDir, name) {
|
|
124
|
+
const camelName = camelBase(name);
|
|
125
|
+
const substitute = (s) =>
|
|
126
|
+
s.replace(/__camelName__/g, camelName).replace(/__name__/g, name);
|
|
127
|
+
|
|
128
|
+
function copyRecursive(src, dst) {
|
|
129
|
+
if (!fs.existsSync(src)) return;
|
|
130
|
+
const stat = fs.statSync(src);
|
|
131
|
+
if (stat.isDirectory()) {
|
|
132
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
133
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
134
|
+
copyRecursive(
|
|
135
|
+
path.join(src, entry.name),
|
|
136
|
+
path.join(dst, substitute(entry.name))
|
|
137
|
+
);
|
|
138
|
+
}
|
|
47
139
|
} else {
|
|
48
|
-
|
|
49
|
-
fs.writeFileSync(destPath, substitute(content, tokens));
|
|
140
|
+
fs.writeFileSync(dst, substitute(fs.readFileSync(src, "utf8")));
|
|
50
141
|
}
|
|
51
142
|
}
|
|
143
|
+
copyRecursive(TEMPLATE_MODULE_DIR, destDir);
|
|
52
144
|
}
|
|
53
145
|
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
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) {
|
|
146
|
+
// Replace projects.<name> in angular.json with the application-type +
|
|
147
|
+
// 3-target dual-build shape. We overwrite (not merge) because the schematic
|
|
148
|
+
// emits a library-type block that we'd need to fully restructure anyway.
|
|
149
|
+
function replaceAngularJsonBlock(workspaceDir, name) {
|
|
79
150
|
const angularJsonPath = path.join(workspaceDir, "angular.json");
|
|
80
151
|
const angular = JSON.parse(fs.readFileSync(angularJsonPath, "utf8"));
|
|
81
152
|
angular.projects = angular.projects || {};
|
|
@@ -84,7 +155,7 @@ function registerInAngularJson(workspaceDir, name) {
|
|
|
84
155
|
schematics: {},
|
|
85
156
|
root: `projects/${name}`,
|
|
86
157
|
sourceRoot: `projects/${name}/src`,
|
|
87
|
-
prefix: "
|
|
158
|
+
prefix: "lib",
|
|
88
159
|
architect: {
|
|
89
160
|
"build-lib": {
|
|
90
161
|
builder: "@angular-devkit/build-angular:ng-packagr",
|
|
@@ -93,7 +164,9 @@ function registerInAngularJson(workspaceDir, name) {
|
|
|
93
164
|
production: {
|
|
94
165
|
tsConfig: `projects/${name}/tsconfig.lib.prod.json`,
|
|
95
166
|
},
|
|
96
|
-
development: {
|
|
167
|
+
development: {
|
|
168
|
+
tsConfig: `projects/${name}/tsconfig.lib.json`,
|
|
169
|
+
},
|
|
97
170
|
},
|
|
98
171
|
defaultConfiguration: "production",
|
|
99
172
|
},
|
|
@@ -114,20 +187,7 @@ function registerInAngularJson(workspaceDir, name) {
|
|
|
114
187
|
browser: `projects/${name}/src/main.ts`,
|
|
115
188
|
polyfills: [],
|
|
116
189
|
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
190
|
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
191
|
assets: [
|
|
132
192
|
{
|
|
133
193
|
glob: "**/*",
|
|
@@ -152,26 +212,51 @@ function registerInAngularJson(workspaceDir, name) {
|
|
|
152
212
|
);
|
|
153
213
|
}
|
|
154
214
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
215
|
+
// ng g library appends `<name>: ["./dist/<name>"]` to the root tsconfig's
|
|
216
|
+
// compilerOptions.paths. tsconfig.federation.json is our SSOT for federation
|
|
217
|
+
// paths so this duplicate would just be confusing. Strip it.
|
|
218
|
+
function cleanTsconfigPathsEntry(workspaceDir, name) {
|
|
219
|
+
const tsconfigPath = path.join(workspaceDir, "tsconfig.json");
|
|
220
|
+
if (!fs.existsSync(tsconfigPath)) return;
|
|
221
|
+
const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, "utf8"));
|
|
222
|
+
if (
|
|
223
|
+
!tsconfig.compilerOptions ||
|
|
224
|
+
!tsconfig.compilerOptions.paths ||
|
|
225
|
+
!(name in tsconfig.compilerOptions.paths)
|
|
226
|
+
) {
|
|
227
|
+
return;
|
|
160
228
|
}
|
|
161
|
-
|
|
162
|
-
|
|
229
|
+
delete tsconfig.compilerOptions.paths[name];
|
|
230
|
+
if (Object.keys(tsconfig.compilerOptions.paths).length === 0) {
|
|
231
|
+
delete tsconfig.compilerOptions.paths;
|
|
163
232
|
}
|
|
164
|
-
|
|
165
|
-
|
|
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);
|
|
233
|
+
fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, "\t") + "\n");
|
|
234
|
+
}
|
|
173
235
|
|
|
174
|
-
|
|
236
|
+
// Add "projects/<name>" to package.json#workspaces if missing. This is the
|
|
237
|
+
// gap-closer: without this entry, npm-workspaces machinery won't symlink
|
|
238
|
+
// node_modules/<name> → projects/<name>, and native-federation's
|
|
239
|
+
// findDepPackageJson lookup fails at build time.
|
|
240
|
+
function registerInWorkspaces(workspaceDir, name) {
|
|
241
|
+
const pkgPath = path.join(workspaceDir, "package.json");
|
|
242
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
243
|
+
const entry = `projects/${name}`;
|
|
244
|
+
pkg.workspaces = pkg.workspaces || [];
|
|
245
|
+
if (!Array.isArray(pkg.workspaces)) {
|
|
246
|
+
// npm also supports { packages: [...] } shape. Handle both.
|
|
247
|
+
if (pkg.workspaces.packages) {
|
|
248
|
+
if (!pkg.workspaces.packages.includes(entry)) {
|
|
249
|
+
pkg.workspaces.packages.push(entry);
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
throw new Error(
|
|
253
|
+
"Unsupported package.json#workspaces shape; expected array or { packages: [...] }"
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
} else if (!pkg.workspaces.includes(entry)) {
|
|
257
|
+
pkg.workspaces.push(entry);
|
|
258
|
+
}
|
|
259
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, "\t") + "\n");
|
|
175
260
|
}
|
|
176
261
|
|
|
177
|
-
module.exports = { generateModule
|
|
262
|
+
module.exports = { generateModule };
|
package/lib/ng-shell.js
ADDED
|
@@ -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.
|
|
3
|
+
"version": "0.6.4-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": {
|
|
@@ -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>
|
|
File without changes
|
|
@@ -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
|
-
}
|