@ojiepermana/angular 21.0.0 → 21.0.2
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/collection.json +25 -0
- package/fesm2022/ojiepermana-angular-generator-api.mjs +67 -0
- package/fesm2022/ojiepermana-angular-generator-api.mjs.map +1 -0
- package/fesm2022/ojiepermana-angular-layout.mjs +76 -56
- package/fesm2022/ojiepermana-angular-layout.mjs.map +1 -1
- package/fesm2022/ojiepermana-angular.mjs +1 -0
- package/fesm2022/ojiepermana-angular.mjs.map +1 -1
- package/generator/api/README.md +183 -0
- package/generator/api/bin/schematics/init/index.js +88 -0
- package/generator/api/bin/schematics/sdk/index.js +58 -0
- package/generator/api/bin/src/config/loader.js +41 -0
- package/generator/api/bin/src/config/schema.js +56 -0
- package/generator/api/bin/src/emit/client.js +246 -0
- package/generator/api/bin/src/emit/metadata.js +295 -0
- package/generator/api/bin/src/emit/models.js +106 -0
- package/generator/api/bin/src/emit/navigation.js +56 -0
- package/generator/api/bin/src/emit/operations.js +122 -0
- package/generator/api/bin/src/emit/public-api.js +54 -0
- package/generator/api/bin/src/emit/services.js +87 -0
- package/generator/api/bin/src/engine.js +65 -0
- package/generator/api/bin/src/layout/per-domain.js +346 -0
- package/generator/api/bin/src/parser/bundle.js +25 -0
- package/generator/api/bin/src/parser/ir.js +320 -0
- package/generator/api/bin/src/parser/types.js +7 -0
- package/generator/api/bin/src/render/template.js +58 -0
- package/generator/api/bin/src/writer/index.js +69 -0
- package/generator/api/schematics/init/schema.json +19 -0
- package/generator/api/schematics/sdk/schema.json +19 -0
- package/generator/api/sdk.config.example.json +22 -0
- package/generator/guide/README.md +84 -0
- package/generator/guide/bin/schematics/build/index.js +35 -0
- package/generator/guide/bin/schematics/init/index.js +70 -0
- package/generator/guide/bin/src/config/loader.js +50 -0
- package/generator/guide/bin/src/config/schema.js +12 -0
- package/generator/guide/bin/src/engine/component.js +73 -0
- package/generator/guide/bin/src/engine/frontmatter.js +42 -0
- package/generator/guide/bin/src/engine/index.js +42 -0
- package/generator/guide/bin/src/engine/naming.js +39 -0
- package/generator/guide/bin/src/engine/render.js +18 -0
- package/generator/guide/bin/src/engine/routes.js +106 -0
- package/generator/guide/bin/src/engine/walk.js +35 -0
- package/generator/guide/guide.config.example.json +9 -0
- package/generator/guide/schematics/build/schema.json +14 -0
- package/generator/guide/schematics/init/schema.json +19 -0
- package/package.json +10 -3
- package/types/ojiepermana-angular-generator-api.d.ts +85 -0
- package/types/ojiepermana-angular-layout.d.ts +2 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildIR = buildIR;
|
|
4
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
|
|
5
|
+
/**
|
|
6
|
+
* Build the Intermediate Representation from a bundled OpenAPI document.
|
|
7
|
+
*
|
|
8
|
+
* Expectations about input:
|
|
9
|
+
* - `$ref` strings in the form `#/components/schemas/<Name>` (or parameters).
|
|
10
|
+
* - OpenAPI 3.0.x / 3.1.x / 3.2.x (nullable via `type: ['string', 'null']` or
|
|
11
|
+
* `nullable: true`).
|
|
12
|
+
*/
|
|
13
|
+
function buildIR(doc) {
|
|
14
|
+
const info = doc.info ?? {};
|
|
15
|
+
const servers = Array.isArray(doc.servers)
|
|
16
|
+
? doc.servers.map((s) => ({ url: String(s.url ?? ''), description: s.description }))
|
|
17
|
+
: [];
|
|
18
|
+
const tags = Array.isArray(doc.tags)
|
|
19
|
+
? doc.tags.map((t) => ({
|
|
20
|
+
name: String(t.name),
|
|
21
|
+
parent: t.parent,
|
|
22
|
+
kind: t.kind,
|
|
23
|
+
xIcon: t['x-icon'],
|
|
24
|
+
description: t.description,
|
|
25
|
+
}))
|
|
26
|
+
: [];
|
|
27
|
+
const schemas = buildSchemas(doc.components?.schemas);
|
|
28
|
+
const operations = buildOperations(doc.paths, doc.components?.parameters);
|
|
29
|
+
const security = buildSecuritySchemes(doc.components?.securitySchemes);
|
|
30
|
+
return {
|
|
31
|
+
title: String(info.title ?? 'API'),
|
|
32
|
+
version: String(info.version ?? '0.0.0'),
|
|
33
|
+
description: info.description,
|
|
34
|
+
servers,
|
|
35
|
+
tags,
|
|
36
|
+
navigation: buildNavigation(tags),
|
|
37
|
+
schemas,
|
|
38
|
+
operations,
|
|
39
|
+
security,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Schemas
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
function buildSchemas(rawSchemas) {
|
|
46
|
+
if (!rawSchemas)
|
|
47
|
+
return [];
|
|
48
|
+
const names = Object.keys(rawSchemas).sort();
|
|
49
|
+
return names.map((name) => buildSchema(name, rawSchemas[name]));
|
|
50
|
+
}
|
|
51
|
+
function buildSchema(name, raw) {
|
|
52
|
+
const required = Array.isArray(raw?.required) ? raw.required.slice() : [];
|
|
53
|
+
const properties = {};
|
|
54
|
+
// allOf — merge inline: fold each `properties` + required lists into the parent.
|
|
55
|
+
const parts = Array.isArray(raw?.allOf) ? raw.allOf : [];
|
|
56
|
+
const mergedProps = { ...(raw?.properties ?? {}) };
|
|
57
|
+
for (const part of parts) {
|
|
58
|
+
if (part?.properties)
|
|
59
|
+
Object.assign(mergedProps, part.properties);
|
|
60
|
+
if (Array.isArray(part?.required)) {
|
|
61
|
+
for (const r of part.required)
|
|
62
|
+
if (!required.includes(r))
|
|
63
|
+
required.push(r);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const [propName, propSchema] of Object.entries(mergedProps)) {
|
|
67
|
+
properties[propName] = buildFieldRule(propSchema, required.includes(propName));
|
|
68
|
+
}
|
|
69
|
+
// Primitive enum schemas → emit as alias.
|
|
70
|
+
const primitiveType = resolvePrimitiveType(raw);
|
|
71
|
+
if (primitiveType && Array.isArray(raw?.enum)) {
|
|
72
|
+
return {
|
|
73
|
+
name,
|
|
74
|
+
description: raw?.description,
|
|
75
|
+
required: [],
|
|
76
|
+
properties: {},
|
|
77
|
+
enumValues: raw.enum,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// Array alias schemas.
|
|
81
|
+
if (resolvedTypeIs(raw, 'array') && raw?.items) {
|
|
82
|
+
const itemRef = refName(raw.items?.$ref);
|
|
83
|
+
const itemType = itemRef ? undefined : resolvePrimitiveType(raw.items);
|
|
84
|
+
return {
|
|
85
|
+
name,
|
|
86
|
+
description: raw?.description,
|
|
87
|
+
required: [],
|
|
88
|
+
properties: {},
|
|
89
|
+
arrayItemRef: itemRef ?? undefined,
|
|
90
|
+
arrayItemType: itemType ?? undefined,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
name,
|
|
95
|
+
description: raw?.description,
|
|
96
|
+
required,
|
|
97
|
+
properties,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function buildFieldRule(raw, required) {
|
|
101
|
+
const rule = { required };
|
|
102
|
+
// $ref → named reference.
|
|
103
|
+
const ref = refName(raw?.$ref);
|
|
104
|
+
if (ref) {
|
|
105
|
+
rule.ref = ref;
|
|
106
|
+
return rule;
|
|
107
|
+
}
|
|
108
|
+
// Array with items.
|
|
109
|
+
if (resolvedTypeIs(raw, 'array') && raw?.items) {
|
|
110
|
+
rule.type = 'array';
|
|
111
|
+
const itemsRef = refName(raw.items?.$ref);
|
|
112
|
+
if (itemsRef)
|
|
113
|
+
rule.itemsRef = itemsRef;
|
|
114
|
+
else
|
|
115
|
+
rule.itemsType = resolvePrimitiveType(raw.items) ?? 'unknown';
|
|
116
|
+
const itemEnum = raw.items?.enum;
|
|
117
|
+
if (Array.isArray(itemEnum))
|
|
118
|
+
rule.enumValues = itemEnum;
|
|
119
|
+
if (raw.description)
|
|
120
|
+
rule.description = raw.description;
|
|
121
|
+
rule.nullable = isNullable(raw);
|
|
122
|
+
return rule;
|
|
123
|
+
}
|
|
124
|
+
// Primitive / object.
|
|
125
|
+
rule.type = resolvePrimitiveType(raw) ?? 'unknown';
|
|
126
|
+
rule.nullable = isNullable(raw);
|
|
127
|
+
if (raw?.format)
|
|
128
|
+
rule.format = raw.format;
|
|
129
|
+
if (raw?.description)
|
|
130
|
+
rule.description = raw.description;
|
|
131
|
+
if (typeof raw?.minLength === 'number')
|
|
132
|
+
rule.minLength = raw.minLength;
|
|
133
|
+
if (typeof raw?.maxLength === 'number')
|
|
134
|
+
rule.maxLength = raw.maxLength;
|
|
135
|
+
if (typeof raw?.minimum === 'number')
|
|
136
|
+
rule.minimum = raw.minimum;
|
|
137
|
+
if (typeof raw?.maximum === 'number')
|
|
138
|
+
rule.maximum = raw.maximum;
|
|
139
|
+
if (typeof raw?.pattern === 'string')
|
|
140
|
+
rule.pattern = raw.pattern;
|
|
141
|
+
if (Array.isArray(raw?.enum))
|
|
142
|
+
rule.enumValues = raw.enum;
|
|
143
|
+
return rule;
|
|
144
|
+
}
|
|
145
|
+
function resolvePrimitiveType(raw) {
|
|
146
|
+
if (!raw)
|
|
147
|
+
return undefined;
|
|
148
|
+
const t = raw.type;
|
|
149
|
+
if (Array.isArray(t)) {
|
|
150
|
+
const nonNull = t.find((x) => x !== 'null');
|
|
151
|
+
return nonNull ?? undefined;
|
|
152
|
+
}
|
|
153
|
+
if (typeof t === 'string')
|
|
154
|
+
return t;
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
function resolvedTypeIs(raw, target) {
|
|
158
|
+
return resolvePrimitiveType(raw) === target;
|
|
159
|
+
}
|
|
160
|
+
function isNullable(raw) {
|
|
161
|
+
if (raw?.nullable === true)
|
|
162
|
+
return true;
|
|
163
|
+
const t = raw?.type;
|
|
164
|
+
if (Array.isArray(t) && t.includes('null'))
|
|
165
|
+
return true;
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
function refName(ref) {
|
|
169
|
+
if (typeof ref !== 'string')
|
|
170
|
+
return undefined;
|
|
171
|
+
const idx = ref.lastIndexOf('/');
|
|
172
|
+
return idx >= 0 ? ref.slice(idx + 1) : ref;
|
|
173
|
+
}
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Operations
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
function buildOperations(paths, sharedParams) {
|
|
178
|
+
if (!paths)
|
|
179
|
+
return [];
|
|
180
|
+
const operations = [];
|
|
181
|
+
for (const [pathKey, pathItem] of Object.entries(paths)) {
|
|
182
|
+
if (!pathItem || typeof pathItem !== 'object')
|
|
183
|
+
continue;
|
|
184
|
+
const pathLevelParams = Array.isArray(pathItem.parameters) ? pathItem.parameters : [];
|
|
185
|
+
for (const method of HTTP_METHODS) {
|
|
186
|
+
const op = pathItem[method];
|
|
187
|
+
if (!op)
|
|
188
|
+
continue;
|
|
189
|
+
operations.push(buildOperation(pathKey, method, op, pathLevelParams, sharedParams));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return operations.sort((a, b) => a.operationId.localeCompare(b.operationId));
|
|
193
|
+
}
|
|
194
|
+
function buildOperation(path, method, op, pathLevelParams, sharedParams) {
|
|
195
|
+
const tags = Array.isArray(op.tags) && op.tags.length ? op.tags.map(String) : ['Default'];
|
|
196
|
+
const params = mergeParams(pathLevelParams, op.parameters, sharedParams);
|
|
197
|
+
const body = op.requestBody;
|
|
198
|
+
const bodySchemaRef = refName(body?.content?.['application/json']?.schema?.$ref) ?? refName(body?.$ref) ?? undefined;
|
|
199
|
+
const responses = {};
|
|
200
|
+
let successStatus;
|
|
201
|
+
let successRef;
|
|
202
|
+
for (const [status, resp] of Object.entries(op.responses ?? {})) {
|
|
203
|
+
const r = resp;
|
|
204
|
+
const ref = refName(r?.content?.['application/json']?.schema?.$ref) ?? refName(r?.$ref) ?? undefined;
|
|
205
|
+
responses[status] = ref;
|
|
206
|
+
if (!successStatus && status.startsWith('2')) {
|
|
207
|
+
successStatus = status;
|
|
208
|
+
successRef = ref;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const securitySchemes = [];
|
|
212
|
+
if (Array.isArray(op.security)) {
|
|
213
|
+
for (const s of op.security) {
|
|
214
|
+
if (s && typeof s === 'object') {
|
|
215
|
+
for (const key of Object.keys(s)) {
|
|
216
|
+
if (!securitySchemes.includes(key))
|
|
217
|
+
securitySchemes.push(key);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const requiredPermissions = Array.isArray(op['x-required-permissions'])
|
|
223
|
+
? op['x-required-permissions'].map(String)
|
|
224
|
+
: [];
|
|
225
|
+
const authorizationNotes = Array.isArray(op['x-authorization-notes'])
|
|
226
|
+
? op['x-authorization-notes'].map(String)
|
|
227
|
+
: [];
|
|
228
|
+
const operationId = op.operationId ?? synthesizeOperationId(method, path);
|
|
229
|
+
return {
|
|
230
|
+
operationId,
|
|
231
|
+
method,
|
|
232
|
+
path,
|
|
233
|
+
tag: tags[0],
|
|
234
|
+
tags,
|
|
235
|
+
summary: op.summary,
|
|
236
|
+
description: op.description,
|
|
237
|
+
params,
|
|
238
|
+
bodySchemaRef,
|
|
239
|
+
bodyRequired: body?.required === true,
|
|
240
|
+
responses,
|
|
241
|
+
successStatus,
|
|
242
|
+
successRef,
|
|
243
|
+
securitySchemes,
|
|
244
|
+
requiredPermissions,
|
|
245
|
+
authorizationNotes,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function mergeParams(pathLevelParams, opParams, sharedParams) {
|
|
249
|
+
const all = [...(pathLevelParams ?? []), ...(opParams ?? [])];
|
|
250
|
+
const seen = new Set();
|
|
251
|
+
const out = [];
|
|
252
|
+
for (const raw of all) {
|
|
253
|
+
const resolved = resolveParamRef(raw, sharedParams);
|
|
254
|
+
if (!resolved?.name || !resolved?.in)
|
|
255
|
+
continue;
|
|
256
|
+
const key = `${resolved.in}:${resolved.name}`;
|
|
257
|
+
if (seen.has(key))
|
|
258
|
+
continue;
|
|
259
|
+
seen.add(key);
|
|
260
|
+
const loc = resolved.in === 'path' || resolved.in === 'query' || resolved.in === 'header' ? resolved.in : 'query';
|
|
261
|
+
const rule = buildFieldRule(resolved.schema ?? {}, !!resolved.required);
|
|
262
|
+
out.push({
|
|
263
|
+
name: resolved.name,
|
|
264
|
+
in: loc,
|
|
265
|
+
required: !!resolved.required,
|
|
266
|
+
rule,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
function resolveParamRef(raw, sharedParams) {
|
|
272
|
+
if (!raw)
|
|
273
|
+
return raw;
|
|
274
|
+
if (typeof raw.$ref !== 'string')
|
|
275
|
+
return raw;
|
|
276
|
+
const name = refName(raw.$ref);
|
|
277
|
+
if (!name || !sharedParams)
|
|
278
|
+
return raw;
|
|
279
|
+
return sharedParams[name] ?? raw;
|
|
280
|
+
}
|
|
281
|
+
function synthesizeOperationId(method, path) {
|
|
282
|
+
const cleaned = path
|
|
283
|
+
.replace(/\/+/g, '/')
|
|
284
|
+
.split('/')
|
|
285
|
+
.filter(Boolean)
|
|
286
|
+
.map((seg) => seg.replace(/^\{/, 'by-').replace(/\}$/, ''))
|
|
287
|
+
.join('-');
|
|
288
|
+
return `${method}-${cleaned || 'root'}`;
|
|
289
|
+
}
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// Security & navigation
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
function buildSecuritySchemes(raw) {
|
|
294
|
+
if (!raw)
|
|
295
|
+
return [];
|
|
296
|
+
return Object.entries(raw).map(([name, s]) => ({
|
|
297
|
+
name,
|
|
298
|
+
type: String(s.type ?? 'unknown'),
|
|
299
|
+
scheme: s.scheme,
|
|
300
|
+
bearerFormat: s.bearerFormat,
|
|
301
|
+
in: s.in,
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
function buildNavigation(tags) {
|
|
305
|
+
const byName = new Map();
|
|
306
|
+
for (const t of tags) {
|
|
307
|
+
byName.set(t.name, { name: t.name, xIcon: t.xIcon, description: t.description, children: [] });
|
|
308
|
+
}
|
|
309
|
+
const roots = [];
|
|
310
|
+
for (const t of tags) {
|
|
311
|
+
const node = byName.get(t.name);
|
|
312
|
+
if (t.parent && byName.has(t.parent)) {
|
|
313
|
+
byName.get(t.parent).children.push(node);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
roots.push(node);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return roots;
|
|
320
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Intermediate Representation (IR) that emitters consume. Keeping this thin +
|
|
4
|
+
* plain-object makes emitters trivial to unit test without pulling in an OpenAPI
|
|
5
|
+
* parser in the test path.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tiny rendering helpers. We use template literals + small utilities rather
|
|
4
|
+
* than a template engine to keep the generator dependency-free and the
|
|
5
|
+
* emitter output predictable.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.kebabCase = kebabCase;
|
|
9
|
+
exports.pascalCase = pascalCase;
|
|
10
|
+
exports.camelCase = camelCase;
|
|
11
|
+
exports.finalize = finalize;
|
|
12
|
+
exports.comment = comment;
|
|
13
|
+
exports.joinBlocks = joinBlocks;
|
|
14
|
+
/** kebab-case (preserving digits) — matches the naming style in `old/api/`. */
|
|
15
|
+
function kebabCase(input) {
|
|
16
|
+
return String(input)
|
|
17
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
18
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
|
|
19
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
20
|
+
.replace(/^-+|-+$/g, '')
|
|
21
|
+
.toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
/** PascalCase — used for class/interface names when sanitizing tags. */
|
|
24
|
+
function pascalCase(input) {
|
|
25
|
+
return String(input)
|
|
26
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
27
|
+
.trim()
|
|
28
|
+
.split(/\s+/)
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.map((w) => w[0].toUpperCase() + w.slice(1))
|
|
31
|
+
.join('');
|
|
32
|
+
}
|
|
33
|
+
/** camelCase — used for variable / method / fn names. */
|
|
34
|
+
function camelCase(input) {
|
|
35
|
+
const pc = pascalCase(input);
|
|
36
|
+
return pc ? pc[0].toLowerCase() + pc.slice(1) : '';
|
|
37
|
+
}
|
|
38
|
+
/** Trim trailing whitespace on every line and ensure a single trailing newline. */
|
|
39
|
+
function finalize(text) {
|
|
40
|
+
const trimmed = text
|
|
41
|
+
.split('\n')
|
|
42
|
+
.map((line) => line.replace(/\s+$/g, ''))
|
|
43
|
+
.join('\n')
|
|
44
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
45
|
+
.replace(/^\s*\n/, '');
|
|
46
|
+
return trimmed.endsWith('\n') ? trimmed : trimmed + '\n';
|
|
47
|
+
}
|
|
48
|
+
/** Render an indented block-comment (1-line friendly). */
|
|
49
|
+
function comment(text) {
|
|
50
|
+
if (!text)
|
|
51
|
+
return '';
|
|
52
|
+
const safe = text.replace(/\*\//g, '*\\/');
|
|
53
|
+
return `/** ${safe} */`;
|
|
54
|
+
}
|
|
55
|
+
/** Join non-empty blocks with a blank line between them. */
|
|
56
|
+
function joinBlocks(blocks) {
|
|
57
|
+
return blocks.filter(Boolean).join('\n\n');
|
|
58
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.writeStandalone = writeStandalone;
|
|
4
|
+
exports.writeLibrary = writeLibrary;
|
|
5
|
+
exports.writeSecondaryEntrypoint = writeSecondaryEntrypoint;
|
|
6
|
+
const template_1 = require("../render/template");
|
|
7
|
+
/**
|
|
8
|
+
* No-op writer for the standalone mode — returns emitted files unchanged. Kept
|
|
9
|
+
* as a named function so modes are composable from a single switch at the
|
|
10
|
+
* engine layer.
|
|
11
|
+
*/
|
|
12
|
+
function writeStandalone(files) {
|
|
13
|
+
return files;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Library mode: add `ng-package.json`, `package.json`, and a top-level README
|
|
17
|
+
* so the `output` folder can be built with ng-packagr directly.
|
|
18
|
+
*/
|
|
19
|
+
function writeLibrary(files, ir, target) {
|
|
20
|
+
const ngPackage = {
|
|
21
|
+
$schema: '../../node_modules/ng-packagr/ng-package.schema.json',
|
|
22
|
+
dest: `../dist/${target.packageName.replace(/^@/, '').replace(/\//g, '-')}`,
|
|
23
|
+
lib: { entryFile: 'public-api.ts' },
|
|
24
|
+
};
|
|
25
|
+
const pkg = {
|
|
26
|
+
name: target.packageName,
|
|
27
|
+
version: target.packageVersion,
|
|
28
|
+
description: `Generated SDK for ${ir.title} ${ir.version}`,
|
|
29
|
+
peerDependencies: {
|
|
30
|
+
'@angular/common': '>=20.0.0',
|
|
31
|
+
'@angular/core': '>=20.0.0',
|
|
32
|
+
rxjs: '>=7.0.0',
|
|
33
|
+
},
|
|
34
|
+
sideEffects: false,
|
|
35
|
+
};
|
|
36
|
+
return [
|
|
37
|
+
...files,
|
|
38
|
+
{
|
|
39
|
+
path: 'ng-package.json',
|
|
40
|
+
content: (0, template_1.finalize)(JSON.stringify(ngPackage, null, 2)),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
path: 'package.json',
|
|
44
|
+
content: (0, template_1.finalize)(JSON.stringify(pkg, null, 2)),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
path: 'README.md',
|
|
48
|
+
content: (0, template_1.finalize)(`# ${target.packageName}\n\nGenerated SDK for **${ir.title}** v${ir.version}.\n\nThis folder was generated by the local SDK generator. Do not edit by hand.\n`),
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Secondary-entrypoint mode: same file set as standalone, plus a minimal
|
|
54
|
+
* `ng-package.json` pointing at `public-api.ts`. Meant to live **inside** an
|
|
55
|
+
* existing library (e.g. `projects/angular/sdk-api/`) so ng-packagr picks it
|
|
56
|
+
* up automatically as a secondary entrypoint of the parent library.
|
|
57
|
+
*/
|
|
58
|
+
function writeSecondaryEntrypoint(files, _ir, _target) {
|
|
59
|
+
const ngPackage = {
|
|
60
|
+
lib: { entryFile: 'public-api.ts' },
|
|
61
|
+
};
|
|
62
|
+
return [
|
|
63
|
+
...files,
|
|
64
|
+
{
|
|
65
|
+
path: 'ng-package.json',
|
|
66
|
+
content: (0, template_1.finalize)(JSON.stringify(ngPackage, null, 2)),
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema",
|
|
3
|
+
"$id": "SdkInitSchema",
|
|
4
|
+
"title": "Initialize SDK generator config",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"path": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "Path (relative to workspace root) where the config file will be created.",
|
|
10
|
+
"default": "config/sdk.config.json"
|
|
11
|
+
},
|
|
12
|
+
"force": {
|
|
13
|
+
"type": "boolean",
|
|
14
|
+
"description": "Overwrite the file if it already exists.",
|
|
15
|
+
"default": false
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"required": []
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema",
|
|
3
|
+
"$id": "SdkGeneratorSchema",
|
|
4
|
+
"title": "Generate an Angular SDK from an OpenAPI spec",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"config": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "Path to sdk.config.json (defaults to ./config/sdk.config.json at workspace root).",
|
|
10
|
+
"default": "config/sdk.config.json"
|
|
11
|
+
},
|
|
12
|
+
"target": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "Optional name/index of a specific target in the config to generate."
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"required": [],
|
|
18
|
+
"additionalProperties": false
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/@ojiepermana/angular/generator/api/schematics/sdk/schema.json",
|
|
3
|
+
"targets": [
|
|
4
|
+
{
|
|
5
|
+
"input": "./openapi.yaml",
|
|
6
|
+
"output": "./sdk",
|
|
7
|
+
"mode": "standalone",
|
|
8
|
+
"clientName": "Api",
|
|
9
|
+
"rootUrl": "http://127.0.0.1:8080",
|
|
10
|
+
"splitByDomain": false,
|
|
11
|
+
"splitDepth": "service",
|
|
12
|
+
"features": {
|
|
13
|
+
"models": true,
|
|
14
|
+
"operations": true,
|
|
15
|
+
"services": true,
|
|
16
|
+
"client": true,
|
|
17
|
+
"metadata": true,
|
|
18
|
+
"navigation": true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# `@ojiepermana/angular/generator/guide`
|
|
2
|
+
|
|
3
|
+
Build-time schematic that compiles a folder of Markdown files into Angular standalone components plus a nested `Routes` file.
|
|
4
|
+
|
|
5
|
+
This entry point ships **no runtime code**. It is only invoked through the Angular CLI:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 1. scaffold a config file at the workspace root
|
|
9
|
+
bun run gen:guide:init
|
|
10
|
+
|
|
11
|
+
# 2. edit guide.config.json (sourceDir, outputDir, ...)
|
|
12
|
+
|
|
13
|
+
# 3. generate components + routes
|
|
14
|
+
bun run gen:guide
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Inside this monorepo the schematic factory is registered in the parent collection at [`projects/angular/collection.json`](../../collection.json).
|
|
18
|
+
After publishing, consumers run `ng generate @ojiepermana/angular:guide` (and `:guide-init` to scaffold the config).
|
|
19
|
+
|
|
20
|
+
## Config (`guide.config.json`)
|
|
21
|
+
|
|
22
|
+
| Key | Type | Default | Description |
|
|
23
|
+
| ----------------- | --------------------------- | --------------- | ------------------------------------------------------------------- |
|
|
24
|
+
| `sourceDir` | `string` (required) | — | Folder containing `.md` files. Nested folders become nested routes. |
|
|
25
|
+
| `outputDir` | `string` (required) | — | Destination folder for generated components and the routes file. |
|
|
26
|
+
| `routeFile` | `string` | `doc.routes.ts` | File name of the generated routes file at `outputDir`. |
|
|
27
|
+
| `componentPrefix` | `string` | `Doc` | Prefix on generated component class names. |
|
|
28
|
+
| `componentStyle` | `'none' \| 'css' \| 'scss'` | `'none'` | Whether to emit a sibling stylesheet per component. |
|
|
29
|
+
| `routeExportName` | `string` | `DOC_ROUTES` | Identifier used for the exported `Routes` constant. |
|
|
30
|
+
|
|
31
|
+
## Frontmatter
|
|
32
|
+
|
|
33
|
+
Each `.md` file may declare YAML-style frontmatter:
|
|
34
|
+
|
|
35
|
+
```md
|
|
36
|
+
---
|
|
37
|
+
title: Getting Started
|
|
38
|
+
order: 1
|
|
39
|
+
path: getting-started
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
# Hello
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- `title` → `data.title` on the route
|
|
46
|
+
- `order` → controls sibling sort order (lower comes first)
|
|
47
|
+
- `path` → overrides the URL segment derived from the file name (`index.md` → empty path)
|
|
48
|
+
|
|
49
|
+
## Output shape
|
|
50
|
+
|
|
51
|
+
For `sourceDir: ./docs` containing `docs/intro/overview.md`:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
<outputDir>/
|
|
55
|
+
intro/
|
|
56
|
+
overview.component.ts // standalone @Component, OnPush, inline template
|
|
57
|
+
doc.routes.ts // exports DOC_ROUTES: Routes
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Routes mirror the folder tree with lazy `loadComponent`:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
export const DOC_ROUTES: Routes = [
|
|
64
|
+
{
|
|
65
|
+
path: 'intro',
|
|
66
|
+
children: [
|
|
67
|
+
{
|
|
68
|
+
path: 'overview',
|
|
69
|
+
loadComponent: () => import('./intro/overview.component').then((m) => m.DocIntroOverviewComponent),
|
|
70
|
+
data: { title: 'Overview', order: 1 },
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Then plug into your app:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
{
|
|
81
|
+
path: 'docs',
|
|
82
|
+
loadChildren: () => import('./docs/doc.routes').then((m) => m.DOC_ROUTES),
|
|
83
|
+
},
|
|
84
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.build = build;
|
|
4
|
+
const node_path_1 = require("node:path");
|
|
5
|
+
const loader_1 = require("../../src/config/loader");
|
|
6
|
+
const engine_1 = require("../../src/engine");
|
|
7
|
+
function build(options = {}) {
|
|
8
|
+
return (tree, context) => {
|
|
9
|
+
const workspaceRoot = process.cwd();
|
|
10
|
+
const configPath = options.config ?? 'config/guide.config.json';
|
|
11
|
+
const config = (0, loader_1.loadConfig)(configPath, workspaceRoot);
|
|
12
|
+
const result = (0, engine_1.generate)(config, workspaceRoot);
|
|
13
|
+
writeResult(tree, workspaceRoot, result, context);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function writeResult(tree, workspaceRoot, result, context) {
|
|
17
|
+
const relOutput = (0, node_path_1.relative)(workspaceRoot, result.outputDir) || '.';
|
|
18
|
+
context.logger.info(`[guide] ${relOutput} (markdown=${result.stats.markdown}, components=${result.stats.components}, files=${result.stats.files})`);
|
|
19
|
+
for (const file of result.files) {
|
|
20
|
+
const treePath = normalizeTreePath(workspaceRoot, file.path);
|
|
21
|
+
const buffer = Buffer.from(file.content, 'utf8');
|
|
22
|
+
if (tree.exists(treePath)) {
|
|
23
|
+
tree.overwrite(treePath, buffer);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
tree.create(treePath, buffer);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function normalizeTreePath(workspaceRoot, path) {
|
|
31
|
+
const abs = (0, node_path_1.resolve)(workspaceRoot, path);
|
|
32
|
+
const rel = (0, node_path_1.relative)(workspaceRoot, abs);
|
|
33
|
+
const posix = rel.split(/\\+/).join('/');
|
|
34
|
+
return posix.startsWith('/') ? posix : `/${posix}`;
|
|
35
|
+
}
|