generate-ui-cli 2.1.7 → 2.3.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/README.md +216 -27
- package/dist/commands/angular.js +535 -40
- package/dist/commands/generate.js +350 -20
- package/dist/commands/login.js +131 -42
- package/dist/commands/merge.js +201 -0
- package/dist/generate-ui/routes.gen.js +4 -0
- package/dist/generators/angular/feature.generator.js +3183 -583
- package/dist/generators/angular/menu.generator.js +165 -0
- package/dist/generators/angular/routes.generator.js +8 -3
- package/dist/generators/screen.generator.js +100 -5
- package/dist/generators/screen.merge.js +70 -15
- package/dist/index.js +88 -12
- package/dist/license/guard.js +2 -1
- package/dist/license/permissions.js +63 -9
- package/dist/license/token.js +38 -3
- package/dist/postinstall.js +47 -0
- package/dist/runtime/config.js +4 -0
- package/dist/runtime/logger.js +29 -0
- package/dist/runtime/project-config.js +64 -0
- package/package.json +1 -1
package/dist/commands/angular.js
CHANGED
|
@@ -7,83 +7,578 @@ exports.angular = angular;
|
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const feature_generator_1 = require("../generators/angular/feature.generator");
|
|
10
|
+
const feature_generator_2 = require("../generators/angular/feature.generator");
|
|
10
11
|
const routes_generator_1 = require("../generators/angular/routes.generator");
|
|
12
|
+
const menu_generator_1 = require("../generators/angular/menu.generator");
|
|
11
13
|
const telemetry_1 = require("../telemetry");
|
|
12
14
|
const user_config_1 = require("../runtime/user-config");
|
|
15
|
+
const permissions_1 = require("../license/permissions");
|
|
16
|
+
const logger_1 = require("../runtime/logger");
|
|
17
|
+
const project_config_1 = require("../runtime/project-config");
|
|
13
18
|
async function angular(options) {
|
|
14
19
|
void (0, telemetry_1.trackCommand)('angular', options.telemetryEnabled);
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
let intelligentEnabled = false;
|
|
21
|
+
let subscriptionReason = '';
|
|
22
|
+
try {
|
|
23
|
+
const permissions = await (0, permissions_1.getPermissions)();
|
|
24
|
+
intelligentEnabled = Boolean(permissions.features.intelligentGeneration);
|
|
25
|
+
subscriptionReason = String(permissions.subscription.reason ?? '').trim();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
intelligentEnabled = false;
|
|
29
|
+
subscriptionReason = '';
|
|
30
|
+
}
|
|
31
|
+
const projectConfig = (0, project_config_1.findProjectConfig)(process.cwd());
|
|
32
|
+
const configuredFeatures = (0, project_config_1.pickConfiguredPath)(projectConfig.config, 'features');
|
|
33
|
+
const configuredSchemas = (0, project_config_1.pickConfiguredPath)(projectConfig.config, 'schemas') ??
|
|
34
|
+
(0, project_config_1.pickConfiguredPath)(projectConfig.config, 'output');
|
|
35
|
+
const featuresRoot = resolveFeaturesRoot(options.featuresPath, configuredFeatures, projectConfig.configPath);
|
|
36
|
+
const generatedFeaturesRoot = path_1.default.join(featuresRoot, 'generated');
|
|
37
|
+
const overridesFeaturesRoot = path_1.default.join(featuresRoot, 'overrides');
|
|
38
|
+
const schemasRoot = resolveSchemasRoot(options.schemasPath, configuredSchemas, projectConfig.configPath, featuresRoot);
|
|
39
|
+
(0, logger_1.logStep)(`Features output: ${featuresRoot}`);
|
|
40
|
+
(0, logger_1.logDebug)(`Generated features: ${generatedFeaturesRoot}`);
|
|
41
|
+
(0, logger_1.logDebug)(`Overrides: ${overridesFeaturesRoot}`);
|
|
42
|
+
(0, logger_1.logStep)(`Schemas input: ${schemasRoot}`);
|
|
17
43
|
/**
|
|
18
44
|
* Onde estão os schemas
|
|
19
45
|
* Ex: generate-ui
|
|
20
46
|
*/
|
|
47
|
+
if (options.schemasPath && !fs_1.default.existsSync(schemasRoot)) {
|
|
48
|
+
fs_1.default.mkdirSync(schemasRoot, { recursive: true });
|
|
49
|
+
console.log(`ℹ Created generate-ui folder at ${schemasRoot}`);
|
|
50
|
+
}
|
|
21
51
|
const overlaysDir = path_1.default.join(schemasRoot, 'overlays');
|
|
22
|
-
|
|
23
|
-
|
|
52
|
+
await generateAngularOnce({
|
|
53
|
+
overlaysDir,
|
|
54
|
+
featuresRoot,
|
|
55
|
+
generatedFeaturesRoot,
|
|
56
|
+
overridesFeaturesRoot,
|
|
57
|
+
schemasRoot,
|
|
58
|
+
intelligentEnabled,
|
|
59
|
+
subscriptionReason
|
|
60
|
+
});
|
|
61
|
+
if (!options.watch) {
|
|
62
|
+
(0, logger_1.logTip)('Run with --dev to see detailed logs and file paths.');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
console.log(`👀 Watching ${overlaysDir} for .screen.json changes...`);
|
|
66
|
+
let debounceTimer = null;
|
|
67
|
+
const rerun = (fileName) => {
|
|
68
|
+
if (debounceTimer)
|
|
69
|
+
clearTimeout(debounceTimer);
|
|
70
|
+
debounceTimer = setTimeout(async () => {
|
|
71
|
+
console.log(`↻ Change detected: ${fileName}`);
|
|
72
|
+
try {
|
|
73
|
+
await generateAngularOnce({
|
|
74
|
+
overlaysDir,
|
|
75
|
+
featuresRoot,
|
|
76
|
+
generatedFeaturesRoot,
|
|
77
|
+
overridesFeaturesRoot,
|
|
78
|
+
schemasRoot,
|
|
79
|
+
intelligentEnabled,
|
|
80
|
+
subscriptionReason
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const message = error instanceof Error ? error.message : 'Unexpected error';
|
|
85
|
+
console.error(message);
|
|
86
|
+
}
|
|
87
|
+
}, 180);
|
|
88
|
+
};
|
|
89
|
+
const watcher = fs_1.default.watch(overlaysDir, (event, fileName) => {
|
|
90
|
+
if (!fileName)
|
|
91
|
+
return;
|
|
92
|
+
if (!String(fileName).endsWith('.screen.json'))
|
|
93
|
+
return;
|
|
94
|
+
if (event !== 'change' && event !== 'rename')
|
|
95
|
+
return;
|
|
96
|
+
rerun(String(fileName));
|
|
97
|
+
});
|
|
98
|
+
const closeWatcher = () => {
|
|
99
|
+
watcher.close();
|
|
100
|
+
console.log('\nStopped screen watch.');
|
|
101
|
+
process.exit(0);
|
|
102
|
+
};
|
|
103
|
+
process.on('SIGINT', closeWatcher);
|
|
104
|
+
process.on('SIGTERM', closeWatcher);
|
|
105
|
+
}
|
|
106
|
+
async function generateAngularOnce(options) {
|
|
107
|
+
if (!fs_1.default.existsSync(options.overlaysDir)) {
|
|
108
|
+
throw new Error(`Overlays directory not found: ${options.overlaysDir}\n` +
|
|
109
|
+
'Run `generate-ui generate` first to create overlays.\n' +
|
|
110
|
+
'If needed, configure "openapi", "schemas", and "features" in generateui-config.json.');
|
|
24
111
|
}
|
|
112
|
+
(0, logger_1.logDebug)(`Overlays dir: ${options.overlaysDir}`);
|
|
25
113
|
const screens = fs_1.default
|
|
26
|
-
.readdirSync(overlaysDir)
|
|
114
|
+
.readdirSync(options.overlaysDir)
|
|
27
115
|
.filter(f => f.endsWith('.screen.json'));
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
116
|
+
(0, logger_1.logDebug)(`Screens found: ${screens.length}`);
|
|
117
|
+
if (screens.length === 0) {
|
|
118
|
+
throw new Error(`No .screen.json files found in: ${options.overlaysDir}\n` +
|
|
119
|
+
'Run `generate-ui generate` and then `generate-ui angular`.\n' +
|
|
120
|
+
'If needed, set "schemas" (or "paths.schemas") in generateui-config.json.');
|
|
121
|
+
}
|
|
122
|
+
fs_1.default.mkdirSync(options.featuresRoot, { recursive: true });
|
|
123
|
+
fs_1.default.mkdirSync(options.generatedFeaturesRoot, { recursive: true });
|
|
124
|
+
fs_1.default.mkdirSync(options.overridesFeaturesRoot, { recursive: true });
|
|
32
125
|
const routes = [];
|
|
126
|
+
const schemas = [];
|
|
127
|
+
const appRoot = path_1.default.resolve(options.featuresRoot, '..');
|
|
128
|
+
const configInfo = findConfig(appRoot);
|
|
129
|
+
const views = configInfo.config?.views ?? {};
|
|
130
|
+
const generatedSchemasDir = path_1.default.join(options.schemasRoot, 'generated');
|
|
33
131
|
for (const file of screens) {
|
|
34
|
-
|
|
35
|
-
const
|
|
132
|
+
let schema = JSON.parse(fs_1.default.readFileSync(path_1.default.join(options.overlaysDir, file), 'utf-8'));
|
|
133
|
+
const generatedPath = path_1.default.join(generatedSchemasDir, file);
|
|
134
|
+
if (fs_1.default.existsSync(generatedPath)) {
|
|
135
|
+
try {
|
|
136
|
+
const generatedSchema = JSON.parse(fs_1.default.readFileSync(generatedPath, 'utf-8'));
|
|
137
|
+
const normalized = enforceScreenShape(schema, generatedSchema);
|
|
138
|
+
schema = normalized.schema;
|
|
139
|
+
for (const warning of normalized.warnings) {
|
|
140
|
+
console.warn(warning);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Ignore malformed generated schema and keep overlay as source.
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
schemas.push({ file, schema });
|
|
148
|
+
}
|
|
149
|
+
const schemaByOpId = new Map();
|
|
150
|
+
for (const { schema } of schemas) {
|
|
151
|
+
const opId = schema?.api?.operationId;
|
|
152
|
+
if (opId)
|
|
153
|
+
schemaByOpId.set(opId, schema);
|
|
154
|
+
}
|
|
155
|
+
for (const { schema } of schemas) {
|
|
156
|
+
const opId = schema?.api?.operationId;
|
|
157
|
+
const view = opId ? views[opId] : undefined;
|
|
158
|
+
if (view) {
|
|
159
|
+
schema.meta = {
|
|
160
|
+
...(schema.meta ?? {}),
|
|
161
|
+
view
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (schema?.meta?.intelligent?.kind === 'adminList') {
|
|
165
|
+
if (!options.intelligentEnabled) {
|
|
166
|
+
const reason = String(options.subscriptionReason ?? '').trim();
|
|
167
|
+
(0, logger_1.logTip)(reason
|
|
168
|
+
? `Intelligent generation is disabled: ${reason}`
|
|
169
|
+
: 'Intelligent generation is disabled. Login and an active subscription are required to generate admin list screens.');
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const adminRoute = (0, feature_generator_2.generateAdminFeature)(schema, schemaByOpId, options.featuresRoot, options.generatedFeaturesRoot, options.schemasRoot);
|
|
173
|
+
routes.push(adminRoute);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const route = (0, feature_generator_1.generateFeature)(schema, options.featuresRoot, options.generatedFeaturesRoot, options.schemasRoot);
|
|
36
177
|
routes.push(route);
|
|
37
178
|
}
|
|
38
|
-
(
|
|
39
|
-
|
|
179
|
+
migrateLegacyFeatures(routes, options.featuresRoot, options.generatedFeaturesRoot, options.overridesFeaturesRoot);
|
|
180
|
+
syncOverrides(routes, options.generatedFeaturesRoot, options.overridesFeaturesRoot);
|
|
181
|
+
(0, routes_generator_1.generateRoutes)(routes, options.generatedFeaturesRoot, options.overridesFeaturesRoot, options.schemasRoot);
|
|
182
|
+
(0, menu_generator_1.generateMenu)(options.schemasRoot);
|
|
183
|
+
applyAppLayout(options.featuresRoot, options.schemasRoot);
|
|
184
|
+
console.log(`✔ Angular features generated at ${options.featuresRoot}`);
|
|
185
|
+
const overrides = findOverrides(routes, options.overridesFeaturesRoot, options.generatedFeaturesRoot);
|
|
186
|
+
if (overrides.length) {
|
|
187
|
+
console.log('');
|
|
188
|
+
console.log('ℹ Overrides with differences detected:');
|
|
189
|
+
console.log(' Use merge to compare generated (left) vs overrides (right).');
|
|
190
|
+
console.log(' Keep your changes in the right file (overrides).');
|
|
191
|
+
for (const override of overrides) {
|
|
192
|
+
console.log(` - ${override.component} -> generate-ui merge --feature ${override.folder} --file all`);
|
|
193
|
+
}
|
|
194
|
+
console.log('');
|
|
195
|
+
}
|
|
40
196
|
}
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
197
|
+
function enforceScreenShape(overlay, generated) {
|
|
198
|
+
const warnings = [];
|
|
199
|
+
const next = overlay ?? {};
|
|
200
|
+
const generatedType = generated?.screen?.type;
|
|
201
|
+
const overlayType = next?.screen?.type;
|
|
202
|
+
if (generatedType &&
|
|
203
|
+
overlayType &&
|
|
204
|
+
String(overlayType) !== String(generatedType)) {
|
|
205
|
+
next.screen = { ...(next.screen ?? {}), type: generatedType };
|
|
206
|
+
warnings.push(`⚠ Ignored overlay change: screen.type is fixed by generation (${generatedType}).`);
|
|
44
207
|
}
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
208
|
+
const generatedMode = generated?.screen?.mode;
|
|
209
|
+
const overlayMode = next?.screen?.mode;
|
|
210
|
+
if (generatedMode &&
|
|
211
|
+
overlayMode &&
|
|
212
|
+
String(overlayMode) !== String(generatedMode)) {
|
|
213
|
+
next.screen = { ...(next.screen ?? {}), mode: generatedMode };
|
|
214
|
+
warnings.push(`⚠ Ignored overlay change: screen.mode is fixed by generation (${generatedMode}).`);
|
|
215
|
+
}
|
|
216
|
+
const generatedMethod = generated?.api?.method;
|
|
217
|
+
const overlayMethod = next?.api?.method;
|
|
218
|
+
if (generatedMethod &&
|
|
219
|
+
overlayMethod &&
|
|
220
|
+
String(overlayMethod).toLowerCase() !==
|
|
221
|
+
String(generatedMethod).toLowerCase()) {
|
|
222
|
+
next.api = { ...(next.api ?? {}), method: generatedMethod };
|
|
223
|
+
warnings.push(`⚠ Ignored overlay change: api.method is fixed by generation (${generatedMethod}).`);
|
|
224
|
+
}
|
|
225
|
+
const generatedOpId = generated?.api?.operationId;
|
|
226
|
+
const overlayOpId = next?.api?.operationId;
|
|
227
|
+
if (generatedOpId &&
|
|
228
|
+
overlayOpId &&
|
|
229
|
+
String(overlayOpId) !== String(generatedOpId)) {
|
|
230
|
+
next.api = { ...(next.api ?? {}), operationId: generatedOpId };
|
|
231
|
+
warnings.push(`⚠ Ignored overlay change: api.operationId is fixed by generation (${generatedOpId}).`);
|
|
232
|
+
}
|
|
233
|
+
return { schema: next, warnings };
|
|
234
|
+
}
|
|
235
|
+
function resolveSchemasRoot(value, configured, configPath, featuresRoot) {
|
|
236
|
+
const fromConfig = (0, project_config_1.resolveOptionalPath)(value, configured, configPath);
|
|
237
|
+
if (fromConfig) {
|
|
238
|
+
return fromConfig;
|
|
51
239
|
}
|
|
52
240
|
const inferred = inferSchemasRootFromFeatures(featuresRoot);
|
|
53
241
|
if (inferred)
|
|
54
242
|
return inferred;
|
|
55
243
|
return resolveDefaultSchemasRoot();
|
|
56
244
|
}
|
|
57
|
-
function resolveFeaturesRoot(value) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
path_1.default.basename(path_1.default.dirname(resolved)) === 'src';
|
|
62
|
-
if (isSrcApp) {
|
|
63
|
-
return path_1.default.join(resolved, 'features');
|
|
64
|
-
}
|
|
65
|
-
return resolved;
|
|
245
|
+
function resolveFeaturesRoot(value, configured, configPath) {
|
|
246
|
+
const fromConfig = (0, project_config_1.resolveOptionalPath)(value, configured, configPath);
|
|
247
|
+
if (fromConfig) {
|
|
248
|
+
return normalizeFeaturesRoot(fromConfig);
|
|
66
249
|
}
|
|
67
250
|
const srcAppRoot = path_1.default.resolve(process.cwd(), 'src', 'app');
|
|
68
|
-
if (
|
|
69
|
-
|
|
251
|
+
if (fs_1.default.existsSync(srcAppRoot)) {
|
|
252
|
+
return path_1.default.join(srcAppRoot, 'features');
|
|
70
253
|
}
|
|
71
|
-
|
|
254
|
+
throw new Error('Default features path not found.\n' +
|
|
255
|
+
'Use --features <path> or set "features" (or "paths.features") in generateui-config.json.');
|
|
72
256
|
}
|
|
73
257
|
function inferSchemasRootFromFeatures(featuresRoot) {
|
|
74
|
-
const
|
|
75
|
-
if (fs_1.default.existsSync(path_1.default.join(
|
|
76
|
-
return
|
|
258
|
+
const srcCandidate = path_1.default.resolve(featuresRoot, '..', '..', 'generate-ui');
|
|
259
|
+
if (fs_1.default.existsSync(path_1.default.join(srcCandidate, 'overlays'))) {
|
|
260
|
+
return srcCandidate;
|
|
261
|
+
}
|
|
262
|
+
const rootCandidate = path_1.default.resolve(featuresRoot, '../../..', 'generate-ui');
|
|
263
|
+
if (fs_1.default.existsSync(path_1.default.join(rootCandidate, 'overlays'))) {
|
|
264
|
+
return rootCandidate;
|
|
77
265
|
}
|
|
78
266
|
return null;
|
|
79
267
|
}
|
|
80
268
|
function resolveDefaultSchemasRoot() {
|
|
269
|
+
const userConfig = (0, user_config_1.loadUserConfig)();
|
|
270
|
+
if (userConfig?.lastSchemasPath &&
|
|
271
|
+
fs_1.default.existsSync(path_1.default.join(userConfig.lastSchemasPath, 'overlays'))) {
|
|
272
|
+
return userConfig.lastSchemasPath;
|
|
273
|
+
}
|
|
81
274
|
const cwd = process.cwd();
|
|
82
275
|
if (fs_1.default.existsSync(path_1.default.join(cwd, 'src'))) {
|
|
83
276
|
return path_1.default.join(cwd, 'src', 'generate-ui');
|
|
84
277
|
}
|
|
85
|
-
if (fs_1.default.existsSync(path_1.default.join(cwd, 'frontend', 'src'))) {
|
|
86
|
-
return path_1.default.join(cwd, 'frontend', 'src', 'generate-ui');
|
|
87
|
-
}
|
|
88
278
|
return path_1.default.join(cwd, 'generate-ui');
|
|
89
279
|
}
|
|
280
|
+
function normalizeFeaturesRoot(value) {
|
|
281
|
+
const isSrcApp = path_1.default.basename(value) === 'app' &&
|
|
282
|
+
path_1.default.basename(path_1.default.dirname(value)) === 'src';
|
|
283
|
+
if (isSrcApp)
|
|
284
|
+
return path_1.default.join(value, 'features');
|
|
285
|
+
return value;
|
|
286
|
+
}
|
|
287
|
+
function findOverrides(routes, overridesRoot, generatedRoot) {
|
|
288
|
+
const results = [];
|
|
289
|
+
for (const route of routes) {
|
|
290
|
+
const overridePath = path_1.default.join(overridesRoot, route.folder, `${route.fileBase}.component.ts`);
|
|
291
|
+
const generatedPath = path_1.default.join(generatedRoot, route.folder, `${route.fileBase}.component.ts`);
|
|
292
|
+
if (!fs_1.default.existsSync(overridePath))
|
|
293
|
+
continue;
|
|
294
|
+
if (!fs_1.default.existsSync(generatedPath))
|
|
295
|
+
continue;
|
|
296
|
+
const overrideRaw = fs_1.default.readFileSync(overridePath, 'utf-8');
|
|
297
|
+
const generatedRaw = fs_1.default.readFileSync(generatedPath, 'utf-8');
|
|
298
|
+
if (overrideRaw !== generatedRaw) {
|
|
299
|
+
results.push({
|
|
300
|
+
folder: route.folder,
|
|
301
|
+
component: route.component,
|
|
302
|
+
generatedPath,
|
|
303
|
+
overridePath
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return results;
|
|
308
|
+
}
|
|
309
|
+
function syncOverrides(routes, generatedRoot, overridesRoot) {
|
|
310
|
+
const overridesEmpty = fs_1.default.existsSync(overridesRoot) &&
|
|
311
|
+
fs_1.default.readdirSync(overridesRoot).length === 0;
|
|
312
|
+
if (overridesEmpty && fs_1.default.existsSync(generatedRoot)) {
|
|
313
|
+
fs_1.default.cpSync(generatedRoot, overridesRoot, { recursive: true });
|
|
314
|
+
console.log('ℹ Seeded overrides from generated (initial sync).');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
for (const route of routes) {
|
|
318
|
+
const sourceDir = path_1.default.join(generatedRoot, route.folder);
|
|
319
|
+
const targetDir = path_1.default.join(overridesRoot, route.folder);
|
|
320
|
+
if (!fs_1.default.existsSync(sourceDir))
|
|
321
|
+
continue;
|
|
322
|
+
if (fs_1.default.existsSync(targetDir))
|
|
323
|
+
continue;
|
|
324
|
+
fs_1.default.mkdirSync(path_1.default.dirname(targetDir), { recursive: true });
|
|
325
|
+
fs_1.default.cpSync(sourceDir, targetDir, { recursive: true });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function migrateLegacyFeatures(routes, featuresRoot, generatedRoot, overridesRoot) {
|
|
329
|
+
let migrated = false;
|
|
330
|
+
for (const route of routes) {
|
|
331
|
+
const legacyDir = path_1.default.join(featuresRoot, route.folder);
|
|
332
|
+
const generatedDir = path_1.default.join(generatedRoot, route.folder);
|
|
333
|
+
const overrideDir = path_1.default.join(overridesRoot, route.folder);
|
|
334
|
+
if (!fs_1.default.existsSync(legacyDir))
|
|
335
|
+
continue;
|
|
336
|
+
if (fs_1.default.existsSync(generatedDir) || fs_1.default.existsSync(overrideDir)) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
fs_1.default.mkdirSync(path_1.default.dirname(generatedDir), { recursive: true });
|
|
340
|
+
fs_1.default.cpSync(legacyDir, generatedDir, { recursive: true });
|
|
341
|
+
fs_1.default.cpSync(legacyDir, overrideDir, { recursive: true });
|
|
342
|
+
migrated = true;
|
|
343
|
+
}
|
|
344
|
+
if (migrated) {
|
|
345
|
+
console.log('ℹ Legacy feature folders detected. Seeded generated/overrides.');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function applyAppLayout(featuresRoot, schemasRoot) {
|
|
349
|
+
const appRoot = path_1.default.resolve(featuresRoot, '..');
|
|
350
|
+
const configInfo = findConfig(appRoot);
|
|
351
|
+
const config = configInfo.config;
|
|
352
|
+
if (configInfo.configPath) {
|
|
353
|
+
(0, logger_1.logDebug)(`Config path: ${configInfo.configPath}`);
|
|
354
|
+
}
|
|
355
|
+
if (config) {
|
|
356
|
+
const resolvedTitle = config.appTitle ?? 'Generate UI';
|
|
357
|
+
const resolvedRoute = config.defaultRoute ?? '(not set)';
|
|
358
|
+
const resolvedInject = config.menu?.autoInject !== false;
|
|
359
|
+
console.log('✅ GenerateUI config detected');
|
|
360
|
+
console.log('');
|
|
361
|
+
console.log(` 🏷️ appTitle: "${resolvedTitle}"`);
|
|
362
|
+
console.log(` ➡️ defaultRoute: ${resolvedRoute}`);
|
|
363
|
+
console.log(` 🧭 menu.autoInject: ${resolvedInject}`);
|
|
364
|
+
console.log(' ✍️ Edit generateui-config.json before running `generate-ui generate` when you want to change title, route, views, or menu behavior.');
|
|
365
|
+
console.log(' 🧩 menu overrides: edit generate-ui/menu.overrides.json to customize labels, groups, and order');
|
|
366
|
+
console.log(' (this file is created once and never overwritten)');
|
|
367
|
+
console.log('');
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
console.log('ℹ️ No generateui-config.json found. Using defaults.');
|
|
371
|
+
console.log('');
|
|
372
|
+
console.log(' ✨ To customize, create/edit generateui-config.json at your project root before running `generate-ui generate`.');
|
|
373
|
+
console.log(' 🧩 To customize the menu, edit generate-ui/menu.overrides.json (created on first generate).');
|
|
374
|
+
console.log('');
|
|
375
|
+
}
|
|
376
|
+
if (config?.defaultRoute) {
|
|
377
|
+
injectDefaultRoute(appRoot, config.defaultRoute);
|
|
378
|
+
}
|
|
379
|
+
ensureBaseStyles(appRoot);
|
|
380
|
+
const autoInject = config?.menu?.autoInject !== false;
|
|
381
|
+
if (autoInject) {
|
|
382
|
+
const appTitle = config?.appTitle || 'Generate UI';
|
|
383
|
+
injectMenuLayout(appRoot, appTitle, schemasRoot);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function findConfig(startDir) {
|
|
387
|
+
let dir = startDir;
|
|
388
|
+
let config = null;
|
|
389
|
+
let configPath = null;
|
|
390
|
+
const root = path_1.default.parse(dir).root;
|
|
391
|
+
while (true) {
|
|
392
|
+
const candidate = path_1.default.join(dir, 'generateui-config.json');
|
|
393
|
+
if (!config && fs_1.default.existsSync(candidate)) {
|
|
394
|
+
try {
|
|
395
|
+
config = JSON.parse(fs_1.default.readFileSync(candidate, 'utf-8'));
|
|
396
|
+
configPath = candidate;
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
config = null;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (dir === root)
|
|
403
|
+
break;
|
|
404
|
+
dir = path_1.default.dirname(dir);
|
|
405
|
+
}
|
|
406
|
+
return { config, configPath };
|
|
407
|
+
}
|
|
408
|
+
function injectDefaultRoute(appRoot, value) {
|
|
409
|
+
const routesPath = path_1.default.join(appRoot, 'app.routes.ts');
|
|
410
|
+
if (!fs_1.default.existsSync(routesPath)) {
|
|
411
|
+
(0, logger_1.logDebug)(`Skip defaultRoute: ${routesPath} not found`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
let content = fs_1.default.readFileSync(routesPath, 'utf-8');
|
|
415
|
+
const route = normalizeRoutePath(value);
|
|
416
|
+
const insertion = ` { path: '', pathMatch: 'full', redirectTo: '${route}' },\n`;
|
|
417
|
+
if (!content.trim().length) {
|
|
418
|
+
const template = `import { Routes } from '@angular/router'\nimport { generatedRoutes } from '../generate-ui/routes.gen'\n\nexport const routes: Routes = [\n${insertion} ...generatedRoutes\n]\n`;
|
|
419
|
+
fs_1.default.writeFileSync(routesPath, template);
|
|
420
|
+
(0, logger_1.logDebug)(`Default route injected (created): ${routesPath}`);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (!content.match(/export const routes\s*:\s*Routes\s*=/)) {
|
|
424
|
+
if (!content.match(/import\s+\{\s*Routes\s*\}\s+from\s+['"]@angular\/router['"]/)) {
|
|
425
|
+
content = `import { Routes } from '@angular/router'\n${content}`;
|
|
426
|
+
}
|
|
427
|
+
content = content.replace(/export const routes\s*=/, 'export const routes: Routes =');
|
|
428
|
+
}
|
|
429
|
+
if (content.includes('generatedRoutes') &&
|
|
430
|
+
!content.match(/import\s+\{\s*generatedRoutes\s*\}\s+from\s+['"].*generate-ui\/routes\.gen['"]/)) {
|
|
431
|
+
content =
|
|
432
|
+
`import { generatedRoutes } from '../generate-ui/routes.gen'\n` +
|
|
433
|
+
content;
|
|
434
|
+
}
|
|
435
|
+
content = content.replace(/export const routes\s*=\s*\[/, match => `${match}\n${insertion}`);
|
|
436
|
+
const emptyRedirect = /path:\s*['"]\s*['"]\s*,\s*pathMatch:\s*['"]full['"]\s*,\s*redirectTo:\s*['"]\s*['"]/;
|
|
437
|
+
if (emptyRedirect.test(content)) {
|
|
438
|
+
content = content.replace(emptyRedirect, `path: '', pathMatch: 'full', redirectTo: '${route}'`);
|
|
439
|
+
}
|
|
440
|
+
fs_1.default.writeFileSync(routesPath, content);
|
|
441
|
+
(0, logger_1.logDebug)(`Default route injected (updated): ${routesPath}`);
|
|
442
|
+
}
|
|
443
|
+
function injectMenuLayout(appRoot, appTitle, schemasRoot) {
|
|
444
|
+
const appHtmlPath = path_1.default.join(appRoot, 'app.html');
|
|
445
|
+
const appCssPath = path_1.default.join(appRoot, 'app.css');
|
|
446
|
+
const appTsPath = path_1.default.join(appRoot, 'app.ts');
|
|
447
|
+
if (!fs_1.default.existsSync(appHtmlPath) ||
|
|
448
|
+
!fs_1.default.existsSync(appCssPath) ||
|
|
449
|
+
!fs_1.default.existsSync(appTsPath)) {
|
|
450
|
+
(0, logger_1.logDebug)('Skip menu injection: app.html/app.css/app.ts not found');
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const htmlRaw = fs_1.default.readFileSync(appHtmlPath, 'utf-8');
|
|
454
|
+
if (htmlRaw.includes('<ui-menu')) {
|
|
455
|
+
let updatedHtml = htmlRaw;
|
|
456
|
+
updatedHtml = updatedHtml.replace(/<ui-menu(?![^>]*\[\s*title\s*\])[\\s>]/, '<ui-menu [title]="appTitle()">');
|
|
457
|
+
if (updatedHtml !== htmlRaw) {
|
|
458
|
+
fs_1.default.writeFileSync(appHtmlPath, updatedHtml);
|
|
459
|
+
(0, logger_1.logDebug)(`Updated ui-menu title binding: ${appHtmlPath}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
const normalized = htmlRaw.replace(/\s+/g, '');
|
|
464
|
+
const isDefaultOutlet = normalized === '<router-outlet></router-outlet>' ||
|
|
465
|
+
normalized === '<router-outlet/>' ||
|
|
466
|
+
normalized === '<router-outlet/>';
|
|
467
|
+
if (!isDefaultOutlet)
|
|
468
|
+
return;
|
|
469
|
+
const newHtml = `<div class="app-shell">\n <ui-menu [title]="appTitle()"></ui-menu>\n <main class="app-content">\n <router-outlet></router-outlet>\n </main>\n</div>\n`;
|
|
470
|
+
fs_1.default.writeFileSync(appHtmlPath, newHtml);
|
|
471
|
+
(0, logger_1.logDebug)(`Injected menu layout into: ${appHtmlPath}`);
|
|
472
|
+
}
|
|
473
|
+
const cssRaw = fs_1.default.readFileSync(appCssPath, 'utf-8');
|
|
474
|
+
if (!cssRaw.includes('.app-shell')) {
|
|
475
|
+
const shellCss = `:host {\n display: block;\n min-height: 100vh;\n color: #0f172a;\n background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);\n}\n\n.app-shell {\n display: grid;\n grid-template-columns: 260px 1fr;\n gap: 24px;\n padding: 24px;\n align-items: start;\n}\n\n.app-content {\n min-width: 0;\n}\n\n@media (max-width: 900px) {\n .app-shell {\n grid-template-columns: 1fr;\n }\n}\n`;
|
|
476
|
+
fs_1.default.writeFileSync(appCssPath, cssRaw.trim().length ? `${cssRaw.trim()}\n\n${shellCss}` : shellCss);
|
|
477
|
+
(0, logger_1.logDebug)(`Injected menu shell styles into: ${appCssPath}`);
|
|
478
|
+
}
|
|
479
|
+
let tsRaw = fs_1.default.readFileSync(appTsPath, 'utf-8');
|
|
480
|
+
if (!tsRaw.includes('UiMenuComponent')) {
|
|
481
|
+
tsRaw = tsRaw.replace(/import\s+\{\s*([^}]+)\s*\}\s+from\s+['"]@angular\/router['"];/, (match) => `${match}\nimport { UiMenuComponent } from './ui/ui-menu/ui-menu.component';`);
|
|
482
|
+
}
|
|
483
|
+
if (tsRaw.includes('imports: [')) {
|
|
484
|
+
tsRaw = tsRaw.replace(/imports:\s*\[/, match => `${match}RouterOutlet, UiMenuComponent, `);
|
|
485
|
+
tsRaw = tsRaw.replace(/UiMenuComponent,\s*UiMenuComponent,\s*/g, 'UiMenuComponent, ');
|
|
486
|
+
tsRaw = tsRaw.replace(/RouterOutlet,\s*RouterOutlet,\s*/g, 'RouterOutlet, ');
|
|
487
|
+
tsRaw = tsRaw.replace(/UiMenuComponent,\s*RouterOutlet,\s*UiMenuComponent,/g, 'UiMenuComponent, ');
|
|
488
|
+
}
|
|
489
|
+
if (tsRaw.includes('appTitle')) {
|
|
490
|
+
tsRaw = tsRaw.replace(/appTitle\s*=\s*signal\('([^']*)'\)/, `appTitle = signal('${escapeString(appTitle)}')`);
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
if (!tsRaw.match(/import\s+\{\s*[^}]*\bsignal\b[^}]*\}\s+from\s+['"]@angular\/core['"]/)) {
|
|
494
|
+
tsRaw = tsRaw.replace(/import\s+\{\s*([^}]+)\s*\}\s+from\s+['"]@angular\/core['"];?/, (match, imports) => {
|
|
495
|
+
if (imports.includes('signal'))
|
|
496
|
+
return match;
|
|
497
|
+
return `import { ${imports.trim()}, signal } from '@angular/core';`;
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
tsRaw = tsRaw.replace(/export class App\s*\{\s*/, match => `${match}\n protected readonly appTitle = signal('${escapeString(appTitle)}');\n`);
|
|
501
|
+
}
|
|
502
|
+
// Remove legacy runtime config loader if present.
|
|
503
|
+
if (tsRaw.includes('loadRuntimeConfig')) {
|
|
504
|
+
tsRaw = tsRaw.replace(/\\s*constructor\\(\\)\\s*\\{[\\s\\S]*?\\}\\s*/m, '\n');
|
|
505
|
+
tsRaw = tsRaw.replace(/\\s*private\\s+loadRuntimeConfig\\(\\)\\s*\\{[\\s\\S]*?\\}\\s*/m, '\n');
|
|
506
|
+
}
|
|
507
|
+
fs_1.default.writeFileSync(appTsPath, tsRaw);
|
|
508
|
+
(0, logger_1.logDebug)(`Updated app title/menu imports: ${appTsPath}`);
|
|
509
|
+
const menuComponentPath = path_1.default.join(appRoot, 'ui', 'ui-menu', 'ui-menu.component.ts');
|
|
510
|
+
if (fs_1.default.existsSync(menuComponentPath)) {
|
|
511
|
+
// Touch to keep consistent in case it was generated before config title existed.
|
|
512
|
+
void schemasRoot;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function ensureBaseStyles(appRoot) {
|
|
516
|
+
const workspaceRoot = findAngularWorkspaceRoot(appRoot) ?? path_1.default.resolve(appRoot, '..');
|
|
517
|
+
const stylesPath = path_1.default.join(workspaceRoot, 'src', 'styles.css');
|
|
518
|
+
if (fs_1.default.existsSync(stylesPath))
|
|
519
|
+
return;
|
|
520
|
+
if (fs_1.default.existsSync(path_1.default.join(workspaceRoot, 'src', 'styles.scss')))
|
|
521
|
+
return;
|
|
522
|
+
const styles = `@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Instrument+Serif:ital@0;1&display=swap');
|
|
523
|
+
|
|
524
|
+
:root {
|
|
525
|
+
--bg-page: #f8fafc;
|
|
526
|
+
--bg-surface: #ffffff;
|
|
527
|
+
--bg-ink: #0f172a;
|
|
528
|
+
--color-text: #0f172a;
|
|
529
|
+
--color-muted: #64748b;
|
|
530
|
+
--color-border: #d1d5db;
|
|
531
|
+
--color-primary: #3b82f6;
|
|
532
|
+
--color-primary-strong: #1d4ed8;
|
|
533
|
+
--color-primary-soft: rgba(59, 130, 246, 0.14);
|
|
534
|
+
--color-accent: #0ea5e9;
|
|
535
|
+
--color-accent-strong: #0284c7;
|
|
536
|
+
--color-accent-soft: rgba(14, 165, 233, 0.14);
|
|
537
|
+
--shadow-card: 0 4px 16px rgba(15, 23, 42, 0.08);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
body {
|
|
541
|
+
margin: 0;
|
|
542
|
+
background: var(--bg-page);
|
|
543
|
+
color: var(--color-text);
|
|
544
|
+
font-family: "DM Sans", "Segoe UI", sans-serif;
|
|
545
|
+
}
|
|
546
|
+
`;
|
|
547
|
+
fs_1.default.writeFileSync(stylesPath, styles);
|
|
548
|
+
}
|
|
549
|
+
function findAngularWorkspaceRoot(startDir) {
|
|
550
|
+
let dir = startDir;
|
|
551
|
+
const root = path_1.default.parse(dir).root;
|
|
552
|
+
while (true) {
|
|
553
|
+
const candidate = path_1.default.join(dir, 'angular.json');
|
|
554
|
+
if (fs_1.default.existsSync(candidate))
|
|
555
|
+
return dir;
|
|
556
|
+
if (dir === root)
|
|
557
|
+
return null;
|
|
558
|
+
dir = path_1.default.dirname(dir);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function escapeString(value) {
|
|
562
|
+
return String(value).replace(/'/g, "\\'");
|
|
563
|
+
}
|
|
564
|
+
function normalizeRoutePath(value) {
|
|
565
|
+
const trimmed = String(value ?? '').trim();
|
|
566
|
+
if (!trimmed)
|
|
567
|
+
return trimmed;
|
|
568
|
+
if (trimmed.includes('/'))
|
|
569
|
+
return trimmed.replace(/^\//, '');
|
|
570
|
+
const pascal = toPascalCase(trimmed);
|
|
571
|
+
return toRouteSegment(pascal);
|
|
572
|
+
}
|
|
573
|
+
function toRouteSegment(value) {
|
|
574
|
+
if (!value)
|
|
575
|
+
return value;
|
|
576
|
+
return value[0].toLowerCase() + value.slice(1);
|
|
577
|
+
}
|
|
578
|
+
function toPascalCase(value) {
|
|
579
|
+
return String(value)
|
|
580
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
581
|
+
.filter(Boolean)
|
|
582
|
+
.map(part => part[0].toUpperCase() + part.slice(1))
|
|
583
|
+
.join('');
|
|
584
|
+
}
|