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 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).
@@ -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
+ }