@researchdatabox/redbox-dev-tools 0.0.1 → 5.0.0-RC1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +259 -14
  2. package/bin/cli.js +3 -0
  3. package/bin/run-hook-mocha.js +3 -0
  4. package/bin/run-hook-tsc.js +3 -0
  5. package/bin/wrapper-webpack.js +168 -0
  6. package/config/tsconfig.base.json +16 -0
  7. package/dist/bin/run-hook-mocha.d.ts +1 -0
  8. package/dist/bin/run-hook-mocha.js +49 -0
  9. package/dist/bin/run-hook-tsc.d.ts +1 -0
  10. package/dist/bin/run-hook-tsc.js +19 -0
  11. package/dist/cli-parsers.d.ts +9 -0
  12. package/dist/cli-parsers.js +131 -0
  13. package/dist/cli.d.ts +1 -0
  14. package/dist/cli.js +495 -0
  15. package/dist/commands/form-config.d.ts +6 -0
  16. package/dist/commands/form-config.js +401 -0
  17. package/dist/commands/sync-translation.d.ts +23 -0
  18. package/dist/commands/sync-translation.js +348 -0
  19. package/dist/completion/index.d.ts +1 -0
  20. package/dist/completion/index.js +218 -0
  21. package/dist/generators/add-method.d.ts +27 -0
  22. package/dist/generators/add-method.js +235 -0
  23. package/dist/generators/angular-app.d.ts +24 -0
  24. package/dist/generators/angular-app.js +342 -0
  25. package/dist/generators/angular-service.d.ts +21 -0
  26. package/dist/generators/angular-service.js +154 -0
  27. package/dist/generators/controller.d.ts +40 -0
  28. package/dist/generators/controller.js +195 -0
  29. package/dist/generators/form-component.d.ts +46 -0
  30. package/dist/generators/form-component.js +906 -0
  31. package/dist/generators/form-field.d.ts +15 -0
  32. package/dist/generators/form-field.js +160 -0
  33. package/dist/generators/migration.d.ts +15 -0
  34. package/dist/generators/migration.js +100 -0
  35. package/dist/generators/model.d.ts +42 -0
  36. package/dist/generators/model.js +206 -0
  37. package/dist/generators/service.d.ts +16 -0
  38. package/dist/generators/service.js +111 -0
  39. package/dist/runtime-resolver.d.ts +3 -0
  40. package/dist/runtime-resolver.js +27 -0
  41. package/dist/templates/hook-archetype.d.ts +12 -0
  42. package/dist/templates/hook-archetype.js +187 -0
  43. package/dist/testing.d.ts +7 -0
  44. package/dist/testing.js +53 -0
  45. package/dist/utils/config-helper.d.ts +71 -0
  46. package/dist/utils/config-helper.js +574 -0
  47. package/dist/utils/generator.d.ts +10 -0
  48. package/dist/utils/generator.js +68 -0
  49. package/dist/utils/hook-tsconfig.d.ts +9 -0
  50. package/dist/utils/hook-tsconfig.js +104 -0
  51. package/dist/utils/paths.d.ts +10 -0
  52. package/dist/utils/paths.js +60 -0
  53. package/index.js +4 -0
  54. package/package.json +76 -19
  55. package/runtime-resolver.d.ts +1 -0
  56. package/runtime-resolver.js +3 -0
  57. package/skills/redbox-angular-apps/SKILL.md +42 -0
  58. package/skills/redbox-angular-services/SKILL.md +29 -0
  59. package/skills/redbox-controllers/SKILL.md +65 -0
  60. package/skills/redbox-feature-design-planner/SKILL.md +169 -0
  61. package/skills/redbox-feature-implementation-review/SKILL.md +117 -0
  62. package/skills/redbox-form-components/SKILL.md +114 -0
  63. package/skills/redbox-form-config/SKILL.md +34 -0
  64. package/skills/redbox-hook-development/SKILL.md +66 -0
  65. package/skills/redbox-services/SKILL.md +54 -0
  66. package/skills/redbox-test-verification/SKILL.md +38 -0
  67. package/skills/redbox-testing/SKILL.md +64 -0
  68. package/skills/redbox-web-verification/SKILL.md +71 -0
  69. package/skills/redbox-web-verification/redbox-angular-apps/SKILL.md +48 -0
  70. package/skills/redbox-web-verification/redbox-angular-services/SKILL.md +30 -0
  71. package/skills/redbox-web-verification/redbox-controllers/SKILL.md +56 -0
  72. package/skills/redbox-web-verification/redbox-form-config/SKILL.md +29 -0
  73. package/skills/redbox-web-verification/redbox-hook-development/SKILL.md +61 -0
  74. package/skills/redbox-web-verification/redbox-services/SKILL.md +45 -0
  75. package/skills/redbox-web-verification/redbox-testing/SKILL.md +53 -0
  76. package/skills/redbox-web-verification/redbox-web-verification/SKILL.md +47 -0
  77. package/templates/hook-archetype/standard/.gitignore.hbs +5 -0
  78. package/templates/hook-archetype/standard/.npmignore.hbs +4 -0
  79. package/templates/hook-archetype/standard/.npmrc.hbs +6 -0
  80. package/templates/hook-archetype/standard/.nvmrc.hbs +1 -0
  81. package/templates/hook-archetype/standard/AGENTS.md.hbs +51 -0
  82. package/templates/hook-archetype/standard/Dockerfile.hbs +29 -0
  83. package/templates/hook-archetype/standard/README.md.hbs +57 -0
  84. package/templates/hook-archetype/standard/assets/default/.gitkeep +1 -0
  85. package/templates/hook-archetype/standard/assets/fonts/.gitkeep +1 -0
  86. package/templates/hook-archetype/standard/assets/images/.gitkeep +1 -0
  87. package/templates/hook-archetype/standard/assets/styles/.gitkeep +1 -0
  88. package/templates/hook-archetype/standard/language-defaults/.gitkeep +1 -0
  89. package/templates/hook-archetype/standard/language-defaults/README.md.hbs +10 -0
  90. package/templates/hook-archetype/standard/language-defaults/en/translation.json.hbs +3 -0
  91. package/templates/hook-archetype/standard/language-defaults/meta.json.hbs +6 -0
  92. package/templates/hook-archetype/standard/package.json.hbs +44 -0
  93. package/templates/hook-archetype/standard/src/api/controllers/ExampleController.ts.hbs +21 -0
  94. package/templates/hook-archetype/standard/src/api/controllers/index.ts.hbs +23 -0
  95. package/templates/hook-archetype/standard/src/api/services/ExampleService.ts.hbs +22 -0
  96. package/templates/hook-archetype/standard/src/api/services/index.ts.hbs +23 -0
  97. package/templates/hook-archetype/standard/src/config/brandingConfigurationDefaults.ts.hbs +9 -0
  98. package/templates/hook-archetype/standard/src/config/reusableFormDefinitions.ts.hbs +6 -0
  99. package/templates/hook-archetype/standard/src/form-config/default-1.0-draft.ts.hbs +20 -0
  100. package/templates/hook-archetype/standard/src/form-config/index.ts.hbs +8 -0
  101. package/templates/hook-archetype/standard/src/index.ts.hbs +55 -0
  102. package/templates/hook-archetype/standard/src/migrations/index.ts.hbs +19 -0
  103. package/templates/hook-archetype/standard/support/bootstrap-data/.gitkeep +1 -0
  104. package/templates/hook-archetype/standard/support/bootstrap-data/namedqueries/.gitkeep +1 -0
  105. package/templates/hook-archetype/standard/support/bootstrap-data/records/.gitkeep +1 -0
  106. package/templates/hook-archetype/standard/support/bootstrap-data/reports/.gitkeep +1 -0
  107. package/templates/hook-archetype/standard/support/bootstrap-data/vocabularies/.gitkeep +1 -0
  108. package/templates/hook-archetype/standard/support/development/core-mount.env.hbs +3 -0
  109. package/templates/hook-archetype/standard/support/development/docker-compose.core-mount.yml.hbs +37 -0
  110. package/templates/hook-archetype/standard/support/development/docker-compose.yml.hbs +108 -0
  111. package/templates/hook-archetype/standard/support/development/prepare-core-mount.sh.hbs +61 -0
  112. package/templates/hook-archetype/standard/support/integration-testing/docker-compose.mocha.yml.hbs +118 -0
  113. package/templates/hook-archetype/standard/support/integration-testing/run-mocha-hook.sh.hbs +14 -0
  114. package/templates/hook-archetype/standard/support/integration-testing/run-mocha-redbox.sh.hbs +100 -0
  115. package/templates/hook-archetype/standard/support/prepare-minio.sh.hbs +74 -0
  116. package/templates/hook-archetype/standard/test/integration/hook-registration.test.ts.hbs +15 -0
  117. package/templates/hook-archetype/standard/test/support/globals.ts.hbs +40 -0
  118. package/templates/hook-archetype/standard/test/tsconfig.json.hbs +8 -0
  119. package/templates/hook-archetype/standard/test/unit/.mocharc.cjs.hbs +9 -0
  120. package/templates/hook-archetype/standard/test/unit/index.test.ts.hbs +46 -0
  121. package/templates/hook-archetype/standard/tsconfig.json.hbs +15 -0
  122. package/templates/hook-archetype/standard/views/.gitkeep +1 -0
  123. package/templates/hook-archetype/standard/views/default/.gitkeep +1 -0
  124. package/testing.d.ts +1 -0
  125. package/testing.js +3 -0
  126. package/.travis.yml +0 -23
  127. package/commands/initProject.js +0 -129
  128. package/redbox-dev-tools +0 -70
  129. package/yarn.lock +0 -1061
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.AddMethodGenerator = void 0;
37
+ const path = __importStar(require("path"));
38
+ const fs = __importStar(require("fs"));
39
+ const ts_morph_1 = require("ts-morph");
40
+ const generator_1 = require("../utils/generator");
41
+ const config_helper_1 = require("../utils/config-helper");
42
+ class AddMethodGenerator extends generator_1.Generator {
43
+ filePath;
44
+ method;
45
+ route;
46
+ http;
47
+ auth;
48
+ navigation;
49
+ languageDefaults;
50
+ paths;
51
+ project;
52
+ constructor(options) {
53
+ super(options);
54
+ this.filePath = path.isAbsolute(options.file) ? options.file : path.resolve(this.root, options.file);
55
+ this.method = options.method;
56
+ this.route = options.route;
57
+ this.http = options.http || 'GET';
58
+ this.auth = options.auth;
59
+ this.navigation = options.navigation || [];
60
+ this.languageDefaults = options.languageDefaults || [];
61
+ this.paths = options.paths;
62
+ this.project = new ts_morph_1.Project({
63
+ manipulationSettings: {
64
+ quoteKind: ts_morph_1.QuoteKind.Single,
65
+ },
66
+ });
67
+ }
68
+ async generate() {
69
+ if (!fs.existsSync(this.filePath)) {
70
+ throw new Error(`File not found: ${this.filePath}`);
71
+ }
72
+ const sourceFile = this.project.addSourceFileAtPath(this.filePath);
73
+ const relativePath = path.relative(this.root, this.filePath);
74
+ // 1. Identify if it's a Controller or Service
75
+ const isController = this.filePath.endsWith('Controller.ts');
76
+ const isService = this.filePath.endsWith('Service.ts');
77
+ if (!isController && !isService) {
78
+ throw new Error('Target file must be a Controller or Service (.ts)');
79
+ }
80
+ // 2. Find the class
81
+ const targetClass = sourceFile.getFirstDescendantByKind(ts_morph_1.SyntaxKind.ClassDeclaration);
82
+ if (!targetClass) {
83
+ throw new Error('Could not find a class in the target file');
84
+ }
85
+ this.updateClass(targetClass, isController);
86
+ sourceFile.formatText();
87
+ if (this.dryRun) {
88
+ console.log(` [UPDATE] ${relativePath} (dry run)`);
89
+ }
90
+ else {
91
+ await sourceFile.save();
92
+ console.log(` [UPDATE] ${relativePath}`);
93
+ }
94
+ const fileName = path.basename(this.filePath, '.ts');
95
+ const className = isController ? fileName.replace('Controller', '') : fileName.replace('Service', '');
96
+ if (!this.dryRun) {
97
+ if (isController) {
98
+ await (0, config_helper_1.updateControllerIndex)({
99
+ project: this.project,
100
+ paths: this.paths,
101
+ root: this.root,
102
+ controllerName: fileName,
103
+ className: className,
104
+ webservice: this.filePath.includes(path.join('controllers', 'webservice')),
105
+ dryRun: this.dryRun
106
+ });
107
+ }
108
+ else {
109
+ await (0, config_helper_1.updateServiceIndex)({
110
+ project: this.project,
111
+ paths: this.paths,
112
+ root: this.root,
113
+ serviceName: fileName,
114
+ className: className,
115
+ dryRun: this.dryRun
116
+ });
117
+ }
118
+ }
119
+ // 3. Update routes and auth if it's a controller and route is provided
120
+ if (isController && this.route) {
121
+ const trimmedRoute = this.route.trim();
122
+ if (!trimmedRoute) {
123
+ throw new Error('Route cannot be empty or whitespace.');
124
+ }
125
+ const controllerName = fileName;
126
+ const webservice = this.filePath.includes(path.join('controllers', 'webservice'));
127
+ await (0, config_helper_1.updateRoutes)({
128
+ project: this.project,
129
+ paths: this.paths,
130
+ root: this.root,
131
+ route: trimmedRoute,
132
+ controllerName: controllerName,
133
+ action: this.method,
134
+ webservice: webservice,
135
+ dryRun: this.dryRun
136
+ });
137
+ if (this.auth && this.auth.length > 0) {
138
+ await (0, config_helper_1.updateAuth)({
139
+ project: this.project,
140
+ paths: this.paths,
141
+ root: this.root,
142
+ route: trimmedRoute,
143
+ auth: this.auth,
144
+ dryRun: this.dryRun
145
+ });
146
+ }
147
+ if (this.navigation.length > 0) {
148
+ const routePath = trimmedRoute.split(' ').pop();
149
+ if (!routePath) {
150
+ throw new Error(`Route is missing a path segment: '${trimmedRoute}'.`);
151
+ }
152
+ for (const nav of this.navigation) {
153
+ const actionName = nav.action || this.method;
154
+ if (actionName !== this.method) {
155
+ console.warn(` [NAV] Navigation mapping action '${actionName}' does not match method '${this.method}'. Skipping.`);
156
+ continue;
157
+ }
158
+ await (0, config_helper_1.updateNavigationConfig)({
159
+ project: this.project,
160
+ paths: this.paths,
161
+ root: this.root,
162
+ mapping: {
163
+ ...nav,
164
+ href: routePath
165
+ },
166
+ dryRun: this.dryRun
167
+ });
168
+ }
169
+ }
170
+ }
171
+ if (this.languageDefaults.length > 0) {
172
+ await (0, config_helper_1.updateLanguageDefaults)({
173
+ root: this.root,
174
+ entries: this.languageDefaults,
175
+ dryRun: this.dryRun
176
+ });
177
+ }
178
+ }
179
+ updateClass(klass, isController) {
180
+ // 1. Add method stub if it doesn't exist
181
+ if (!klass.getMethod(this.method)) {
182
+ if (isController) {
183
+ klass.addMethod({
184
+ name: this.method,
185
+ scope: ts_morph_1.Scope.Public,
186
+ isAsync: true,
187
+ returnType: 'Promise<unknown>',
188
+ parameters: [
189
+ { name: 'req', type: 'Sails.Req' },
190
+ { name: 'res', type: 'Sails.Res' }
191
+ ],
192
+ statements: `return this.sendResp(req, res, { data: { message: 'Action ${this.method} in ${klass.getName()}' } });`
193
+ });
194
+ }
195
+ else {
196
+ klass.addMethod({
197
+ name: this.method,
198
+ scope: ts_morph_1.Scope.Public,
199
+ isAsync: true,
200
+ returnType: 'Promise<Record<string, unknown>>',
201
+ parameters: [
202
+ { name: '..._args', type: 'unknown[]' }
203
+ ],
204
+ statements: [
205
+ `sails.log.verbose(\`\${this.logHeader} ${this.method} called\`);`,
206
+ `return { message: 'Method ${this.method} in ${klass.getName()}' };`
207
+ ]
208
+ });
209
+ }
210
+ }
211
+ // 2. Update _exportedMethods
212
+ const exportedMethodsProp = klass.getProperty('_exportedMethods');
213
+ if (exportedMethodsProp) {
214
+ const initializer = exportedMethodsProp.getInitializerIfKind(ts_morph_1.SyntaxKind.ArrayLiteralExpression);
215
+ if (initializer) {
216
+ if (!initializer.getElements().some((e) => e.getText().replace(/['"]/g, '') === this.method)) {
217
+ initializer.addElement(`'${this.method}'`);
218
+ }
219
+ }
220
+ }
221
+ else {
222
+ // Create _exportedMethods if missing
223
+ const hasInit = !!klass.getMethod('init');
224
+ const methods = hasInit ? ['init', this.method] : [this.method];
225
+ klass.addProperty({
226
+ name: '_exportedMethods',
227
+ scope: ts_morph_1.Scope.Protected,
228
+ hasOverrideKeyword: true,
229
+ type: 'string[]',
230
+ initializer: `[${methods.map(m => `'${m}'`).join(', ')}]`
231
+ });
232
+ }
233
+ }
234
+ }
235
+ exports.AddMethodGenerator = AddMethodGenerator;
@@ -0,0 +1,24 @@
1
+ import { Generator, GeneratorOptions } from '../utils/generator';
2
+ import { RedboxPaths } from '../utils/paths';
3
+ export interface AngularAppGeneratorOptions extends GeneratorOptions {
4
+ name: string;
5
+ ejsView: string;
6
+ auth?: string[];
7
+ paths: RedboxPaths;
8
+ }
9
+ export declare class AngularAppGenerator extends Generator {
10
+ private name;
11
+ private ejsView;
12
+ private auth?;
13
+ private paths;
14
+ private project;
15
+ constructor(options: AngularAppGeneratorOptions);
16
+ generate(): Promise<void>;
17
+ private scaffoldAngularApp;
18
+ private updateAngularJson;
19
+ private generateEjsView;
20
+ private updateRouteConfig;
21
+ private updateAuthConfig;
22
+ private toClassName;
23
+ private getSafeEjsViewName;
24
+ }
@@ -0,0 +1,342 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.AngularAppGenerator = void 0;
37
+ const path = __importStar(require("path"));
38
+ const fs = __importStar(require("fs"));
39
+ const ts_morph_1 = require("ts-morph");
40
+ const generator_1 = require("../utils/generator");
41
+ const config_helper_1 = require("../utils/config-helper");
42
+ class AngularAppGenerator extends generator_1.Generator {
43
+ name;
44
+ ejsView;
45
+ auth;
46
+ paths;
47
+ project;
48
+ constructor(options) {
49
+ super(options);
50
+ this.name = options.name.toLowerCase();
51
+ this.ejsView = options.ejsView;
52
+ this.auth = options.auth;
53
+ this.paths = options.paths;
54
+ this.project = new ts_morph_1.Project();
55
+ }
56
+ async generate() {
57
+ // 1. Scaffold Angular app
58
+ this.scaffoldAngularApp();
59
+ // 2. Update angular.json
60
+ this.updateAngularJson();
61
+ // 3. Generate EJS view
62
+ this.generateEjsView();
63
+ // 4. Update route config
64
+ await this.updateRouteConfig();
65
+ // 5. Update auth config
66
+ if (this.auth && this.auth.length > 0) {
67
+ await this.updateAuthConfig();
68
+ }
69
+ }
70
+ scaffoldAngularApp() {
71
+ const projectRoot = path.join(this.paths.angular, 'projects', 'researchdatabox', this.name);
72
+ const srcRoot = path.join(projectRoot, 'src');
73
+ const appRoot = path.join(srcRoot, 'app');
74
+ const files = {
75
+ 'tsconfig.app.json': JSON.stringify({
76
+ "extends": "../../../tsconfig.json",
77
+ "compilerOptions": {
78
+ "outDir": "../../../dist/out-tsc",
79
+ "types": []
80
+ },
81
+ "files": [
82
+ "src/main.ts"
83
+ ],
84
+ "include": [
85
+ "src/**/*.d.ts"
86
+ ]
87
+ }, null, 2),
88
+ 'tsconfig.spec.json': JSON.stringify({
89
+ "extends": "../../../tsconfig.json",
90
+ "compilerOptions": {
91
+ "outDir": "../../../dist/out-tsc",
92
+ "types": [
93
+ "jasmine"
94
+ ]
95
+ },
96
+ "include": [
97
+ "src/**/*.spec.ts",
98
+ "src/**/*.d.ts"
99
+ ]
100
+ }, null, 2),
101
+ 'src/main.ts': `import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
102
+ import { ${this.toClassName(this.name)}Module } from './app/${this.name}.module';
103
+
104
+ platformBrowserDynamic().bootstrapModule(${this.toClassName(this.name)}Module)
105
+ .catch(err => console.error(err));
106
+ `,
107
+ 'src/styles.scss': `/* Add application styles & imports to this file! */\n`,
108
+ 'src/favicon.ico': '',
109
+ 'src/assets/.gitkeep': '',
110
+ 'src/index.html': `<!doctype html>
111
+ <html lang="en">
112
+ <head>
113
+ <meta charset="utf-8">
114
+ <title>${this.toClassName(this.name)}</title>
115
+ <base href="/">
116
+ <meta name="viewport" content="width=device-width, initial-scale=1">
117
+ <link rel="icon" type="image/x-icon" href="favicon.ico">
118
+ </head>
119
+ <body>
120
+ <${this.name}></${this.name}>
121
+ </body>
122
+ </html>
123
+ `,
124
+ [`src/app/${this.name}.module.ts`]: `import { NgModule } from '@angular/core';
125
+ import { BrowserModule } from '@angular/platform-browser';
126
+ import { APP_BASE_HREF, PlatformLocation, CommonModule } from '@angular/common';
127
+ import { RedboxPortalCoreModule, trimLastSlashFromUrl } from '@researchdatabox/portal-ng-common';
128
+ import { ${this.toClassName(this.name)}Component } from './${this.name}.component';
129
+
130
+ @NgModule({
131
+ declarations: [
132
+ ${this.toClassName(this.name)}Component
133
+ ],
134
+ imports: [
135
+ BrowserModule,
136
+ CommonModule,
137
+ RedboxPortalCoreModule
138
+ ],
139
+ providers: [
140
+ {
141
+ provide: APP_BASE_HREF,
142
+ useFactory: (s: PlatformLocation) => trimLastSlashFromUrl(s.getBaseHrefFromDOM()),
143
+ deps: [PlatformLocation]
144
+ }
145
+ ],
146
+ bootstrap: [${this.toClassName(this.name)}Component]
147
+ })
148
+ export class ${this.toClassName(this.name)}Module { }
149
+ `,
150
+ [`src/app/${this.name}.component.ts`]: `import { Component } from '@angular/core';
151
+
152
+ @Component({
153
+ selector: '${this.name}',
154
+ templateUrl: './${this.name}.component.html',
155
+ styleUrls: ['./${this.name}.component.scss'],
156
+ standalone: false
157
+ })
158
+ export class ${this.toClassName(this.name)}Component {
159
+ title = '${this.name}';
160
+ }
161
+ `,
162
+ [`src/app/${this.name}.component.html`]: `<div class="container-fluid">
163
+ <h1>${this.toClassName(this.name)} App</h1>
164
+ <p>${this.name} works!</p>
165
+ </div>
166
+ `,
167
+ [`src/app/${this.name}.component.scss`]: ``,
168
+ };
169
+ for (const [relPath, content] of Object.entries(files)) {
170
+ this.writeFile(path.join(projectRoot, relPath), content);
171
+ }
172
+ }
173
+ updateAngularJson() {
174
+ const angularJsonPath = path.join(this.paths.angular, 'angular.json');
175
+ if (!fs.existsSync(angularJsonPath)) {
176
+ console.warn(`Warning: Could not find angular.json at ${angularJsonPath}`);
177
+ return;
178
+ }
179
+ let angularJson;
180
+ try {
181
+ angularJson = JSON.parse(fs.readFileSync(angularJsonPath, 'utf-8'));
182
+ }
183
+ catch (error) {
184
+ const message = error instanceof Error ? error.message : String(error);
185
+ console.error(`Error parsing angular.json at ${angularJsonPath}: ${message}`);
186
+ return;
187
+ }
188
+ const projectName = `@researchdatabox/${this.name}`;
189
+ if (typeof angularJson.projects !== 'object' || angularJson.projects === null) {
190
+ angularJson.projects = {};
191
+ }
192
+ if (angularJson.projects[projectName]) {
193
+ console.log(` [SKIP] angular.json project ${projectName} already exists`);
194
+ return;
195
+ }
196
+ angularJson.projects[projectName] = {
197
+ "projectType": "application",
198
+ "schematics": {
199
+ "@schematics/angular:component": {
200
+ "style": "scss"
201
+ }
202
+ },
203
+ "root": `projects/researchdatabox/${this.name}`,
204
+ "sourceRoot": `projects/researchdatabox/${this.name}/src`,
205
+ "prefix": "app",
206
+ "architect": {
207
+ "build": {
208
+ "builder": "@angular-devkit/build-angular:application",
209
+ "options": {
210
+ "outputPath": {
211
+ "base": `../assets/angular/${this.name}`
212
+ },
213
+ "index": `projects/researchdatabox/${this.name}/src/index.html`,
214
+ "polyfills": [
215
+ "zone.js"
216
+ ],
217
+ "tsConfig": `projects/researchdatabox/${this.name}/tsconfig.app.json`,
218
+ "inlineStyleLanguage": "scss",
219
+ "assets": [
220
+ `projects/researchdatabox/${this.name}/src/favicon.ico`,
221
+ `projects/researchdatabox/${this.name}/src/assets`
222
+ ],
223
+ "styles": [
224
+ `projects/researchdatabox/${this.name}/src/styles.scss`
225
+ ],
226
+ "scripts": [],
227
+ "browser": `projects/researchdatabox/${this.name}/src/main.ts`
228
+ },
229
+ "configurations": {
230
+ "production": {
231
+ "budgets": [
232
+ {
233
+ "type": "initial",
234
+ "maximumWarning": "500kb",
235
+ "maximumError": "1mb"
236
+ },
237
+ {
238
+ "type": "anyComponentStyle",
239
+ "maximumWarning": "2kb",
240
+ "maximumError": "4kb"
241
+ }
242
+ ],
243
+ "outputHashing": "all"
244
+ },
245
+ "development": {
246
+ "optimization": false,
247
+ "extractLicenses": false,
248
+ "sourceMap": true
249
+ }
250
+ },
251
+ "defaultConfiguration": "production"
252
+ }
253
+ }
254
+ };
255
+ this.writeFile(angularJsonPath, JSON.stringify(angularJson, null, 2));
256
+ }
257
+ generateEjsView() {
258
+ const safeViewName = this.getSafeEjsViewName();
259
+ const baseDir = path.join(this.root, 'views', 'default', 'default', 'admin');
260
+ const viewPath = path.resolve(baseDir, `${safeViewName}.ejs`);
261
+ if (!viewPath.startsWith(path.resolve(baseDir) + path.sep)) {
262
+ throw new Error(`EJS view name resolves outside the admin views directory: ${safeViewName}`);
263
+ }
264
+ const content = `<% let appName = '${this.name}'; %>
265
+ <div class="row admin-layout">
266
+ <div class="col-md-3 col-lg-2">
267
+ <%- include('layout/sidebar.ejs') %>
268
+ </div>
269
+ <div class="col-md-9 col-lg-10 admin-main-content">
270
+ <${this.name} <% if (typeof contentSecurityPolicyNonce !== "undefined" && contentSecurityPolicyNonce) {%>ngCspNonce="<%= contentSecurityPolicyNonce %>"<% } %>>
271
+ <div class="col-xs-12">
272
+ <img class="center-block" src="<%= BrandingService.getRootContext() %>/images/loading.svg" alt="<%= TranslationService.t('loading-alt-text') %>">
273
+ </div>
274
+ </${this.name}>
275
+ </div>
276
+ </div>
277
+ <script src="<%= BrandingService.getRootContext() %>/angular/<%=appName%>/browser/polyfills<%=CacheService.getNgAppFileHash(appName, 'polyfills', '-') %>.js"
278
+ type="module"></script>
279
+
280
+ <script src="<%= BrandingService.getRootContext() %>/angular/<%=appName%>/browser/main<%=CacheService.getNgAppFileHash(appName, 'main', '-') %>.js"
281
+ type="module"></script>
282
+ <link rel="stylesheet" href="<%= BrandingService.getRootContext() %>/angular/<%= appName %>/browser/styles<%=CacheService.getNgAppFileHash(appName, 'styles', '-')%>.css">
283
+ `;
284
+ this.writeFile(viewPath, content);
285
+ }
286
+ async updateRouteConfig() {
287
+ const route = `/:branding/:portal/admin/${this.name}`; // Defaulting to admin path
288
+ const safeViewName = this.getSafeEjsViewName();
289
+ // We can't use updateRoutes directly because it assumes a controller.action string
290
+ // But here we want a RouteTargetObject with locals.view
291
+ const routesPath = path.join(this.paths.coreTypes, 'src', 'config', 'routes.config.ts');
292
+ if (!fs.existsSync(routesPath))
293
+ return;
294
+ const sourceFile = this.project.addSourceFileAtPath(routesPath);
295
+ const routesVar = sourceFile.getVariableDeclaration('routes');
296
+ if (!routesVar)
297
+ return;
298
+ const initializer = routesVar.getInitializerIfKind(ts_morph_1.SyntaxKind.ObjectLiteralExpression);
299
+ if (!initializer)
300
+ return;
301
+ if (!initializer.getProperty(`'${route}'`) && !initializer.getProperty(`"${route}"`)) {
302
+ initializer.addPropertyAssignment({
303
+ name: `'${route}'`,
304
+ initializer: `{
305
+ controller: 'RenderViewController',
306
+ action: 'render',
307
+ locals: { 'view': 'admin/${safeViewName}' }
308
+ }`
309
+ });
310
+ }
311
+ sourceFile.formatText();
312
+ if (!this.dryRun) {
313
+ await sourceFile.save();
314
+ console.log(` [UPDATE] ${path.relative(this.root, routesPath)}`);
315
+ }
316
+ }
317
+ async updateAuthConfig() {
318
+ const route = `/:branding/:portal/admin/${this.name}`;
319
+ await (0, config_helper_1.updateAuth)({
320
+ project: this.project,
321
+ paths: this.paths,
322
+ root: this.root,
323
+ route: route,
324
+ auth: this.auth,
325
+ dryRun: this.dryRun
326
+ });
327
+ }
328
+ toClassName(name) {
329
+ return name.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');
330
+ }
331
+ getSafeEjsViewName() {
332
+ const trimmed = this.ejsView.trim();
333
+ if (!trimmed) {
334
+ throw new Error('EJS view name cannot be empty.');
335
+ }
336
+ if (!/^[A-Za-z0-9_-]+$/.test(trimmed)) {
337
+ throw new Error(`EJS view name contains invalid characters: '${this.ejsView}'`);
338
+ }
339
+ return trimmed;
340
+ }
341
+ }
342
+ exports.AngularAppGenerator = AngularAppGenerator;
@@ -0,0 +1,21 @@
1
+ import { Generator, GeneratorOptions } from '../utils/generator';
2
+ import { RedboxPaths } from '../utils/paths';
3
+ export interface AngularServiceGeneratorOptions extends GeneratorOptions {
4
+ app: string;
5
+ name: string;
6
+ methods?: string[];
7
+ paths: RedboxPaths;
8
+ }
9
+ export declare class AngularServiceGenerator extends Generator {
10
+ private app;
11
+ private name;
12
+ private methods;
13
+ private paths;
14
+ constructor(options: AngularServiceGeneratorOptions);
15
+ generate(): Promise<void>;
16
+ private generateServiceContent;
17
+ private toClassName;
18
+ private capitalize;
19
+ private isValidTsIdentifier;
20
+ private toSafeMethodName;
21
+ }