ng-tablekit 1.0.0
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/LICENSE +21 -0
- package/PUBLISHING.md +46 -0
- package/README.md +102 -0
- package/bin/tablekit.mjs +436 -0
- package/package.json +81 -0
- package/src/app/core/config/table.config.ts +18 -0
- package/src/app/core/data/record.data.ts +26 -0
- package/src/app/core/models/table.models.ts +61 -0
- package/src/app/core/utils/table.utils.ts +49 -0
- package/src/app/templates/master-page/master-map-drawer.component.css +118 -0
- package/src/app/templates/master-page/master-map-drawer.component.html +49 -0
- package/src/app/templates/master-page/master-map-drawer.component.ts +94 -0
- package/src/app/templates/master-page/master-page.component.css +311 -0
- package/src/app/templates/master-page/master-page.component.html +131 -0
- package/src/app/templates/master-page/master-page.component.ts +290 -0
- package/src/app/templates/master-page/master-record-drawer.component.css +3 -0
- package/src/app/templates/master-page/master-record-drawer.component.html +52 -0
- package/src/app/templates/master-page/master-record-drawer.component.ts +74 -0
- package/src/app/templates/report-page/report-page.component.css +274 -0
- package/src/app/templates/report-page/report-page.component.html +75 -0
- package/src/app/templates/report-page/report-page.component.ts +174 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TableKit contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/PUBLISHING.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Publishing ng-tablekit
|
|
2
|
+
|
|
3
|
+
The package is configured for public npm distribution as `ng-tablekit`.
|
|
4
|
+
|
|
5
|
+
## Release checklist
|
|
6
|
+
|
|
7
|
+
1. Confirm that the package name is still available:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm view ng-tablekit
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
A `404 Not Found` response means no package currently uses the name. Availability is not reserved until the first successful publish.
|
|
14
|
+
|
|
15
|
+
2. Sign in to npmjs.com, open **Account → Two-Factor Authentication**, and add
|
|
16
|
+
a WebAuthn security key or passkey. New TOTP enrollment is no longer
|
|
17
|
+
supported. Windows Hello can be used as the passkey on Windows.
|
|
18
|
+
|
|
19
|
+
Save the recovery codes somewhere secure, then refresh the CLI session:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm login
|
|
23
|
+
npm whoami
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
3. Run all publication checks:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm run release:check
|
|
30
|
+
npm run test:package
|
|
31
|
+
npm pack --dry-run
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
4. Publish the first public release:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm publish --access public
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
5. Verify the public command from a clean Angular 19 workspace:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx ng-tablekit@latest add report-page
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For later releases, update the version with `npm version patch`, `minor`, or `major` before publishing. Never reuse an npm version that has already been published.
|
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# ng-tablekit
|
|
2
|
+
|
|
3
|
+
A cross-platform CLI that adds ready-to-use Angular 19 and NG-ZORRO 19 table pages to an existing Angular workspace.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js 18.19 or newer
|
|
8
|
+
- An Angular 19 workspace
|
|
9
|
+
- npm, pnpm, Yarn, or Bun
|
|
10
|
+
|
|
11
|
+
The CLI runs on Windows, macOS, and Linux. It detects the nearest Angular workspace, so you can run it from the workspace root or any folder below it.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
Add a report page with filter, Excel export, PDF export, global search, sorting, and pagination:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx ng-tablekit@latest add report-page
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or add a master page with global search, Add/Edit drawers, row actions, role mapping, sorting, and pagination:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx ng-tablekit@latest add master-page
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then start the Angular application and open `/report-page` or `/master-page`.
|
|
28
|
+
|
|
29
|
+
The CLI automatically:
|
|
30
|
+
|
|
31
|
+
- copies the selected page and its shared runtime files;
|
|
32
|
+
- installs only missing page dependencies;
|
|
33
|
+
- configures the NG-ZORRO stylesheet and Angular animations;
|
|
34
|
+
- adds a lazy route to `src/app/app.routes.ts`;
|
|
35
|
+
- uses relative imports, so no TypeScript path aliases are required;
|
|
36
|
+
- preserves existing files and stops before overwriting changed content.
|
|
37
|
+
|
|
38
|
+
## Commands
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx ng-tablekit@latest list
|
|
42
|
+
npx ng-tablekit@latest --help
|
|
43
|
+
npx ng-tablekit@latest --version
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Options for `add`:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
--project <directory> Angular workspace root (normally auto-detected)
|
|
50
|
+
--path <directory> Custom destination inside the workspace
|
|
51
|
+
--route <path> Custom route path
|
|
52
|
+
--skip-route Do not edit app.routes.ts
|
|
53
|
+
--skip-install Update package.json without installing dependencies
|
|
54
|
+
--force Replace conflicting generated files
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Example with a custom destination and route:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx ng-tablekit@latest add master-page --path src/app/admin/users --route users
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Optional project installation
|
|
64
|
+
|
|
65
|
+
To pin a CLI version for a team, install it as a development dependency:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm install --save-dev ng-tablekit
|
|
69
|
+
npx tablekit add report-page
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The `tablekit` executable is included as a convenient alias when the package is installed locally.
|
|
73
|
+
|
|
74
|
+
## Safe updates
|
|
75
|
+
|
|
76
|
+
Running the same command again is safe when generated files still match. If any target file has different content, the CLI stops and lists the conflicts. Use `--force` only when replacing those files is intentional.
|
|
77
|
+
|
|
78
|
+
## Column configuration
|
|
79
|
+
|
|
80
|
+
Columns opt in to sorting. A sorting icon and sort behavior are included only when `sortable: true` is present.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
columns = [
|
|
84
|
+
{ key: 'name', title: 'Name', sortable: true },
|
|
85
|
+
{ key: 'department', title: 'Department' },
|
|
86
|
+
{ key: 'status', title: 'Status', sortable: false, type: 'status' }
|
|
87
|
+
];
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Adding, removing, or reordering configured columns does not require table markup changes.
|
|
91
|
+
|
|
92
|
+
## Develop this package
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm install
|
|
96
|
+
npm run test:cli
|
|
97
|
+
npm run test:package
|
|
98
|
+
npm run verify:static
|
|
99
|
+
npm run build
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Release instructions are in [PUBLISHING.md](PUBLISHING.md).
|
package/bin/tablekit.mjs
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
writeFileSync
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import {
|
|
11
|
+
dirname,
|
|
12
|
+
isAbsolute,
|
|
13
|
+
join,
|
|
14
|
+
relative,
|
|
15
|
+
resolve,
|
|
16
|
+
sep
|
|
17
|
+
} from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
const cliPackage = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
|
|
22
|
+
const cliVersion = cliPackage.version;
|
|
23
|
+
const commonSourceRoot = join(packageRoot, 'src', 'app', 'core');
|
|
24
|
+
const commonFiles = [
|
|
25
|
+
'models/table.models.ts',
|
|
26
|
+
'config/table.config.ts',
|
|
27
|
+
'data/record.data.ts',
|
|
28
|
+
'utils/table.utils.ts'
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const templates = {
|
|
32
|
+
'report-page': {
|
|
33
|
+
className: 'ReportPageComponent',
|
|
34
|
+
componentFile: 'report-page.component',
|
|
35
|
+
files: [
|
|
36
|
+
'report-page.component.html',
|
|
37
|
+
'report-page.component.css',
|
|
38
|
+
'report-page.component.ts'
|
|
39
|
+
],
|
|
40
|
+
dependencies: {
|
|
41
|
+
'@ant-design/icons-angular': '^19.0.0',
|
|
42
|
+
'ng-zorro-antd': '^19.0.0',
|
|
43
|
+
'jspdf': '^3.0.4',
|
|
44
|
+
'xlsx': '^0.18.5'
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
'master-page': {
|
|
48
|
+
className: 'MasterPageComponent',
|
|
49
|
+
componentFile: 'master-page.component',
|
|
50
|
+
files: [
|
|
51
|
+
'master-page.component.html',
|
|
52
|
+
'master-page.component.css',
|
|
53
|
+
'master-page.component.ts',
|
|
54
|
+
'master-record-drawer.component.html',
|
|
55
|
+
'master-record-drawer.component.css',
|
|
56
|
+
'master-record-drawer.component.ts',
|
|
57
|
+
'master-map-drawer.component.html',
|
|
58
|
+
'master-map-drawer.component.css',
|
|
59
|
+
'master-map-drawer.component.ts'
|
|
60
|
+
],
|
|
61
|
+
dependencies: {
|
|
62
|
+
'@ant-design/icons-angular': '^19.0.0',
|
|
63
|
+
'ng-zorro-antd': '^19.0.0',
|
|
64
|
+
'jspdf': '^3.0.4',
|
|
65
|
+
'xlsx': '^0.18.5'
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function printHelp() {
|
|
71
|
+
console.log(`TableKit CLI
|
|
72
|
+
|
|
73
|
+
Usage:
|
|
74
|
+
npx ng-tablekit@latest list
|
|
75
|
+
npx ng-tablekit@latest add <report-page|master-page> [options]
|
|
76
|
+
|
|
77
|
+
Options:
|
|
78
|
+
--project <directory> Angular workspace root (default: nearest parent workspace)
|
|
79
|
+
--path <directory> Destination relative to the workspace
|
|
80
|
+
(default: src/app/pages/<template>)
|
|
81
|
+
--route <path> Route path (default: template name)
|
|
82
|
+
--skip-route Do not update src/app/app.routes.ts
|
|
83
|
+
--skip-install Update package.json without running the package manager
|
|
84
|
+
--force Replace conflicting generated files
|
|
85
|
+
-v, --version Show the installed CLI version
|
|
86
|
+
-h, --help Show this help
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
npx ng-tablekit@latest add report-page
|
|
90
|
+
npx ng-tablekit@latest add master-page --path src/app/admin/master-page
|
|
91
|
+
npx ng-tablekit@latest add report-page --skip-install
|
|
92
|
+
`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function fail(message) {
|
|
96
|
+
console.error(`\nTableKit: ${message}`);
|
|
97
|
+
process.exitCode = 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseArguments(argv) {
|
|
101
|
+
const options = {
|
|
102
|
+
command: argv[0],
|
|
103
|
+
templateName: argv[1],
|
|
104
|
+
project: null,
|
|
105
|
+
destination: null,
|
|
106
|
+
route: null,
|
|
107
|
+
skipRoute: false,
|
|
108
|
+
skipInstall: false,
|
|
109
|
+
force: false,
|
|
110
|
+
version: false,
|
|
111
|
+
help: false
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (argv.includes('--help') || argv.includes('-h')) options.help = true;
|
|
115
|
+
if (argv.includes('--version') || argv.includes('-v')) options.version = true;
|
|
116
|
+
|
|
117
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
118
|
+
const argument = argv[index];
|
|
119
|
+
if (argument === '--project' || argument === '--path' || argument === '--route') {
|
|
120
|
+
const value = argv[index + 1];
|
|
121
|
+
if (!value || value.startsWith('-')) throw new Error(`${argument} requires a value.`);
|
|
122
|
+
if (argument === '--project') options.project = value;
|
|
123
|
+
if (argument === '--path') options.destination = value;
|
|
124
|
+
if (argument === '--route') options.route = value;
|
|
125
|
+
index += 1;
|
|
126
|
+
} else if (argument === '--skip-route') {
|
|
127
|
+
options.skipRoute = true;
|
|
128
|
+
} else if (argument === '--skip-install') {
|
|
129
|
+
options.skipInstall = true;
|
|
130
|
+
} else if (argument === '--force') {
|
|
131
|
+
options.force = true;
|
|
132
|
+
} else if (argument === '--version' || argument === '-v') {
|
|
133
|
+
options.version = true;
|
|
134
|
+
} else if (argument === '--help' || argument === '-h') {
|
|
135
|
+
options.help = true;
|
|
136
|
+
} else {
|
|
137
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return options;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readJson(path, label) {
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw new Error(`Could not read ${label} at ${path}: ${error.message}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function writeJson(path, value) {
|
|
153
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function toImportPath(fromDirectory, targetWithoutExtension) {
|
|
157
|
+
let importPath = relative(fromDirectory, targetWithoutExtension).split(sep).join('/');
|
|
158
|
+
if (!importPath.startsWith('.')) importPath = `./${importPath}`;
|
|
159
|
+
return importPath;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isInside(parent, child) {
|
|
163
|
+
const value = relative(parent, child);
|
|
164
|
+
return value === '' || (!value.startsWith(`..${sep}`) && value !== '..' && !isAbsolute(value));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function findWorkspaceRoot(startDirectory) {
|
|
168
|
+
let current = resolve(startDirectory);
|
|
169
|
+
while (true) {
|
|
170
|
+
if (existsSync(join(current, 'angular.json')) && existsSync(join(current, 'package.json'))) return current;
|
|
171
|
+
const parent = dirname(current);
|
|
172
|
+
if (parent === current) return null;
|
|
173
|
+
current = parent;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function stripCatalogueCode(source, extension) {
|
|
178
|
+
let result = source;
|
|
179
|
+
if (extension === '.html') {
|
|
180
|
+
result = result.replace(/^\s*<div class="template-code-actions">[\s\S]*?<\/div>\s*/i, '');
|
|
181
|
+
}
|
|
182
|
+
if (extension === '.ts') {
|
|
183
|
+
result = result.replace(/import\s+{\s*CopyTemplateButtonComponent\s*}\s+from\s+['"][^'"]+['"];?\s*/g, '');
|
|
184
|
+
result = result.replace(/,\s*CopyTemplateButtonComponent\b/g, '');
|
|
185
|
+
result = result.replace(/\bCopyTemplateButtonComponent\s*,\s*/g, '');
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function prepareFiles(projectRoot, destinationRoot, templateName, force) {
|
|
191
|
+
const definition = templates[templateName];
|
|
192
|
+
const templateSourceRoot = join(packageRoot, 'src', 'app', 'templates', templateName);
|
|
193
|
+
const commonDestinationRoot = join(projectRoot, 'src', 'app', 'tablekit');
|
|
194
|
+
const operations = [];
|
|
195
|
+
|
|
196
|
+
for (const file of commonFiles) {
|
|
197
|
+
operations.push({
|
|
198
|
+
path: join(commonDestinationRoot, file),
|
|
199
|
+
content: readFileSync(join(commonSourceRoot, file), 'utf8')
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (const file of definition.files) {
|
|
204
|
+
const destinationPath = join(destinationRoot, file);
|
|
205
|
+
const extension = file.endsWith('.html') ? '.html' : file.endsWith('.css') ? '.css' : '.ts';
|
|
206
|
+
let content = readFileSync(join(templateSourceRoot, file), 'utf8');
|
|
207
|
+
content = stripCatalogueCode(content, extension);
|
|
208
|
+
|
|
209
|
+
if (extension === '.ts') {
|
|
210
|
+
content = content.replace(/@core\/([^'"]+)/g, (_match, corePath) => {
|
|
211
|
+
return toImportPath(dirname(destinationPath), join(commonDestinationRoot, corePath));
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
operations.push({ path: destinationPath, content });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const conflicts = operations.filter(operation => {
|
|
218
|
+
return existsSync(operation.path) && readFileSync(operation.path, 'utf8') !== operation.content;
|
|
219
|
+
});
|
|
220
|
+
if (conflicts.length > 0 && !force) {
|
|
221
|
+
const paths = conflicts.map(conflict => ` - ${relative(projectRoot, conflict.path)}`).join('\n');
|
|
222
|
+
throw new Error(`These files already exist with different content:\n${paths}\nRun again with --force to replace them.`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for (const operation of operations) {
|
|
226
|
+
mkdirSync(dirname(operation.path), { recursive: true });
|
|
227
|
+
if (!existsSync(operation.path) || force || readFileSync(operation.path, 'utf8') !== operation.content) {
|
|
228
|
+
writeFileSync(operation.path, operation.content, 'utf8');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return definition;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function updatePackageJson(projectRoot, definition) {
|
|
236
|
+
const packagePath = join(projectRoot, 'package.json');
|
|
237
|
+
const packageJson = readJson(packagePath, 'package.json');
|
|
238
|
+
packageJson.dependencies ??= {};
|
|
239
|
+
const added = [];
|
|
240
|
+
|
|
241
|
+
const angularVersion = packageJson.dependencies['@angular/core'] ?? packageJson.devDependencies?.['@angular/core'];
|
|
242
|
+
const required = { ...definition.dependencies };
|
|
243
|
+
if (angularVersion && !packageJson.dependencies['@angular/animations'] && !packageJson.devDependencies?.['@angular/animations']) {
|
|
244
|
+
required['@angular/animations'] = angularVersion;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const [name, version] of Object.entries(required)) {
|
|
248
|
+
if (!packageJson.dependencies[name] && !packageJson.devDependencies?.[name]) {
|
|
249
|
+
packageJson.dependencies[name] = version;
|
|
250
|
+
added.push(name);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
packageJson.dependencies = Object.fromEntries(Object.entries(packageJson.dependencies).sort(([left], [right]) => left.localeCompare(right)));
|
|
255
|
+
if (added.length > 0) writeJson(packagePath, packageJson);
|
|
256
|
+
return added;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function validateAngularWorkspace(projectRoot) {
|
|
260
|
+
const packageJson = readJson(join(projectRoot, 'package.json'), 'package.json');
|
|
261
|
+
const angularVersion = packageJson.dependencies?.['@angular/core'] ?? packageJson.devDependencies?.['@angular/core'];
|
|
262
|
+
if (!angularVersion) throw new Error('The target project does not declare @angular/core.');
|
|
263
|
+
const angularMajor = String(angularVersion).match(/\d+/)?.[0];
|
|
264
|
+
if (angularMajor !== '19') {
|
|
265
|
+
throw new Error(`This TableKit release supports Angular 19; the target declares @angular/core ${angularVersion}.`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const zorroVersion = packageJson.dependencies?.['ng-zorro-antd'] ?? packageJson.devDependencies?.['ng-zorro-antd'];
|
|
269
|
+
const zorroMajor = zorroVersion ? String(zorroVersion).match(/\d+/)?.[0] : null;
|
|
270
|
+
if (zorroMajor && zorroMajor !== '19') {
|
|
271
|
+
throw new Error(`The target declares ng-zorro-antd ${zorroVersion}; version 19 is required.`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function updateAngularStyles(projectRoot) {
|
|
276
|
+
const angularPath = join(projectRoot, 'angular.json');
|
|
277
|
+
const angularJson = readJson(angularPath, 'angular.json');
|
|
278
|
+
const projects = Object.values(angularJson.projects ?? {});
|
|
279
|
+
const application = projects.find(project => project.projectType === 'application') ?? projects[0];
|
|
280
|
+
const styles = application?.architect?.build?.options?.styles ?? application?.targets?.build?.options?.styles;
|
|
281
|
+
if (!Array.isArray(styles)) return false;
|
|
282
|
+
|
|
283
|
+
const zorroStyle = 'node_modules/ng-zorro-antd/ng-zorro-antd.min.css';
|
|
284
|
+
const hasStyle = styles.some(style => {
|
|
285
|
+
const stylePath = typeof style === 'string' ? style : style?.input;
|
|
286
|
+
if (stylePath === zorroStyle) return true;
|
|
287
|
+
if (!stylePath) return false;
|
|
288
|
+
const localStylePath = resolve(projectRoot, stylePath);
|
|
289
|
+
return existsSync(localStylePath) && readFileSync(localStylePath, 'utf8').includes('ng-zorro-antd');
|
|
290
|
+
});
|
|
291
|
+
if (!hasStyle) {
|
|
292
|
+
styles.unshift(zorroStyle);
|
|
293
|
+
writeJson(angularPath, angularJson);
|
|
294
|
+
}
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function updateAnimations(projectRoot) {
|
|
299
|
+
const configPath = join(projectRoot, 'src', 'app', 'app.config.ts');
|
|
300
|
+
if (!existsSync(configPath)) return false;
|
|
301
|
+
|
|
302
|
+
let content = readFileSync(configPath, 'utf8');
|
|
303
|
+
if (/provideAnimations(?:Async)?\s*\(/.test(content)) return true;
|
|
304
|
+
const newline = content.includes('\r\n') ? '\r\n' : '\n';
|
|
305
|
+
const coreImport = /import[^\n]+from ['"]@angular\/core['"];?\r?\n/;
|
|
306
|
+
if (!coreImport.test(content) || !/providers\s*:\s*\[/.test(content)) return false;
|
|
307
|
+
|
|
308
|
+
content = content.replace(coreImport, match => `${match}import { provideAnimations } from '@angular/platform-browser/animations';${newline}`);
|
|
309
|
+
content = content.replace(/providers\s*:\s*\[/, `providers: [${newline} provideAnimations(),`);
|
|
310
|
+
writeFileSync(configPath, content, 'utf8');
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function updateRoutes(projectRoot, destinationRoot, routePath, definition) {
|
|
315
|
+
const routesPath = join(projectRoot, 'src', 'app', 'app.routes.ts');
|
|
316
|
+
if (!existsSync(routesPath)) return false;
|
|
317
|
+
|
|
318
|
+
let content = readFileSync(routesPath, 'utf8');
|
|
319
|
+
const escapedRoute = routePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
320
|
+
const existingRoute = new RegExp(`^\\s*{\\s*path\\s*:\\s*['"]${escapedRoute}['"]`, 'm').exec(content);
|
|
321
|
+
if (existingRoute) {
|
|
322
|
+
const beforeRoute = content.slice(0, existingRoute.index);
|
|
323
|
+
const previousTokenIndex = beforeRoute.search(/\S(?=\s*$)/);
|
|
324
|
+
if (previousTokenIndex >= 0 && beforeRoute[previousTokenIndex] === '}') {
|
|
325
|
+
content = `${beforeRoute.slice(0, previousTokenIndex + 1)},${beforeRoute.slice(previousTokenIndex + 1)}${content.slice(existingRoute.index)}`;
|
|
326
|
+
writeFileSync(routesPath, content, 'utf8');
|
|
327
|
+
}
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const componentFile = join(destinationRoot, definition.componentFile);
|
|
332
|
+
const importPath = toImportPath(dirname(routesPath), componentFile);
|
|
333
|
+
const routeLine = ` { path: '${routePath}', loadComponent: () => import('${importPath}').then(m => m.${definition.className}) },`;
|
|
334
|
+
const wildcard = /^(\s*){\s*path\s*:\s*['"]\*\*['"]/m;
|
|
335
|
+
if (wildcard.test(content)) {
|
|
336
|
+
content = content.replace(wildcard, `${routeLine}\n$&`);
|
|
337
|
+
} else {
|
|
338
|
+
const arrayEnd = content.lastIndexOf('];');
|
|
339
|
+
if (arrayEnd < 0) return false;
|
|
340
|
+
let beforeArrayEnd = content.slice(0, arrayEnd);
|
|
341
|
+
const previousTokenIndex = beforeArrayEnd.search(/\S(?=\s*$)/);
|
|
342
|
+
if (previousTokenIndex >= 0 && beforeArrayEnd[previousTokenIndex] === '}') {
|
|
343
|
+
beforeArrayEnd = `${beforeArrayEnd.slice(0, previousTokenIndex + 1)},${beforeArrayEnd.slice(previousTokenIndex + 1)}`;
|
|
344
|
+
}
|
|
345
|
+
content = `${beforeArrayEnd}${routeLine}\n${content.slice(arrayEnd)}`;
|
|
346
|
+
}
|
|
347
|
+
writeFileSync(routesPath, content, 'utf8');
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function detectPackageManager(projectRoot) {
|
|
352
|
+
if (existsSync(join(projectRoot, 'pnpm-lock.yaml'))) return { command: 'pnpm', args: ['install'] };
|
|
353
|
+
if (existsSync(join(projectRoot, 'yarn.lock'))) return { command: 'yarn', args: ['install'] };
|
|
354
|
+
if (existsSync(join(projectRoot, 'bun.lockb')) || existsSync(join(projectRoot, 'bun.lock'))) return { command: 'bun', args: ['install'] };
|
|
355
|
+
return { command: 'npm', args: ['install'] };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function installDependencies(projectRoot) {
|
|
359
|
+
const manager = detectPackageManager(projectRoot);
|
|
360
|
+
console.log(`\nInstalling dependencies with ${manager.command}...`);
|
|
361
|
+
const result = spawnSync(manager.command, manager.args, {
|
|
362
|
+
cwd: projectRoot,
|
|
363
|
+
stdio: 'inherit',
|
|
364
|
+
shell: process.platform === 'win32'
|
|
365
|
+
});
|
|
366
|
+
if (result.error) throw result.error;
|
|
367
|
+
if (result.status !== 0) throw new Error(`${manager.command} install exited with code ${result.status}.`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function addTemplate(options) {
|
|
371
|
+
const templateName = options.templateName;
|
|
372
|
+
if (!templateName || !templates[templateName]) {
|
|
373
|
+
const oldNameHint = templateName === 'report-with-pdf'
|
|
374
|
+
? ' Use report-page instead.'
|
|
375
|
+
: templateName === 'master-listing'
|
|
376
|
+
? ' Use master-page instead.'
|
|
377
|
+
: '';
|
|
378
|
+
throw new Error(`Choose report-page or master-page.${oldNameHint}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const projectRoot = options.project ? resolve(options.project) : findWorkspaceRoot(process.cwd());
|
|
382
|
+
if (!projectRoot) {
|
|
383
|
+
throw new Error('Could not find an Angular workspace in the current directory or its parents. Use --project <directory>.');
|
|
384
|
+
}
|
|
385
|
+
if (!existsSync(join(projectRoot, 'angular.json')) || !existsSync(join(projectRoot, 'package.json'))) {
|
|
386
|
+
throw new Error(`${projectRoot} is not an Angular workspace.`);
|
|
387
|
+
}
|
|
388
|
+
validateAngularWorkspace(projectRoot);
|
|
389
|
+
|
|
390
|
+
const destinationRoot = resolve(projectRoot, options.destination ?? join('src', 'app', 'pages', templateName));
|
|
391
|
+
if (!isInside(projectRoot, destinationRoot)) throw new Error('The destination must stay inside the Angular workspace.');
|
|
392
|
+
|
|
393
|
+
const routePath = (options.route ?? templateName).replace(/^\/+|\/+$/g, '');
|
|
394
|
+
if (!routePath || !/^[a-z0-9][a-z0-9-]*$/.test(routePath)) {
|
|
395
|
+
throw new Error('The route must contain lowercase letters, numbers, or hyphens.');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
console.log(`\nAdding ${templateName} to ${relative(projectRoot, destinationRoot)}...`);
|
|
399
|
+
const definition = prepareFiles(projectRoot, destinationRoot, templateName, options.force);
|
|
400
|
+
const addedDependencies = updatePackageJson(projectRoot, definition);
|
|
401
|
+
const stylesUpdated = updateAngularStyles(projectRoot);
|
|
402
|
+
const animationsUpdated = updateAnimations(projectRoot);
|
|
403
|
+
const routeUpdated = options.skipRoute ? true : updateRoutes(projectRoot, destinationRoot, routePath, definition);
|
|
404
|
+
|
|
405
|
+
if (addedDependencies.length > 0 && !options.skipInstall) installDependencies(projectRoot);
|
|
406
|
+
|
|
407
|
+
console.log(`\n✓ Added ${templateName}`);
|
|
408
|
+
console.log(`✓ Page files: ${relative(projectRoot, destinationRoot)}`);
|
|
409
|
+
console.log('✓ Shared runtime: src/app/tablekit');
|
|
410
|
+
console.log(options.skipRoute ? '– Route update skipped' : routeUpdated ? `✓ Route: /${routePath}` : '⚠ Could not update app.routes.ts; add the route manually.');
|
|
411
|
+
console.log(stylesUpdated ? '✓ NG-ZORRO stylesheet configured' : '⚠ Could not find the Angular build styles array.');
|
|
412
|
+
console.log(animationsUpdated ? '✓ Angular animations configured' : '⚠ Could not update app.config.ts; ensure animations are provided.');
|
|
413
|
+
if (addedDependencies.length === 0) {
|
|
414
|
+
console.log('✓ Required dependencies already present');
|
|
415
|
+
} else if (options.skipInstall) {
|
|
416
|
+
console.log(`✓ Added dependencies to package.json: ${addedDependencies.join(', ')}`);
|
|
417
|
+
console.log(' Run your package manager install command before starting the app.');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
const options = parseArguments(process.argv.slice(2));
|
|
423
|
+
if (options.version) {
|
|
424
|
+
console.log(cliVersion);
|
|
425
|
+
} else if (options.help || !options.command) {
|
|
426
|
+
printHelp();
|
|
427
|
+
} else if (options.command === 'list') {
|
|
428
|
+
console.log('Available templates:\n report-page\n master-page');
|
|
429
|
+
} else if (options.command === 'add') {
|
|
430
|
+
addTemplate(options);
|
|
431
|
+
} else {
|
|
432
|
+
throw new Error(`Unknown command: ${options.command}`);
|
|
433
|
+
}
|
|
434
|
+
} catch (error) {
|
|
435
|
+
fail(error.message);
|
|
436
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ng-tablekit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Cross-platform CLI for adding Angular 19 and NG-ZORRO report and master table pages.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"angular",
|
|
9
|
+
"angular-19",
|
|
10
|
+
"ng-zorro",
|
|
11
|
+
"table",
|
|
12
|
+
"report",
|
|
13
|
+
"cli",
|
|
14
|
+
"scaffold"
|
|
15
|
+
],
|
|
16
|
+
"bin": {
|
|
17
|
+
"ng-tablekit": "bin/tablekit.mjs",
|
|
18
|
+
"tablekit": "bin/tablekit.mjs"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"bin",
|
|
22
|
+
"src/app/core/config/table.config.ts",
|
|
23
|
+
"src/app/core/data/record.data.ts",
|
|
24
|
+
"src/app/core/models/table.models.ts",
|
|
25
|
+
"src/app/core/utils/table.utils.ts",
|
|
26
|
+
"src/app/templates/report-page",
|
|
27
|
+
"src/app/templates/master-page",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"PUBLISHING.md"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18.19"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"ng": "ng",
|
|
40
|
+
"start": "ng serve",
|
|
41
|
+
"build": "ng build --configuration production",
|
|
42
|
+
"test": "ng test --watch=false --browsers=ChromeHeadless",
|
|
43
|
+
"e2e": "playwright test",
|
|
44
|
+
"verify:static": "node scripts/verify-project.mjs",
|
|
45
|
+
"tablekit": "node ./bin/tablekit.mjs",
|
|
46
|
+
"test:cli": "node --test tests/tablekit-cli.test.mjs",
|
|
47
|
+
"test:package": "node --test tests/tablekit-package.test.mjs",
|
|
48
|
+
"release:check": "node scripts/release-check.mjs",
|
|
49
|
+
"prepack": "npm run release:check"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@angular/animations": "19.2.25",
|
|
53
|
+
"@angular/cdk": "19.2.19",
|
|
54
|
+
"@angular/common": "19.2.25",
|
|
55
|
+
"@angular/compiler": "19.2.25",
|
|
56
|
+
"@angular/compiler-cli": "19.2.25",
|
|
57
|
+
"@angular/core": "19.2.25",
|
|
58
|
+
"@angular/forms": "19.2.25",
|
|
59
|
+
"@angular/platform-browser": "19.2.25",
|
|
60
|
+
"@angular/platform-browser-dynamic": "19.2.25",
|
|
61
|
+
"@angular/router": "19.2.25",
|
|
62
|
+
"@angular-devkit/build-angular": "19.2.27",
|
|
63
|
+
"@angular/cli": "19.2.27",
|
|
64
|
+
"@ant-design/icons-angular": "19.0.0",
|
|
65
|
+
"@playwright/test": "^1.55.0",
|
|
66
|
+
"@types/jasmine": "~5.1.5",
|
|
67
|
+
"jasmine-core": "~5.6.0",
|
|
68
|
+
"jspdf": "^3.0.4",
|
|
69
|
+
"karma": "~6.4.4",
|
|
70
|
+
"karma-chrome-launcher": "~3.2.0",
|
|
71
|
+
"karma-coverage": "~2.2.1",
|
|
72
|
+
"karma-jasmine": "~5.1.0",
|
|
73
|
+
"karma-jasmine-html-reporter": "~2.1.0",
|
|
74
|
+
"ng-zorro-antd": "19.2.0",
|
|
75
|
+
"rxjs": "~7.8.2",
|
|
76
|
+
"tslib": "^2.8.1",
|
|
77
|
+
"typescript": "~5.7.3",
|
|
78
|
+
"xlsx": "^0.18.5",
|
|
79
|
+
"zone.js": "~0.15.0"
|
|
80
|
+
}
|
|
81
|
+
}
|