generate-ui-cli 2.1.7 → 2.2.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 +47 -0
- package/dist/commands/angular.js +221 -12
- package/dist/commands/generate.js +120 -2
- package/dist/commands/login.js +44 -17
- package/dist/generate-ui/routes.gen.js +4 -0
- package/dist/generators/angular/feature.generator.js +1112 -137
- package/dist/generators/angular/menu.generator.js +165 -0
- package/dist/index.js +19 -4
- package/dist/license/permissions.js +1 -1
- package/dist/license/token.js +19 -3
- package/dist/postinstall.js +7 -0
- package/dist/runtime/logger.js +29 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,6 +92,7 @@ What happens after this command:
|
|
|
92
92
|
- a typed API service
|
|
93
93
|
- DTO/types files
|
|
94
94
|
- route definitions
|
|
95
|
+
- `menu.json` and `menu.gen.ts` (if present in `generate-ui/`)
|
|
95
96
|
|
|
96
97
|
What you should review now:
|
|
97
98
|
|
|
@@ -107,6 +108,52 @@ Defaults:
|
|
|
107
108
|
- `--schemas` defaults to the last generated path (stored in `~/.generateui/config.json`), otherwise `./src/generate-ui` (or `./frontend/src/generate-ui` / `./generate-ui`)
|
|
108
109
|
- `--features` defaults to `./src/app/features` when `./src/app` exists; otherwise it errors and asks for `--features`
|
|
109
110
|
|
|
111
|
+
### generateui-config.json (optional)
|
|
112
|
+
|
|
113
|
+
GenerateUI creates a `generateui-config.json` at your project root on first `generate`. You can edit it to:
|
|
114
|
+
|
|
115
|
+
- inject a sidebar menu layout automatically (when `menu.autoInject` is not `false`)
|
|
116
|
+
- add a default redirect for `/` using `defaultRoute`
|
|
117
|
+
- show a custom app title in the menu (`appTitle`)
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"appTitle": "Rick & Morty Admin",
|
|
124
|
+
"defaultRoute": "GetCharacter",
|
|
125
|
+
"menu": {
|
|
126
|
+
"autoInject": true
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Notes:
|
|
132
|
+
- If `menu.autoInject` is `false`, the menu layout is not injected.
|
|
133
|
+
- `defaultRoute` must match a path in `routes.gen.ts` (the same path used by the router).
|
|
134
|
+
- You can provide either the final route path or an `operationId`; the generator normalizes it to the correct path.
|
|
135
|
+
- You can override the menu by adding `menu.overrides.json` inside your `generate-ui/` folder (it replaces the generated menu entirely).
|
|
136
|
+
|
|
137
|
+
Example `menu.overrides.json`:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"groups": [
|
|
142
|
+
{
|
|
143
|
+
"id": "cadastros",
|
|
144
|
+
"label": "Cadastros",
|
|
145
|
+
"items": [
|
|
146
|
+
{ "id": "GetCharacter", "label": "Personagens", "route": "getCharacter" },
|
|
147
|
+
{ "id": "GetLocation", "label": "Localizações", "route": "getLocation" }
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
"ungrouped": [
|
|
152
|
+
{ "id": "GetEpisode", "label": "Episódios", "route": "getEpisode" }
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
110
157
|
Optional paths:
|
|
111
158
|
|
|
112
159
|
```bash
|
package/dist/commands/angular.js
CHANGED
|
@@ -8,23 +8,49 @@ 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
10
|
const routes_generator_1 = require("../generators/angular/routes.generator");
|
|
11
|
+
const menu_generator_1 = require("../generators/angular/menu.generator");
|
|
11
12
|
const telemetry_1 = require("../telemetry");
|
|
12
|
-
const
|
|
13
|
+
const logger_1 = require("../runtime/logger");
|
|
13
14
|
async function angular(options) {
|
|
14
15
|
void (0, telemetry_1.trackCommand)('angular', options.telemetryEnabled);
|
|
15
16
|
const featuresRoot = resolveFeaturesRoot(options.featuresPath);
|
|
16
17
|
const schemasRoot = resolveSchemasRoot(options.schemasPath, featuresRoot);
|
|
18
|
+
(0, logger_1.logStep)(`Features output: ${featuresRoot}`);
|
|
19
|
+
(0, logger_1.logStep)(`Schemas input: ${schemasRoot}`);
|
|
17
20
|
/**
|
|
18
21
|
* Onde estão os schemas
|
|
19
22
|
* Ex: generate-ui
|
|
20
23
|
*/
|
|
24
|
+
if (options.schemasPath && !fs_1.default.existsSync(schemasRoot)) {
|
|
25
|
+
fs_1.default.mkdirSync(schemasRoot, { recursive: true });
|
|
26
|
+
console.log(`ℹ Created generate-ui folder at ${schemasRoot}`);
|
|
27
|
+
}
|
|
21
28
|
const overlaysDir = path_1.default.join(schemasRoot, 'overlays');
|
|
22
29
|
if (!fs_1.default.existsSync(overlaysDir)) {
|
|
23
|
-
|
|
30
|
+
const example = [
|
|
31
|
+
'generate-ui angular \\',
|
|
32
|
+
' --schemas /path/to/generate-ui \\',
|
|
33
|
+
' --features /path/to/src/app/features'
|
|
34
|
+
].join('\n');
|
|
35
|
+
throw new Error(`Overlays directory not found: ${overlaysDir}\n` +
|
|
36
|
+
'Run `generate-ui generate --openapi <path> --output <schemas>` first to create overlays.\n' +
|
|
37
|
+
`Example:\n${example}`);
|
|
24
38
|
}
|
|
39
|
+
(0, logger_1.logDebug)(`Overlays dir: ${overlaysDir}`);
|
|
25
40
|
const screens = fs_1.default
|
|
26
41
|
.readdirSync(overlaysDir)
|
|
27
42
|
.filter(f => f.endsWith('.screen.json'));
|
|
43
|
+
(0, logger_1.logDebug)(`Screens found: ${screens.length}`);
|
|
44
|
+
if (screens.length === 0) {
|
|
45
|
+
const example = [
|
|
46
|
+
'generate-ui angular \\',
|
|
47
|
+
' --schemas /path/to/generate-ui \\',
|
|
48
|
+
' --features /path/to/src/app/features'
|
|
49
|
+
].join('\n');
|
|
50
|
+
throw new Error(`No .screen.json files found in: ${overlaysDir}\n` +
|
|
51
|
+
'Run again with --schemas pointing to your generate-ui folder.\n' +
|
|
52
|
+
`Example:\n${example}`);
|
|
53
|
+
}
|
|
28
54
|
/**
|
|
29
55
|
* Onde gerar as features Angular
|
|
30
56
|
*/
|
|
@@ -36,19 +62,15 @@ async function angular(options) {
|
|
|
36
62
|
routes.push(route);
|
|
37
63
|
}
|
|
38
64
|
(0, routes_generator_1.generateRoutes)(routes, featuresRoot, schemasRoot);
|
|
65
|
+
(0, menu_generator_1.generateMenu)(schemasRoot);
|
|
66
|
+
applyAppLayout(featuresRoot, schemasRoot);
|
|
39
67
|
console.log(`✔ Angular features generated at ${featuresRoot}`);
|
|
68
|
+
(0, logger_1.logTip)('Run with --dev to see detailed logs and file paths.');
|
|
40
69
|
}
|
|
41
70
|
function resolveSchemasRoot(value, featuresRoot) {
|
|
42
71
|
if (value) {
|
|
43
72
|
return path_1.default.resolve(process.cwd(), value);
|
|
44
73
|
}
|
|
45
|
-
const config = (0, user_config_1.loadUserConfig)();
|
|
46
|
-
if (config?.lastSchemasPath) {
|
|
47
|
-
const resolved = path_1.default.resolve(config.lastSchemasPath);
|
|
48
|
-
if (fs_1.default.existsSync(path_1.default.join(resolved, 'overlays'))) {
|
|
49
|
-
return resolved;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
74
|
const inferred = inferSchemasRootFromFeatures(featuresRoot);
|
|
53
75
|
if (inferred)
|
|
54
76
|
return inferred;
|
|
@@ -71,9 +93,13 @@ function resolveFeaturesRoot(value) {
|
|
|
71
93
|
return path_1.default.join(srcAppRoot, 'features');
|
|
72
94
|
}
|
|
73
95
|
function inferSchemasRootFromFeatures(featuresRoot) {
|
|
74
|
-
const
|
|
75
|
-
if (fs_1.default.existsSync(path_1.default.join(
|
|
76
|
-
return
|
|
96
|
+
const srcCandidate = path_1.default.resolve(featuresRoot, '..', '..', 'generate-ui');
|
|
97
|
+
if (fs_1.default.existsSync(path_1.default.join(srcCandidate, 'overlays'))) {
|
|
98
|
+
return srcCandidate;
|
|
99
|
+
}
|
|
100
|
+
const rootCandidate = path_1.default.resolve(featuresRoot, '../../..', 'generate-ui');
|
|
101
|
+
if (fs_1.default.existsSync(path_1.default.join(rootCandidate, 'overlays'))) {
|
|
102
|
+
return rootCandidate;
|
|
77
103
|
}
|
|
78
104
|
return null;
|
|
79
105
|
}
|
|
@@ -87,3 +113,186 @@ function resolveDefaultSchemasRoot() {
|
|
|
87
113
|
}
|
|
88
114
|
return path_1.default.join(cwd, 'generate-ui');
|
|
89
115
|
}
|
|
116
|
+
function applyAppLayout(featuresRoot, schemasRoot) {
|
|
117
|
+
const appRoot = path_1.default.resolve(featuresRoot, '..');
|
|
118
|
+
const configInfo = findConfig(appRoot);
|
|
119
|
+
const config = configInfo.config;
|
|
120
|
+
if (configInfo.configPath) {
|
|
121
|
+
(0, logger_1.logDebug)(`Config path: ${configInfo.configPath}`);
|
|
122
|
+
}
|
|
123
|
+
if (config) {
|
|
124
|
+
const resolvedTitle = config.appTitle ?? 'Generate UI';
|
|
125
|
+
const resolvedRoute = config.defaultRoute ?? '(not set)';
|
|
126
|
+
const resolvedInject = config.menu?.autoInject !== false;
|
|
127
|
+
console.log('✅ GenerateUI config detected');
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log(` 🏷️ appTitle: "${resolvedTitle}"`);
|
|
130
|
+
console.log(` ➡️ defaultRoute: ${resolvedRoute}`);
|
|
131
|
+
console.log(` 🧭 menu.autoInject: ${resolvedInject}`);
|
|
132
|
+
console.log(' 🧩 menu overrides: edit generate-ui/menu.overrides.json to customize labels, groups, and order');
|
|
133
|
+
console.log(' (this file is created once and never overwritten)');
|
|
134
|
+
console.log('');
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
console.log('ℹ️ No generateui-config.json found. Using defaults.');
|
|
138
|
+
console.log('');
|
|
139
|
+
console.log(' ✨ To customize, add generateui-config.json at your project root.');
|
|
140
|
+
console.log(' 🧩 To customize the menu, edit generate-ui/menu.overrides.json (created on first generate).');
|
|
141
|
+
console.log('');
|
|
142
|
+
}
|
|
143
|
+
if (config?.defaultRoute) {
|
|
144
|
+
injectDefaultRoute(appRoot, config.defaultRoute);
|
|
145
|
+
}
|
|
146
|
+
const autoInject = config?.menu?.autoInject !== false;
|
|
147
|
+
if (!autoInject)
|
|
148
|
+
return;
|
|
149
|
+
const appTitle = config?.appTitle || 'Generate UI';
|
|
150
|
+
injectMenuLayout(appRoot, appTitle, schemasRoot);
|
|
151
|
+
}
|
|
152
|
+
function findConfig(startDir) {
|
|
153
|
+
let dir = startDir;
|
|
154
|
+
let config = null;
|
|
155
|
+
let configPath = null;
|
|
156
|
+
const root = path_1.default.parse(dir).root;
|
|
157
|
+
while (true) {
|
|
158
|
+
const candidate = path_1.default.join(dir, 'generateui-config.json');
|
|
159
|
+
if (!config && fs_1.default.existsSync(candidate)) {
|
|
160
|
+
try {
|
|
161
|
+
config = JSON.parse(fs_1.default.readFileSync(candidate, 'utf-8'));
|
|
162
|
+
configPath = candidate;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
config = null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (dir === root)
|
|
169
|
+
break;
|
|
170
|
+
dir = path_1.default.dirname(dir);
|
|
171
|
+
}
|
|
172
|
+
return { config, configPath };
|
|
173
|
+
}
|
|
174
|
+
function injectDefaultRoute(appRoot, value) {
|
|
175
|
+
const routesPath = path_1.default.join(appRoot, 'app.routes.ts');
|
|
176
|
+
if (!fs_1.default.existsSync(routesPath)) {
|
|
177
|
+
(0, logger_1.logDebug)(`Skip defaultRoute: ${routesPath} not found`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
let content = fs_1.default.readFileSync(routesPath, 'utf-8');
|
|
181
|
+
const route = normalizeRoutePath(value);
|
|
182
|
+
const insertion = ` { path: '', pathMatch: 'full', redirectTo: '${route}' },\n`;
|
|
183
|
+
if (!content.trim().length) {
|
|
184
|
+
const template = `import { Routes } from '@angular/router'\nimport { generatedRoutes } from '../generate-ui/routes.gen'\n\nexport const routes: Routes = [\n${insertion} ...generatedRoutes\n]\n`;
|
|
185
|
+
fs_1.default.writeFileSync(routesPath, template);
|
|
186
|
+
(0, logger_1.logDebug)(`Default route injected (created): ${routesPath}`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (!content.match(/export const routes\s*:\s*Routes\s*=/)) {
|
|
190
|
+
if (!content.match(/import\s+\{\s*Routes\s*\}\s+from\s+['"]@angular\/router['"]/)) {
|
|
191
|
+
content = `import { Routes } from '@angular/router'\n${content}`;
|
|
192
|
+
}
|
|
193
|
+
content = content.replace(/export const routes\s*=/, 'export const routes: Routes =');
|
|
194
|
+
}
|
|
195
|
+
content = content.replace(/export const routes\s*=\s*\[/, match => `${match}\n${insertion}`);
|
|
196
|
+
const emptyRedirect = /path:\s*['"]\s*['"]\s*,\s*pathMatch:\s*['"]full['"]\s*,\s*redirectTo:\s*['"]\s*['"]/;
|
|
197
|
+
if (emptyRedirect.test(content)) {
|
|
198
|
+
content = content.replace(emptyRedirect, `path: '', pathMatch: 'full', redirectTo: '${route}'`);
|
|
199
|
+
}
|
|
200
|
+
fs_1.default.writeFileSync(routesPath, content);
|
|
201
|
+
(0, logger_1.logDebug)(`Default route injected (updated): ${routesPath}`);
|
|
202
|
+
}
|
|
203
|
+
function injectMenuLayout(appRoot, appTitle, schemasRoot) {
|
|
204
|
+
const appHtmlPath = path_1.default.join(appRoot, 'app.html');
|
|
205
|
+
const appCssPath = path_1.default.join(appRoot, 'app.css');
|
|
206
|
+
const appTsPath = path_1.default.join(appRoot, 'app.ts');
|
|
207
|
+
if (!fs_1.default.existsSync(appHtmlPath) ||
|
|
208
|
+
!fs_1.default.existsSync(appCssPath) ||
|
|
209
|
+
!fs_1.default.existsSync(appTsPath)) {
|
|
210
|
+
(0, logger_1.logDebug)('Skip menu injection: app.html/app.css/app.ts not found');
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const htmlRaw = fs_1.default.readFileSync(appHtmlPath, 'utf-8');
|
|
214
|
+
if (htmlRaw.includes('<ui-menu')) {
|
|
215
|
+
let updatedHtml = htmlRaw;
|
|
216
|
+
updatedHtml = updatedHtml.replace(/<ui-menu(?![^>]*\[\s*title\s*\])[\\s>]/, '<ui-menu [title]="appTitle()">');
|
|
217
|
+
if (updatedHtml !== htmlRaw) {
|
|
218
|
+
fs_1.default.writeFileSync(appHtmlPath, updatedHtml);
|
|
219
|
+
(0, logger_1.logDebug)(`Updated ui-menu title binding: ${appHtmlPath}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const normalized = htmlRaw.replace(/\s+/g, '');
|
|
224
|
+
const isDefaultOutlet = normalized === '<router-outlet></router-outlet>' ||
|
|
225
|
+
normalized === '<router-outlet/>' ||
|
|
226
|
+
normalized === '<router-outlet/>';
|
|
227
|
+
if (!isDefaultOutlet)
|
|
228
|
+
return;
|
|
229
|
+
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`;
|
|
230
|
+
fs_1.default.writeFileSync(appHtmlPath, newHtml);
|
|
231
|
+
(0, logger_1.logDebug)(`Injected menu layout into: ${appHtmlPath}`);
|
|
232
|
+
}
|
|
233
|
+
const cssRaw = fs_1.default.readFileSync(appCssPath, 'utf-8');
|
|
234
|
+
if (!cssRaw.includes('.app-shell')) {
|
|
235
|
+
const shellCss = `:host {\n display: block;\n min-height: 100vh;\n color: #0f172a;\n background:\n radial-gradient(circle at top left, rgba(14, 116, 144, 0.14), transparent 55%),\n radial-gradient(circle at bottom right, rgba(59, 130, 246, 0.12), transparent 50%),\n linear-gradient(180deg, #f8fafc 0%, #eef2f7 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`;
|
|
236
|
+
fs_1.default.writeFileSync(appCssPath, cssRaw.trim().length ? `${cssRaw.trim()}\n\n${shellCss}` : shellCss);
|
|
237
|
+
(0, logger_1.logDebug)(`Injected menu shell styles into: ${appCssPath}`);
|
|
238
|
+
}
|
|
239
|
+
let tsRaw = fs_1.default.readFileSync(appTsPath, 'utf-8');
|
|
240
|
+
if (!tsRaw.includes('UiMenuComponent')) {
|
|
241
|
+
tsRaw = tsRaw.replace(/import\s+\{\s*([^}]+)\s*\}\s+from\s+['"]@angular\/router['"];/, (match) => `${match}\nimport { UiMenuComponent } from './ui/ui-menu/ui-menu.component';`);
|
|
242
|
+
}
|
|
243
|
+
if (tsRaw.includes('imports: [')) {
|
|
244
|
+
tsRaw = tsRaw.replace(/imports:\s*\[/, match => `${match}RouterOutlet, UiMenuComponent, `);
|
|
245
|
+
tsRaw = tsRaw.replace(/UiMenuComponent,\s*UiMenuComponent,\s*/g, 'UiMenuComponent, ');
|
|
246
|
+
tsRaw = tsRaw.replace(/RouterOutlet,\s*RouterOutlet,\s*/g, 'RouterOutlet, ');
|
|
247
|
+
tsRaw = tsRaw.replace(/UiMenuComponent,\s*RouterOutlet,\s*UiMenuComponent,/g, 'UiMenuComponent, ');
|
|
248
|
+
}
|
|
249
|
+
if (tsRaw.includes('appTitle')) {
|
|
250
|
+
tsRaw = tsRaw.replace(/appTitle\s*=\s*signal\('([^']*)'\)/, `appTitle = signal('${escapeString(appTitle)}')`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
if (!tsRaw.match(/import\s+\{\s*[^}]*\bsignal\b[^}]*\}\s+from\s+['"]@angular\/core['"]/)) {
|
|
254
|
+
tsRaw = tsRaw.replace(/import\s+\{\s*([^}]+)\s*\}\s+from\s+['"]@angular\/core['"];?/, (match, imports) => {
|
|
255
|
+
if (imports.includes('signal'))
|
|
256
|
+
return match;
|
|
257
|
+
return `import { ${imports.trim()}, signal } from '@angular/core';`;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
tsRaw = tsRaw.replace(/export class App\s*\{\s*/, match => `${match}\n protected readonly appTitle = signal('${escapeString(appTitle)}');\n`);
|
|
261
|
+
}
|
|
262
|
+
// Remove legacy runtime config loader if present.
|
|
263
|
+
if (tsRaw.includes('loadRuntimeConfig')) {
|
|
264
|
+
tsRaw = tsRaw.replace(/\\s*constructor\\(\\)\\s*\\{[\\s\\S]*?\\}\\s*/m, '\n');
|
|
265
|
+
tsRaw = tsRaw.replace(/\\s*private\\s+loadRuntimeConfig\\(\\)\\s*\\{[\\s\\S]*?\\}\\s*/m, '\n');
|
|
266
|
+
}
|
|
267
|
+
fs_1.default.writeFileSync(appTsPath, tsRaw);
|
|
268
|
+
(0, logger_1.logDebug)(`Updated app title/menu imports: ${appTsPath}`);
|
|
269
|
+
const menuComponentPath = path_1.default.join(appRoot, 'ui', 'ui-menu', 'ui-menu.component.ts');
|
|
270
|
+
if (fs_1.default.existsSync(menuComponentPath)) {
|
|
271
|
+
// Touch to keep consistent in case it was generated before config title existed.
|
|
272
|
+
void schemasRoot;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function escapeString(value) {
|
|
276
|
+
return String(value).replace(/'/g, "\\'");
|
|
277
|
+
}
|
|
278
|
+
function normalizeRoutePath(value) {
|
|
279
|
+
const trimmed = String(value ?? '').trim();
|
|
280
|
+
if (!trimmed)
|
|
281
|
+
return trimmed;
|
|
282
|
+
if (trimmed.includes('/'))
|
|
283
|
+
return trimmed.replace(/^\//, '');
|
|
284
|
+
const pascal = toPascalCase(trimmed);
|
|
285
|
+
return toRouteSegment(pascal);
|
|
286
|
+
}
|
|
287
|
+
function toRouteSegment(value) {
|
|
288
|
+
if (!value)
|
|
289
|
+
return value;
|
|
290
|
+
return value[0].toLowerCase() + value.slice(1);
|
|
291
|
+
}
|
|
292
|
+
function toPascalCase(value) {
|
|
293
|
+
return String(value)
|
|
294
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
295
|
+
.filter(Boolean)
|
|
296
|
+
.map(part => part[0].toUpperCase() + part.slice(1))
|
|
297
|
+
.join('');
|
|
298
|
+
}
|
|
@@ -13,6 +13,7 @@ const permissions_1 = require("../license/permissions");
|
|
|
13
13
|
const device_1 = require("../license/device");
|
|
14
14
|
const telemetry_1 = require("../telemetry");
|
|
15
15
|
const user_config_1 = require("../runtime/user-config");
|
|
16
|
+
const logger_1 = require("../runtime/logger");
|
|
16
17
|
async function generate(options) {
|
|
17
18
|
void (0, telemetry_1.trackCommand)('generate', options.telemetryEnabled);
|
|
18
19
|
/**
|
|
@@ -20,16 +21,21 @@ async function generate(options) {
|
|
|
20
21
|
* Ex: /Users/.../generateui-playground/realWorldOpenApi.yaml
|
|
21
22
|
*/
|
|
22
23
|
const openApiPath = path_1.default.resolve(process.cwd(), options.openapi);
|
|
24
|
+
(0, logger_1.logStep)(`OpenAPI: ${openApiPath}`);
|
|
23
25
|
/**
|
|
24
26
|
* Raiz do playground (onde está o YAML)
|
|
25
27
|
*/
|
|
26
28
|
const projectRoot = path_1.default.dirname(openApiPath);
|
|
29
|
+
(0, logger_1.logDebug)(`Project root: ${projectRoot}`);
|
|
27
30
|
/**
|
|
28
31
|
* Onde o Angular consome os arquivos
|
|
29
32
|
*/
|
|
30
33
|
const generateUiRoot = resolveGenerateUiRoot(projectRoot, options.output);
|
|
34
|
+
(0, logger_1.logStep)(`Schemas output: ${generateUiRoot}`);
|
|
31
35
|
const generatedDir = path_1.default.join(generateUiRoot, 'generated');
|
|
32
36
|
const overlaysDir = path_1.default.join(generateUiRoot, 'overlays');
|
|
37
|
+
(0, logger_1.logDebug)(`Generated dir: ${generatedDir}`);
|
|
38
|
+
(0, logger_1.logDebug)(`Overlays dir: ${overlaysDir}`);
|
|
33
39
|
(0, user_config_1.updateUserConfig)(config => ({
|
|
34
40
|
...config,
|
|
35
41
|
lastSchemasPath: generateUiRoot
|
|
@@ -43,17 +49,20 @@ async function generate(options) {
|
|
|
43
49
|
const usedOperationIds = new Set();
|
|
44
50
|
const permissions = await (0, permissions_1.getPermissions)();
|
|
45
51
|
const device = (0, device_1.loadDeviceIdentity)();
|
|
52
|
+
(0, logger_1.logDebug)(`License: maxGenerations=${permissions.features.maxGenerations}, overrides=${permissions.features.uiOverrides}, safeRegen=${permissions.features.safeRegeneration}`);
|
|
46
53
|
if (permissions.features.maxGenerations > -1 &&
|
|
47
54
|
device.freeGenerationsUsed >= permissions.features.maxGenerations) {
|
|
48
55
|
throw new Error('🔒 Você já utilizou sua geração gratuita.\n' +
|
|
49
56
|
'O plano Dev libera gerações ilimitadas, regeneração segura e UI inteligente.\n' +
|
|
50
|
-
'👉 Execute `generate-ui login` para continuar
|
|
57
|
+
'👉 Execute `generate-ui login` para continuar.\n' +
|
|
58
|
+
'Se você já fez login e ainda vê esta mensagem, tente novamente com a mesma versão do CLI e verifique a conexão com a API.');
|
|
51
59
|
}
|
|
52
60
|
/**
|
|
53
61
|
* Parse do OpenAPI (já com $refs resolvidos)
|
|
54
62
|
*/
|
|
55
63
|
const api = await (0, load_openapi_1.loadOpenApi)(openApiPath);
|
|
56
64
|
const paths = api.paths ?? {};
|
|
65
|
+
(0, logger_1.logDebug)(`OpenAPI paths: ${Object.keys(paths).length}`);
|
|
57
66
|
/**
|
|
58
67
|
* Itera por todos os endpoints
|
|
59
68
|
*/
|
|
@@ -117,16 +126,58 @@ async function generate(options) {
|
|
|
117
126
|
*/
|
|
118
127
|
routes.push({
|
|
119
128
|
path: operationId,
|
|
120
|
-
operationId
|
|
129
|
+
operationId,
|
|
130
|
+
label: toLabel(screenSchema.entity
|
|
131
|
+
? String(screenSchema.entity)
|
|
132
|
+
: operationId),
|
|
133
|
+
group: inferRouteGroup(op, pathKey)
|
|
121
134
|
});
|
|
122
135
|
console.log(`✔ Generated ${operationId}`);
|
|
123
136
|
}
|
|
124
137
|
}
|
|
138
|
+
(0, logger_1.logStep)(`Screens generated: ${routes.length}`);
|
|
125
139
|
/**
|
|
126
140
|
* 4️⃣ Gera arquivo de rotas
|
|
127
141
|
*/
|
|
128
142
|
const routesPath = path_1.default.join(generateUiRoot, 'routes.json');
|
|
129
143
|
fs_1.default.writeFileSync(routesPath, JSON.stringify(routes, null, 2));
|
|
144
|
+
(0, logger_1.logDebug)(`Routes written: ${routesPath}`);
|
|
145
|
+
/**
|
|
146
|
+
* 4.3️⃣ Gera generateui-config.json (não sobrescreve)
|
|
147
|
+
*/
|
|
148
|
+
const configPath = path_1.default.join(generateUiRoot, '..', '..', 'generateui-config.json');
|
|
149
|
+
if (!fs_1.default.existsSync(configPath)) {
|
|
150
|
+
const defaultConfig = {
|
|
151
|
+
appTitle: 'Generate UI',
|
|
152
|
+
defaultRoute: '',
|
|
153
|
+
menu: {
|
|
154
|
+
autoInject: true
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
fs_1.default.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
|
|
158
|
+
(0, logger_1.logDebug)(`Config created: ${configPath}`);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
(0, logger_1.logDebug)(`Config found: ${configPath}`);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* 4.1️⃣ Gera menu inicial (override possível via menu.overrides.json)
|
|
165
|
+
*/
|
|
166
|
+
const menuPath = path_1.default.join(generateUiRoot, 'menu.json');
|
|
167
|
+
const menu = buildMenuFromRoutes(routes);
|
|
168
|
+
fs_1.default.writeFileSync(menuPath, JSON.stringify(menu, null, 2));
|
|
169
|
+
(0, logger_1.logDebug)(`Menu written: ${menuPath}`);
|
|
170
|
+
/**
|
|
171
|
+
* 4.2️⃣ Gera menu.overrides.json (não sobrescreve)
|
|
172
|
+
*/
|
|
173
|
+
const menuOverridesPath = path_1.default.join(generateUiRoot, 'menu.overrides.json');
|
|
174
|
+
if (!fs_1.default.existsSync(menuOverridesPath)) {
|
|
175
|
+
fs_1.default.writeFileSync(menuOverridesPath, JSON.stringify(menu, null, 2));
|
|
176
|
+
(0, logger_1.logDebug)(`Menu overrides created: ${menuOverridesPath}`);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
(0, logger_1.logDebug)(`Menu overrides found: ${menuOverridesPath}`);
|
|
180
|
+
}
|
|
130
181
|
/**
|
|
131
182
|
* 5️⃣ Remove overlays órfãos (endpoint removido)
|
|
132
183
|
*/
|
|
@@ -146,6 +197,15 @@ async function generate(options) {
|
|
|
146
197
|
(0, device_1.incrementFreeGeneration)();
|
|
147
198
|
}
|
|
148
199
|
console.log('✔ Routes generated');
|
|
200
|
+
console.log('');
|
|
201
|
+
console.log('🎉 Next steps');
|
|
202
|
+
console.log(' 1) Generate Angular code:');
|
|
203
|
+
console.log(' generate-ui angular --schemas <your-generate-ui> --features <your-app>/src/app/features');
|
|
204
|
+
console.log(' 2) Customize screens in generate-ui/overlays/');
|
|
205
|
+
console.log(' 3) Customize menu in generate-ui/menu.overrides.json (created once, never overwritten)');
|
|
206
|
+
console.log(' 4) Edit generateui-config.json to set appTitle/defaultRoute/menu.autoInject');
|
|
207
|
+
console.log('');
|
|
208
|
+
(0, logger_1.logTip)('Run with --dev to see detailed logs and file paths.');
|
|
149
209
|
}
|
|
150
210
|
function resolveGenerateUiRoot(projectRoot, output) {
|
|
151
211
|
if (output) {
|
|
@@ -199,6 +259,64 @@ function buildOperationId(method, pathKey, usedOperationIds) {
|
|
|
199
259
|
usedOperationIds.add(candidate);
|
|
200
260
|
return candidate;
|
|
201
261
|
}
|
|
262
|
+
function inferRouteGroup(op, pathKey) {
|
|
263
|
+
const tag = Array.isArray(op?.tags) && op.tags.length
|
|
264
|
+
? String(op.tags[0]).trim()
|
|
265
|
+
: '';
|
|
266
|
+
if (tag)
|
|
267
|
+
return tag;
|
|
268
|
+
const segment = String(pathKey || '')
|
|
269
|
+
.split('/')
|
|
270
|
+
.map(part => part.trim())
|
|
271
|
+
.filter(Boolean)
|
|
272
|
+
.find(part => !part.startsWith('{') && !part.endsWith('}'));
|
|
273
|
+
return segment ? segment : null;
|
|
274
|
+
}
|
|
275
|
+
function buildMenuFromRoutes(routes) {
|
|
276
|
+
const groups = [];
|
|
277
|
+
const ungrouped = [];
|
|
278
|
+
const groupMap = new Map();
|
|
279
|
+
for (const route of routes) {
|
|
280
|
+
const item = {
|
|
281
|
+
id: route.operationId,
|
|
282
|
+
label: toLabel(route.label || route.operationId),
|
|
283
|
+
route: route.path
|
|
284
|
+
};
|
|
285
|
+
const rawGroup = route.group ? String(route.group) : '';
|
|
286
|
+
if (!rawGroup) {
|
|
287
|
+
ungrouped.push(item);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const groupId = toKebab(rawGroup);
|
|
291
|
+
let group = groupMap.get(groupId);
|
|
292
|
+
if (!group) {
|
|
293
|
+
group = {
|
|
294
|
+
id: groupId,
|
|
295
|
+
label: toLabel(rawGroup),
|
|
296
|
+
items: []
|
|
297
|
+
};
|
|
298
|
+
groupMap.set(groupId, group);
|
|
299
|
+
groups.push(group);
|
|
300
|
+
}
|
|
301
|
+
group.items.push(item);
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
groups,
|
|
305
|
+
ungrouped
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function toKebab(value) {
|
|
309
|
+
return String(value)
|
|
310
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
311
|
+
.replace(/[_\s]+/g, '-')
|
|
312
|
+
.toLowerCase();
|
|
313
|
+
}
|
|
314
|
+
function toLabel(value) {
|
|
315
|
+
return String(value)
|
|
316
|
+
.replace(/[_-]/g, ' ')
|
|
317
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
318
|
+
.replace(/\b\w/g, char => char.toUpperCase());
|
|
319
|
+
}
|
|
202
320
|
function httpVerbToPrefix(verb) {
|
|
203
321
|
switch (verb) {
|
|
204
322
|
case 'get':
|
package/dist/commands/login.js
CHANGED
|
@@ -6,25 +6,30 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.login = login;
|
|
7
7
|
const http_1 = __importDefault(require("http"));
|
|
8
8
|
const url_1 = require("url");
|
|
9
|
-
const
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
10
|
const config_1 = require("../runtime/config");
|
|
11
11
|
const user_config_1 = require("../runtime/user-config");
|
|
12
12
|
const open_browser_1 = require("../runtime/open-browser");
|
|
13
13
|
const token_1 = require("../license/token");
|
|
14
14
|
const permissions_1 = require("../license/permissions");
|
|
15
15
|
const telemetry_1 = require("../telemetry");
|
|
16
|
+
const logger_1 = require("../runtime/logger");
|
|
16
17
|
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
17
18
|
async function login(options) {
|
|
18
19
|
void (0, telemetry_1.trackCommand)('login', options.telemetryEnabled);
|
|
20
|
+
(0, logger_1.logStep)('Starting login flow');
|
|
19
21
|
const token = await waitForLogin();
|
|
20
22
|
(0, token_1.saveToken)(token);
|
|
23
|
+
(0, logger_1.logDebug)('Token saved');
|
|
24
|
+
let permissionsLoaded = false;
|
|
21
25
|
try {
|
|
22
26
|
await (0, permissions_1.fetchPermissions)();
|
|
27
|
+
permissionsLoaded = true;
|
|
23
28
|
}
|
|
24
29
|
catch {
|
|
25
|
-
|
|
30
|
+
console.warn('⚠ Não foi possível validar a licença agora. Verifique sua conexão e rode o comando novamente se necessário.');
|
|
26
31
|
}
|
|
27
|
-
const email =
|
|
32
|
+
const email = resolveLoginEmail();
|
|
28
33
|
if (email) {
|
|
29
34
|
(0, user_config_1.updateUserConfig)((config) => ({
|
|
30
35
|
...config,
|
|
@@ -32,22 +37,27 @@ async function login(options) {
|
|
|
32
37
|
}));
|
|
33
38
|
}
|
|
34
39
|
await (0, telemetry_1.trackLogin)(email, options.telemetryEnabled);
|
|
35
|
-
console.log(
|
|
40
|
+
console.log(permissionsLoaded
|
|
41
|
+
? '✔ Login completo'
|
|
42
|
+
: '✔ Login completo (verificação pendente)');
|
|
36
43
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
+
function resolveLoginEmail() {
|
|
45
|
+
const envEmail = process.env.GIT_AUTHOR_EMAIL ||
|
|
46
|
+
process.env.GIT_COMMITTER_EMAIL ||
|
|
47
|
+
process.env.EMAIL;
|
|
48
|
+
if (envEmail && envEmail.trim().length) {
|
|
49
|
+
return envEmail.trim();
|
|
50
|
+
}
|
|
44
51
|
try {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
const output = (0, child_process_1.execSync)('git config --get user.email', {
|
|
53
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
54
|
+
})
|
|
55
|
+
.toString()
|
|
56
|
+
.trim();
|
|
57
|
+
return output.length ? output : null;
|
|
48
58
|
}
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
51
61
|
}
|
|
52
62
|
}
|
|
53
63
|
async function waitForLogin() {
|
|
@@ -68,7 +78,7 @@ async function waitForLogin() {
|
|
|
68
78
|
res.end('Missing access token');
|
|
69
79
|
return;
|
|
70
80
|
}
|
|
71
|
-
const expiresAt = expiresAtParam ||
|
|
81
|
+
const expiresAt = normalizeExpiresAt(expiresAtParam) ||
|
|
72
82
|
new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
73
83
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
74
84
|
res.end(`<!doctype html>
|
|
@@ -158,7 +168,24 @@ async function waitForLogin() {
|
|
|
158
168
|
url.searchParams.set('api_base', (0, config_1.getApiBaseUrl)());
|
|
159
169
|
loginUrl = url.toString();
|
|
160
170
|
console.log(`Open this URL to finish login: ${loginUrl}`);
|
|
171
|
+
(0, logger_1.logDebug)(`Login callback listening on ${redirectUri}`);
|
|
161
172
|
(0, open_browser_1.openBrowser)(loginUrl);
|
|
162
173
|
});
|
|
163
174
|
});
|
|
164
175
|
}
|
|
176
|
+
function normalizeExpiresAt(value) {
|
|
177
|
+
if (!value)
|
|
178
|
+
return null;
|
|
179
|
+
const trimmed = value.trim();
|
|
180
|
+
if (!trimmed.length)
|
|
181
|
+
return null;
|
|
182
|
+
const asNumber = Number(trimmed);
|
|
183
|
+
if (Number.isFinite(asNumber)) {
|
|
184
|
+
const ms = asNumber < 1e12 ? asNumber * 1000 : asNumber;
|
|
185
|
+
return new Date(ms).toISOString();
|
|
186
|
+
}
|
|
187
|
+
const parsed = new Date(trimmed);
|
|
188
|
+
if (Number.isNaN(parsed.getTime()))
|
|
189
|
+
return null;
|
|
190
|
+
return parsed.toISOString();
|
|
191
|
+
}
|