generate-ui-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/dist/commands/angular.js +39 -0
- package/dist/commands/generate.js +207 -0
- package/dist/commands/login.js +132 -0
- package/dist/generators/angular/feature.generator.js +1211 -0
- package/dist/generators/angular/routes.generator.js +45 -0
- package/dist/generators/form.generator.js +70 -0
- package/dist/generators/infer-entity.js +20 -0
- package/dist/generators/infer-submit-wrapper.js +14 -0
- package/dist/generators/screen.generator.js +134 -0
- package/dist/generators/screen.merge.js +202 -0
- package/dist/index.js +105 -0
- package/dist/license/device.js +58 -0
- package/dist/license/guard.js +9 -0
- package/dist/license/permissions.js +93 -0
- package/dist/license/token.js +46 -0
- package/dist/openapi/load-openapi.js +10 -0
- package/dist/overlay/sync-overlay.js +26 -0
- package/dist/runtime/config.js +20 -0
- package/dist/runtime/open-browser.js +26 -0
- package/dist/telemetry.js +40 -0
- package/package.json +53 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateRoutes = generateRoutes;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
function generateRoutes(routes, featuresRoot) {
|
|
10
|
+
const appRoot = path_1.default.resolve(featuresRoot, '..');
|
|
11
|
+
const out = path_1.default.join(appRoot, 'generated', 'routes.gen.ts');
|
|
12
|
+
fs_1.default.mkdirSync(path_1.default.dirname(out), { recursive: true });
|
|
13
|
+
const content = `
|
|
14
|
+
import { Routes } from '@angular/router'
|
|
15
|
+
|
|
16
|
+
export const generatedRoutes: Routes = [
|
|
17
|
+
${routes
|
|
18
|
+
.flatMap(r => {
|
|
19
|
+
const base = ` {
|
|
20
|
+
path: '${r.path}',
|
|
21
|
+
loadComponent: () =>
|
|
22
|
+
import('../features/${r.folder}/${r.fileBase}.component')
|
|
23
|
+
.then(m => m.${r.component})
|
|
24
|
+
}`;
|
|
25
|
+
const pascal = toPascalCase(r.path);
|
|
26
|
+
if (pascal === r.path)
|
|
27
|
+
return [base];
|
|
28
|
+
const alias = ` {
|
|
29
|
+
path: '${pascal}',
|
|
30
|
+
loadComponent: () =>
|
|
31
|
+
import('../features/${r.folder}/${r.fileBase}.component')
|
|
32
|
+
.then(m => m.${r.component})
|
|
33
|
+
}`;
|
|
34
|
+
return [base, alias];
|
|
35
|
+
})
|
|
36
|
+
.join(',\n')}
|
|
37
|
+
]
|
|
38
|
+
`;
|
|
39
|
+
fs_1.default.writeFileSync(out, content);
|
|
40
|
+
}
|
|
41
|
+
function toPascalCase(value) {
|
|
42
|
+
if (!value)
|
|
43
|
+
return value;
|
|
44
|
+
return value[0].toUpperCase() + value.slice(1);
|
|
45
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateFields = generateFields;
|
|
4
|
+
function mapSchema(schema) {
|
|
5
|
+
if (!schema || !schema.properties)
|
|
6
|
+
return [];
|
|
7
|
+
return Object.entries(schema.properties).map(([name, prop]) => ({
|
|
8
|
+
name,
|
|
9
|
+
type: prop.type ?? 'string',
|
|
10
|
+
required: schema.required?.includes(name) ?? false,
|
|
11
|
+
label: null,
|
|
12
|
+
placeholder: null,
|
|
13
|
+
ui: prop.type === 'array' ? 'tags' : null,
|
|
14
|
+
options: prop.enum ?? null,
|
|
15
|
+
defaultValue: prop.default ?? null,
|
|
16
|
+
validations: []
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* REGRA CRÍTICA:
|
|
21
|
+
* - Wrapper (ex: article, user) NÃO vira campo de formulário
|
|
22
|
+
* - Se houver apenas 1 propriedade object, ela é o wrapper
|
|
23
|
+
*/
|
|
24
|
+
function generateFields(endpoint, api) {
|
|
25
|
+
const schema = endpoint.requestBody?.content?.['application/json']?.schema;
|
|
26
|
+
const unwrapped = unwrapSchema(schema);
|
|
27
|
+
const baseSchema = unwrapped ?? schema;
|
|
28
|
+
if (!baseSchema || !baseSchema.properties)
|
|
29
|
+
return [];
|
|
30
|
+
const method = String(endpoint.method || '').toLowerCase();
|
|
31
|
+
const entity = String(endpoint.operationId || '').replace(/^(Create|Update|Get|Delete)/, '');
|
|
32
|
+
let finalSchema = baseSchema;
|
|
33
|
+
if (method === 'post' && entity && api?.components?.schemas) {
|
|
34
|
+
const schemas = api.components.schemas;
|
|
35
|
+
const newSchema = unwrapSchema(schemas[`New${entity}`]) ??
|
|
36
|
+
schemas[`New${entity}`];
|
|
37
|
+
const updateSchema = unwrapSchema(schemas[`Update${entity}`]) ?? schemas[`Update${entity}`];
|
|
38
|
+
if (newSchema && updateSchema) {
|
|
39
|
+
finalSchema = mergeSchemas(newSchema, updateSchema);
|
|
40
|
+
}
|
|
41
|
+
else if (newSchema) {
|
|
42
|
+
finalSchema = newSchema;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return mapSchema(finalSchema);
|
|
46
|
+
}
|
|
47
|
+
function unwrapSchema(schema) {
|
|
48
|
+
if (!schema || !schema.properties)
|
|
49
|
+
return null;
|
|
50
|
+
const propertyNames = Object.keys(schema.properties);
|
|
51
|
+
if (propertyNames.length === 1 &&
|
|
52
|
+
schema.properties[propertyNames[0]]?.type === 'object') {
|
|
53
|
+
return schema.properties[propertyNames[0]];
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
function mergeSchemas(primary, secondary) {
|
|
58
|
+
const merged = {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
...(primary?.properties ?? {}),
|
|
62
|
+
...(secondary?.properties ?? {})
|
|
63
|
+
},
|
|
64
|
+
required: Array.from(new Set([
|
|
65
|
+
...(primary?.required ?? []),
|
|
66
|
+
...(secondary?.required ?? [])
|
|
67
|
+
]))
|
|
68
|
+
};
|
|
69
|
+
return merged;
|
|
70
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inferEntity = inferEntity;
|
|
4
|
+
function inferEntity(endpoint) {
|
|
5
|
+
const parts = endpoint.path
|
|
6
|
+
.split('/')
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
.filter(p => !p.startsWith('{'));
|
|
9
|
+
if (!parts.length)
|
|
10
|
+
return 'Unknown';
|
|
11
|
+
const raw = parts[0];
|
|
12
|
+
// remove plural simples (users -> user)
|
|
13
|
+
const singular = raw.endsWith('s')
|
|
14
|
+
? raw.slice(0, -1)
|
|
15
|
+
: raw;
|
|
16
|
+
return capitalize(singular);
|
|
17
|
+
}
|
|
18
|
+
function capitalize(value) {
|
|
19
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inferSubmitWrapper = inferSubmitWrapper;
|
|
4
|
+
function inferSubmitWrapper(endpoint) {
|
|
5
|
+
const schema = endpoint.requestBody?.content?.['application/json']?.schema;
|
|
6
|
+
if (!schema?.properties)
|
|
7
|
+
return null;
|
|
8
|
+
const keys = Object.keys(schema.properties);
|
|
9
|
+
if (keys.length === 1 &&
|
|
10
|
+
schema.properties[keys[0]]?.type === 'object') {
|
|
11
|
+
return keys[0];
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateScreen = generateScreen;
|
|
4
|
+
const form_generator_1 = require("./form.generator");
|
|
5
|
+
function inferScreen(method, hasInput) {
|
|
6
|
+
if (method === 'get') {
|
|
7
|
+
if (hasInput) {
|
|
8
|
+
return { type: 'form', mode: 'filter' };
|
|
9
|
+
}
|
|
10
|
+
return { type: 'view', mode: 'readonly' };
|
|
11
|
+
}
|
|
12
|
+
if (method === 'post') {
|
|
13
|
+
return { type: 'form', mode: 'create' };
|
|
14
|
+
}
|
|
15
|
+
if (method === 'put' || method === 'patch') {
|
|
16
|
+
return { type: 'form', mode: 'edit' };
|
|
17
|
+
}
|
|
18
|
+
return { type: 'view', mode: 'readonly' };
|
|
19
|
+
}
|
|
20
|
+
function inferActions(method, hasInput) {
|
|
21
|
+
if (method === 'get' && hasInput) {
|
|
22
|
+
return { primary: { type: 'submit', label: 'Buscar' } };
|
|
23
|
+
}
|
|
24
|
+
if (method === 'post') {
|
|
25
|
+
return { primary: { type: 'submit', label: 'Criar' } };
|
|
26
|
+
}
|
|
27
|
+
if (method === 'put' || method === 'patch') {
|
|
28
|
+
return { primary: { type: 'submit', label: 'Salvar' } };
|
|
29
|
+
}
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
function inferSubmitWrap(endpoint) {
|
|
33
|
+
const schema = endpoint.requestBody?.content?.['application/json']?.schema;
|
|
34
|
+
if (!schema?.properties)
|
|
35
|
+
return null;
|
|
36
|
+
const keys = Object.keys(schema.properties);
|
|
37
|
+
if (keys.length === 1)
|
|
38
|
+
return keys[0];
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function generateScreen(endpoint, api) {
|
|
42
|
+
const fields = (0, form_generator_1.generateFields)(endpoint, api);
|
|
43
|
+
const method = endpoint.method.toLowerCase();
|
|
44
|
+
const baseUrl = api?.servers?.[0]?.url;
|
|
45
|
+
const queryParams = extractQueryParams(endpoint, api);
|
|
46
|
+
const pathParams = extractPathParams(endpoint.path, api);
|
|
47
|
+
const hasInput = fields.length > 0 ||
|
|
48
|
+
queryParams.length > 0 ||
|
|
49
|
+
pathParams.length > 0;
|
|
50
|
+
const openapiVersion = api?.info?.version || 'unknown';
|
|
51
|
+
const screenMeta = buildMeta(endpoint.operationId, 'api', openapiVersion);
|
|
52
|
+
return {
|
|
53
|
+
meta: screenMeta,
|
|
54
|
+
entity: endpoint.summary
|
|
55
|
+
? String(endpoint.summary).trim()
|
|
56
|
+
: endpoint.operationId.replace(/^(Create|Update|Get)/, ''),
|
|
57
|
+
screen: inferScreen(method, hasInput),
|
|
58
|
+
description: endpoint.description ?? null,
|
|
59
|
+
api: {
|
|
60
|
+
operationId: endpoint.operationId,
|
|
61
|
+
endpoint: endpoint.path,
|
|
62
|
+
method,
|
|
63
|
+
baseUrl,
|
|
64
|
+
pathParams,
|
|
65
|
+
queryParams,
|
|
66
|
+
submit: inferSubmitWrap(endpoint)
|
|
67
|
+
? { wrap: inferSubmitWrap(endpoint) }
|
|
68
|
+
: undefined
|
|
69
|
+
},
|
|
70
|
+
layout: { type: 'single' },
|
|
71
|
+
fields: decorateFields(fields, 'body', openapiVersion),
|
|
72
|
+
actions: inferActions(method, hasInput),
|
|
73
|
+
data: {}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function extractQueryParams(endpoint, api) {
|
|
77
|
+
const params = endpoint?.parameters ?? [];
|
|
78
|
+
return params
|
|
79
|
+
.filter((param) => param?.in === 'query')
|
|
80
|
+
.map((param) => ({
|
|
81
|
+
name: param.name,
|
|
82
|
+
type: param.schema?.type ?? 'string',
|
|
83
|
+
required: Boolean(param.required),
|
|
84
|
+
label: toLabel(param.name),
|
|
85
|
+
placeholder: param.schema?.example ?? null,
|
|
86
|
+
options: param.schema?.enum ?? null,
|
|
87
|
+
defaultValue: param.schema?.default ?? null,
|
|
88
|
+
hint: param.description ?? null,
|
|
89
|
+
meta: buildMeta(`query:${param.name}`, 'api', api?.info?.version || 'unknown')
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
function extractPathParams(path, api) {
|
|
93
|
+
const params = [];
|
|
94
|
+
const regex = /{([^}]+)}/g;
|
|
95
|
+
let match = regex.exec(path);
|
|
96
|
+
while (match) {
|
|
97
|
+
params.push(match[1]);
|
|
98
|
+
match = regex.exec(path);
|
|
99
|
+
}
|
|
100
|
+
return params.map(name => ({
|
|
101
|
+
name,
|
|
102
|
+
type: 'string',
|
|
103
|
+
required: true,
|
|
104
|
+
label: toLabel(name),
|
|
105
|
+
placeholder: toLabel(name),
|
|
106
|
+
options: null,
|
|
107
|
+
defaultValue: null,
|
|
108
|
+
hint: null,
|
|
109
|
+
meta: buildMeta(`path:${name}`, 'api', api?.info?.version || 'unknown')
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
function toLabel(value) {
|
|
113
|
+
return String(value)
|
|
114
|
+
.replace(/[_-]/g, ' ')
|
|
115
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
116
|
+
.replace(/\b\w/g, char => char.toUpperCase());
|
|
117
|
+
}
|
|
118
|
+
function buildMeta(id, source, openapiVersion) {
|
|
119
|
+
return {
|
|
120
|
+
id,
|
|
121
|
+
source,
|
|
122
|
+
lastChangedBy: source,
|
|
123
|
+
introducedBy: source,
|
|
124
|
+
openapiVersion,
|
|
125
|
+
autoAdded: false,
|
|
126
|
+
userRemoved: false
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function decorateFields(fields, scope, openapiVersion) {
|
|
130
|
+
return fields.map(field => ({
|
|
131
|
+
...field,
|
|
132
|
+
meta: field.meta || buildMeta(`${scope}:${field.name}`, 'api', openapiVersion)
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mergeScreen = mergeScreen;
|
|
4
|
+
const PRESENTATION_KEYS = [
|
|
5
|
+
'label',
|
|
6
|
+
'placeholder',
|
|
7
|
+
'hint',
|
|
8
|
+
'info',
|
|
9
|
+
'ui',
|
|
10
|
+
'group',
|
|
11
|
+
'hidden'
|
|
12
|
+
];
|
|
13
|
+
function mergeScreen(nextScreen, overlay, prevGenerated, options) {
|
|
14
|
+
const debug = [];
|
|
15
|
+
if (!overlay) {
|
|
16
|
+
return { screen: normalizeScreen(nextScreen, options), debug };
|
|
17
|
+
}
|
|
18
|
+
const normalizedNext = normalizeScreen(nextScreen, options);
|
|
19
|
+
const normalizedOverlay = normalizeScreen(overlay, options, 'user');
|
|
20
|
+
const normalizedPrev = prevGenerated
|
|
21
|
+
? normalizeScreen(prevGenerated, options)
|
|
22
|
+
: null;
|
|
23
|
+
const merged = {
|
|
24
|
+
...normalizedNext,
|
|
25
|
+
entity: normalizedOverlay.entity ?? normalizedNext.entity,
|
|
26
|
+
screen: normalizedOverlay.screen ?? normalizedNext.screen,
|
|
27
|
+
layout: normalizedOverlay.layout ?? normalizedNext.layout,
|
|
28
|
+
actions: mergeActions(normalizedNext.actions, normalizedOverlay.actions)
|
|
29
|
+
};
|
|
30
|
+
merged.api = {
|
|
31
|
+
...normalizedNext.api,
|
|
32
|
+
pathParams: mergeFieldList(normalizedNext.api?.pathParams ?? [], normalizedOverlay.api?.pathParams ?? [], normalizedPrev?.api?.pathParams ?? [], options, debug, 'path'),
|
|
33
|
+
queryParams: mergeFieldList(normalizedNext.api?.queryParams ?? [], normalizedOverlay.api?.queryParams ?? [], normalizedPrev?.api?.queryParams ?? [], options, debug, 'query')
|
|
34
|
+
};
|
|
35
|
+
merged.fields = mergeFieldList(normalizedNext.fields ?? [], normalizedOverlay.fields ?? [], normalizedPrev?.fields ?? [], options, debug, 'body');
|
|
36
|
+
merged.meta = mergeMeta(normalizedNext.meta, normalizedOverlay.meta, options.openapiVersion);
|
|
37
|
+
return { screen: merged, debug };
|
|
38
|
+
}
|
|
39
|
+
function mergeActions(nextActions, overlayActions) {
|
|
40
|
+
if (!overlayActions)
|
|
41
|
+
return nextActions;
|
|
42
|
+
const merged = { ...nextActions };
|
|
43
|
+
if (overlayActions.primary?.label) {
|
|
44
|
+
merged.primary = merged.primary || {};
|
|
45
|
+
merged.primary.label = overlayActions.primary.label;
|
|
46
|
+
}
|
|
47
|
+
return merged;
|
|
48
|
+
}
|
|
49
|
+
function mergeFieldList(nextFields, overlayFields, prevFields, options, debug, scope) {
|
|
50
|
+
const nextMap = indexById(nextFields, scope);
|
|
51
|
+
const overlayMap = indexById(overlayFields, scope);
|
|
52
|
+
const prevMap = indexById(prevFields, scope);
|
|
53
|
+
const result = [];
|
|
54
|
+
const overlayOrder = overlayFields.map(field => getId(field, scope));
|
|
55
|
+
const used = new Set();
|
|
56
|
+
for (const id of overlayOrder) {
|
|
57
|
+
const overlayField = overlayMap.get(id);
|
|
58
|
+
const nextField = nextMap.get(id);
|
|
59
|
+
const prevField = prevMap.get(id);
|
|
60
|
+
used.add(id);
|
|
61
|
+
if (!nextField) {
|
|
62
|
+
debug.push(`REMOVED_BY_API ${id}`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (overlayField?.meta?.userRemoved) {
|
|
66
|
+
result.push({
|
|
67
|
+
...nextField,
|
|
68
|
+
hidden: true,
|
|
69
|
+
meta: {
|
|
70
|
+
...mergeMeta(nextField.meta, overlayField.meta, options.openapiVersion),
|
|
71
|
+
userRemoved: true,
|
|
72
|
+
lastChangedBy: 'user'
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
debug.push(`PRESERVE_USER_REMOVED ${id}`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
result.push(mergeField(nextField, overlayField, prevField, options, debug));
|
|
79
|
+
}
|
|
80
|
+
for (const [id, nextField] of nextMap.entries()) {
|
|
81
|
+
if (used.has(id))
|
|
82
|
+
continue;
|
|
83
|
+
const prevField = prevMap.get(id);
|
|
84
|
+
if (prevField && !overlayMap.has(id)) {
|
|
85
|
+
result.push({
|
|
86
|
+
...nextField,
|
|
87
|
+
hidden: true,
|
|
88
|
+
meta: {
|
|
89
|
+
...mergeMeta(nextField.meta, prevField.meta, options.openapiVersion),
|
|
90
|
+
userRemoved: true,
|
|
91
|
+
lastChangedBy: 'user'
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
debug.push(`USER_REMOVED_TOMBSTONE ${id}`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const autoAdded = !nextField.required;
|
|
98
|
+
result.push({
|
|
99
|
+
...nextField,
|
|
100
|
+
hidden: autoAdded ? true : nextField.hidden,
|
|
101
|
+
meta: {
|
|
102
|
+
...mergeMeta(nextField.meta, nextField.meta, options.openapiVersion),
|
|
103
|
+
autoAdded
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
debug.push(`ADDED_BY_API ${id}`);
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
function mergeField(nextField, overlayField, prevField, options, debug) {
|
|
111
|
+
const merged = { ...nextField };
|
|
112
|
+
const meta = mergeMeta(nextField.meta, overlayField?.meta, options.openapiVersion);
|
|
113
|
+
for (const key of PRESENTATION_KEYS) {
|
|
114
|
+
if (overlayField && overlayField[key] !== undefined) {
|
|
115
|
+
merged[key] = overlayField[key];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (prevField && prevField.required !== nextField.required) {
|
|
119
|
+
if (nextField.required) {
|
|
120
|
+
merged.hidden = false;
|
|
121
|
+
debug.push(`OPTIONAL_TO_REQUIRED ${meta.id}`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
debug.push(`REQUIRED_TO_OPTIONAL ${meta.id}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (prevField && prevField.type !== nextField.type) {
|
|
128
|
+
merged.ui = undefined;
|
|
129
|
+
merged.options = nextField.options ?? null;
|
|
130
|
+
debug.push(`TYPE_CHANGED ${meta.id}`);
|
|
131
|
+
}
|
|
132
|
+
const prevEnum = Array.isArray(prevField?.options);
|
|
133
|
+
const nextEnum = Array.isArray(nextField?.options);
|
|
134
|
+
if (prevEnum && !nextEnum) {
|
|
135
|
+
merged.options = null;
|
|
136
|
+
debug.push(`ENUM_TO_STRING ${meta.id}`);
|
|
137
|
+
}
|
|
138
|
+
if (!prevEnum && nextEnum) {
|
|
139
|
+
merged.options = nextField.options;
|
|
140
|
+
debug.push(`STRING_TO_ENUM ${meta.id}`);
|
|
141
|
+
}
|
|
142
|
+
merged.meta = meta;
|
|
143
|
+
return merged;
|
|
144
|
+
}
|
|
145
|
+
function normalizeScreen(screen, options, fallbackSource = 'api') {
|
|
146
|
+
if (!screen)
|
|
147
|
+
return screen;
|
|
148
|
+
const openapiVersion = options.openapiVersion;
|
|
149
|
+
const meta = screen.meta || buildMeta(screen.api?.operationId || 'screen', fallbackSource, openapiVersion);
|
|
150
|
+
return {
|
|
151
|
+
...screen,
|
|
152
|
+
meta,
|
|
153
|
+
fields: normalizeFieldList(screen.fields, 'body', fallbackSource, openapiVersion),
|
|
154
|
+
api: {
|
|
155
|
+
...screen.api,
|
|
156
|
+
pathParams: normalizeFieldList(screen.api?.pathParams, 'path', fallbackSource, openapiVersion),
|
|
157
|
+
queryParams: normalizeFieldList(screen.api?.queryParams, 'query', fallbackSource, openapiVersion)
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function normalizeFieldList(list, scope, fallbackSource, openapiVersion) {
|
|
162
|
+
if (!Array.isArray(list))
|
|
163
|
+
return [];
|
|
164
|
+
return list.map(field => ({
|
|
165
|
+
...field,
|
|
166
|
+
meta: field.meta || buildMeta(`${scope}:${field.name}`, fallbackSource, openapiVersion)
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
function indexById(fields, scope) {
|
|
170
|
+
const map = new Map();
|
|
171
|
+
for (const field of fields) {
|
|
172
|
+
map.set(getId(field, scope), field);
|
|
173
|
+
}
|
|
174
|
+
return map;
|
|
175
|
+
}
|
|
176
|
+
function getId(field, scope) {
|
|
177
|
+
return field?.meta?.id || `${scope}:${field?.name}`;
|
|
178
|
+
}
|
|
179
|
+
function mergeMeta(nextMeta, overlayMeta, openapiVersion) {
|
|
180
|
+
const base = {
|
|
181
|
+
...(nextMeta || overlayMeta),
|
|
182
|
+
openapiVersion
|
|
183
|
+
};
|
|
184
|
+
if (overlayMeta) {
|
|
185
|
+
base.source = overlayMeta.source || base.source;
|
|
186
|
+
base.introducedBy = overlayMeta.introducedBy || base.introducedBy;
|
|
187
|
+
base.lastChangedBy = overlayMeta.lastChangedBy || base.lastChangedBy;
|
|
188
|
+
base.userRemoved = overlayMeta.userRemoved || base.userRemoved;
|
|
189
|
+
}
|
|
190
|
+
return base;
|
|
191
|
+
}
|
|
192
|
+
function buildMeta(id, source, openapiVersion) {
|
|
193
|
+
return {
|
|
194
|
+
id,
|
|
195
|
+
source,
|
|
196
|
+
lastChangedBy: source,
|
|
197
|
+
introducedBy: source,
|
|
198
|
+
openapiVersion,
|
|
199
|
+
autoAdded: false,
|
|
200
|
+
userRemoved: false
|
|
201
|
+
};
|
|
202
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const generate_1 = require("./commands/generate");
|
|
6
|
+
const angular_1 = require("./commands/angular");
|
|
7
|
+
const login_1 = require("./commands/login");
|
|
8
|
+
const config_1 = require("./runtime/config");
|
|
9
|
+
const program = new commander_1.Command();
|
|
10
|
+
program
|
|
11
|
+
.name('generate-ui')
|
|
12
|
+
.description('Generate UI from OpenAPI')
|
|
13
|
+
.version((0, config_1.getCliVersion)())
|
|
14
|
+
.option('--no-telemetry', 'Disable telemetry');
|
|
15
|
+
/**
|
|
16
|
+
* 1️⃣ OpenAPI → Screen schemas
|
|
17
|
+
*/
|
|
18
|
+
program
|
|
19
|
+
.command('generate')
|
|
20
|
+
.description('Generate screen schemas from OpenAPI')
|
|
21
|
+
.requiredOption('-o, --openapi <path>', 'OpenAPI file')
|
|
22
|
+
.option('-d, --debug', 'Explain merge decisions')
|
|
23
|
+
.action(async (options) => {
|
|
24
|
+
const { telemetry } = program.opts();
|
|
25
|
+
try {
|
|
26
|
+
await (0, generate_1.generate)({
|
|
27
|
+
openapi: options.openapi,
|
|
28
|
+
debug: options.debug,
|
|
29
|
+
telemetryEnabled: telemetry,
|
|
30
|
+
telemetryCommand: 'generate'
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
handleCliError(error);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* 2️⃣ Screen schemas → Angular code
|
|
39
|
+
*/
|
|
40
|
+
program
|
|
41
|
+
.command('angular')
|
|
42
|
+
.description('Generate Angular code from screen schemas')
|
|
43
|
+
.requiredOption('-s, --schemas <path>', 'Directory containing generate-ui (with overlays/)')
|
|
44
|
+
.requiredOption('-f, --features <path>', 'Angular features output directory')
|
|
45
|
+
.action(async (options) => {
|
|
46
|
+
const { telemetry } = program.opts();
|
|
47
|
+
try {
|
|
48
|
+
await (0, angular_1.angular)({
|
|
49
|
+
schemasPath: options.schemas,
|
|
50
|
+
featuresPath: options.features,
|
|
51
|
+
telemetryEnabled: telemetry
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
handleCliError(error);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
/**
|
|
59
|
+
* 3️⃣ Login (Dev plan)
|
|
60
|
+
*/
|
|
61
|
+
program
|
|
62
|
+
.command('login')
|
|
63
|
+
.description('Login to unlock Dev features')
|
|
64
|
+
.action(async () => {
|
|
65
|
+
const { telemetry } = program.opts();
|
|
66
|
+
try {
|
|
67
|
+
await (0, login_1.login)({ telemetryEnabled: telemetry });
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
handleCliError(error);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
/**
|
|
74
|
+
* 4️⃣ Regenerate with safe merge
|
|
75
|
+
*/
|
|
76
|
+
program
|
|
77
|
+
.command('regenerate')
|
|
78
|
+
.description('Regenerate screen schemas with safe merge')
|
|
79
|
+
.requiredOption('-o, --openapi <path>', 'OpenAPI file')
|
|
80
|
+
.option('-d, --debug', 'Explain merge decisions')
|
|
81
|
+
.action(async (options) => {
|
|
82
|
+
const { telemetry } = program.opts();
|
|
83
|
+
try {
|
|
84
|
+
await (0, generate_1.generate)({
|
|
85
|
+
openapi: options.openapi,
|
|
86
|
+
debug: options.debug,
|
|
87
|
+
telemetryEnabled: telemetry,
|
|
88
|
+
telemetryCommand: 'regenerate',
|
|
89
|
+
requireSafeRegeneration: true
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
handleCliError(error);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
function handleCliError(error) {
|
|
97
|
+
if (error instanceof Error) {
|
|
98
|
+
console.error(error.message.replace(/\\n/g, '\n'));
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.error('Unexpected error');
|
|
102
|
+
}
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
program.parse();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadDeviceIdentity = loadDeviceIdentity;
|
|
7
|
+
exports.saveDeviceIdentity = saveDeviceIdentity;
|
|
8
|
+
exports.incrementFreeGeneration = incrementFreeGeneration;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const os_1 = __importDefault(require("os"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
13
|
+
const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.generateui');
|
|
14
|
+
const DEVICE_PATH = path_1.default.join(CONFIG_DIR, 'device.json');
|
|
15
|
+
function ensureConfigDir() {
|
|
16
|
+
fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
function newDeviceIdentity() {
|
|
19
|
+
const deviceId = typeof crypto_1.default.randomUUID === 'function'
|
|
20
|
+
? crypto_1.default.randomUUID()
|
|
21
|
+
: crypto_1.default.randomBytes(16).toString('hex');
|
|
22
|
+
return {
|
|
23
|
+
deviceId,
|
|
24
|
+
createdAt: new Date().toISOString(),
|
|
25
|
+
freeGenerationsUsed: 0
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function loadDeviceIdentity() {
|
|
29
|
+
ensureConfigDir();
|
|
30
|
+
if (!fs_1.default.existsSync(DEVICE_PATH)) {
|
|
31
|
+
const identity = newDeviceIdentity();
|
|
32
|
+
fs_1.default.writeFileSync(DEVICE_PATH, JSON.stringify(identity, null, 2));
|
|
33
|
+
return identity;
|
|
34
|
+
}
|
|
35
|
+
const raw = fs_1.default.readFileSync(DEVICE_PATH, 'utf-8');
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(raw);
|
|
38
|
+
if (!parsed.deviceId) {
|
|
39
|
+
throw new Error('Invalid device identity');
|
|
40
|
+
}
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
const identity = newDeviceIdentity();
|
|
45
|
+
fs_1.default.writeFileSync(DEVICE_PATH, JSON.stringify(identity, null, 2));
|
|
46
|
+
return identity;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function saveDeviceIdentity(identity) {
|
|
50
|
+
ensureConfigDir();
|
|
51
|
+
fs_1.default.writeFileSync(DEVICE_PATH, JSON.stringify(identity, null, 2));
|
|
52
|
+
}
|
|
53
|
+
function incrementFreeGeneration() {
|
|
54
|
+
const identity = loadDeviceIdentity();
|
|
55
|
+
identity.freeGenerationsUsed += 1;
|
|
56
|
+
saveDeviceIdentity(identity);
|
|
57
|
+
return identity;
|
|
58
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requireFeature = requireFeature;
|
|
4
|
+
function requireFeature(features, featureKey, reason) {
|
|
5
|
+
if (!features[featureKey]) {
|
|
6
|
+
const details = reason ? ` ${reason}` : '';
|
|
7
|
+
throw new Error(`Requires Dev plan.${details} Execute \`generate-ui login\` to continue.`);
|
|
8
|
+
}
|
|
9
|
+
}
|