@obsidiane/meridiane 2.0.7 → 2.0.11
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 +19 -2
- package/cli.js +21 -37
- package/package.json +3 -3
- package/src/README.md +9 -0
- package/{tools/app → src/application}/bridge-workflow.js +15 -13
- package/src/application/generate-bridge.js +50 -0
- package/{tools/app → src/application}/run-build.js +1 -1
- package/{tools/app → src/application}/run-dev.js +30 -36
- package/{tools/app → src/application}/run-generate.js +3 -3
- package/{tools → src}/domain/bridge-options.js +8 -21
- package/src/domain/list-values.js +6 -0
- package/src/generators/bridge/write-bridge-files.js +100 -0
- package/src/generators/models/interface-schema.js +116 -0
- package/src/generators/models/model-selection.js +148 -0
- package/src/generators/models/oas-contract.js +170 -0
- package/{tools/generator → src/generators}/models/oas-schema-name.js +6 -15
- package/src/generators/models/openapi-to-models.js +136 -0
- package/{tools/generator → src/generators}/models/schema-utils.js +27 -3
- package/src/generators/models/templates/type-alias.hbs +2 -0
- package/src/generators/models/type-expression.js +65 -0
- package/src/generators/models/type-resolver.js +240 -0
- package/src/generators/models/utils.js +89 -0
- package/src/generators/models/write-model-files.js +55 -0
- package/src/infrastructure/dev-project.js +48 -0
- package/{tools/infra → src/infrastructure}/exec.js +3 -2
- package/src/infrastructure/local-package-install.js +104 -0
- package/src/infrastructure/spec-loader.js +65 -0
- package/{tools/infra → src/infrastructure}/standalone-workspace.js +31 -29
- package/templates/{_lib_template → angular-bridge}/README.md +22 -4
- package/templates/angular-bridge/src/lib/bridge/rest/api-platform.adapter.ts +82 -0
- package/templates/angular-bridge/src/lib/bridge/rest/http-request.options.ts +30 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/bridge/rest/query-builder.ts +10 -10
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-connections.ts +255 -0
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-payload.ts +82 -0
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.builder.ts +39 -0
- package/templates/angular-bridge/src/lib/bridge/sse/mercure.adapter.ts +358 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/ref-count-topic.registry.ts +3 -12
- package/templates/angular-bridge/src/lib/bridge/sse/subscription-claims.ts +76 -0
- package/templates/angular-bridge/src/lib/bridge-options.ts +59 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/facades/bridge.facade.ts +52 -53
- package/templates/{_lib_template → angular-bridge}/src/lib/facades/facade.factory.ts +3 -4
- package/templates/angular-bridge/src/lib/facades/facade.interface.ts +13 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/facades/resource.facade.ts +24 -31
- package/templates/angular-bridge/src/lib/interceptors/bearer-auth.interceptor.ts +33 -0
- package/templates/angular-bridge/src/lib/interceptors/bridge-defaults.interceptor.ts +65 -0
- package/templates/angular-bridge/src/lib/interceptors/content-type.interceptor.ts +37 -0
- package/templates/angular-bridge/src/lib/interceptors/scoped.interceptor.ts +9 -0
- package/templates/angular-bridge/src/lib/interceptors/singleflight.interceptor.ts +46 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/ports/realtime.port.ts +6 -2
- package/templates/{_lib_template → angular-bridge}/src/lib/ports/resource-repository.port.ts +7 -7
- package/templates/angular-bridge/src/lib/provide-bridge.ts +98 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/tokens.ts +9 -3
- package/templates/angular-bridge/src/lib/utils/iri.ts +6 -0
- package/templates/angular-bridge/src/lib/utils/request-url.ts +31 -0
- package/templates/{_lib_template → angular-bridge}/src/public-api.ts +2 -0
- package/templates/_lib_template/src/lib/bridge/rest/api-platform.adapter.ts +0 -84
- package/templates/_lib_template/src/lib/bridge/rest/http-request.options.ts +0 -21
- package/templates/_lib_template/src/lib/bridge/sse/mercure-url.builder.ts +0 -17
- package/templates/_lib_template/src/lib/bridge/sse/mercure.adapter.ts +0 -806
- package/templates/_lib_template/src/lib/facades/facade.interface.ts +0 -31
- package/templates/_lib_template/src/lib/interceptors/bridge-defaults.interceptor.ts +0 -53
- package/templates/_lib_template/src/lib/interceptors/content-type.interceptor.ts +0 -49
- package/templates/_lib_template/src/lib/interceptors/singleflight.interceptor.ts +0 -55
- package/templates/_lib_template/src/lib/provide-bridge.ts +0 -170
- package/tools/build.js +0 -1
- package/tools/dev.js +0 -1
- package/tools/domain/paths.js +0 -10
- package/tools/generate.js +0 -1
- package/tools/generator/models/oas-contract.js +0 -159
- package/tools/generator/models/openapi-to-models.js +0 -240
- package/tools/generator/models/type-resolver.js +0 -107
- package/tools/generator/models/utils.js +0 -62
- package/tools/infra/dev-support.js +0 -189
- package/tools/infra/generate/bridge.js +0 -212
- package/tools/infra/spec-loader.js +0 -59
- /package/{tools → src}/domain/lib-name.js +0 -0
- /package/{tools/generator → src/generators}/models/handlebars.js +0 -0
- /package/{tools/generator → src/generators}/models/templates/index.hbs +0 -0
- /package/{tools/generator → src/generators}/models/templates/model.hbs +0 -0
- /package/{tools/infra → src/infrastructure}/json.js +0 -0
- /package/{tools/infra → src/infrastructure}/logger.js +0 -0
- /package/templates/{_lib_template → angular-bridge}/ng-package.json +0 -0
- /package/templates/{_lib_template → angular-bridge}/package.json +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/eventsource-wrapper.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/mercure-topic.mapper.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/bridge.types.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/interceptors/bridge-debug.interceptor.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/utils/url.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/models/index.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/tsconfig.lib.json +0 -0
- /package/templates/{_lib_template → angular-bridge}/tsconfig.lib.prod.json +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import {hasOwn, resolveLocalRef, schemaNameFromRef} from './schema-utils.js';
|
|
2
|
+
import {
|
|
3
|
+
arrayType,
|
|
4
|
+
atomicType,
|
|
5
|
+
formatType,
|
|
6
|
+
intersectionType,
|
|
7
|
+
nullableType,
|
|
8
|
+
unionType,
|
|
9
|
+
} from './type-expression.js';
|
|
10
|
+
import {quoteKeyIfNeeded, quoteStringLiteral} from './utils.js';
|
|
11
|
+
|
|
12
|
+
const PRIMITIVE_TYPES = {
|
|
13
|
+
string: 'string',
|
|
14
|
+
integer: 'number',
|
|
15
|
+
number: 'number',
|
|
16
|
+
boolean: 'boolean',
|
|
17
|
+
null: 'null',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function literalExpression(value) {
|
|
21
|
+
if (value === null) return atomicType('null', true);
|
|
22
|
+
if (typeof value === 'string') return atomicType(quoteStringLiteral(value));
|
|
23
|
+
if (typeof value === 'number' || typeof value === 'bigint') return atomicType(String(value));
|
|
24
|
+
if (typeof value === 'boolean') return atomicType(String(value));
|
|
25
|
+
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
const items = value.map((item) => literalExpression(item).code);
|
|
28
|
+
return atomicType(`[${items.join(', ')}]`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (value && typeof value === 'object') {
|
|
32
|
+
const properties = Object.entries(value).map(([key, item]) =>
|
|
33
|
+
`${quoteKeyIfNeeded(key)}: ${literalExpression(item).code};`
|
|
34
|
+
);
|
|
35
|
+
return atomicType(`{ ${properties.join(' ')} }`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return atomicType('any');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function tupleMember(type, optional = false) {
|
|
42
|
+
return optional ? `${formatType(type)}?` : type.code;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function nonNegativeInteger(value) {
|
|
46
|
+
return Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Creates one resolver for a model. Its lexical context owns name resolution,
|
|
51
|
+
* dependency collection and local-reference cycle detection.
|
|
52
|
+
*/
|
|
53
|
+
export function createTypeResolver({
|
|
54
|
+
dependencies = new Set(),
|
|
55
|
+
typeNames = new Map(),
|
|
56
|
+
requiredMode = 'all',
|
|
57
|
+
spec,
|
|
58
|
+
} = {}) {
|
|
59
|
+
const resolvingRefs = new Set();
|
|
60
|
+
|
|
61
|
+
function resolveReference(ref) {
|
|
62
|
+
const componentName = schemaNameFromRef(ref);
|
|
63
|
+
if (componentName) {
|
|
64
|
+
if (/jsonMergePatch/i.test(componentName) || /^Hydra/i.test(componentName)) {
|
|
65
|
+
return atomicType('any');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const typeName = typeNames.get(componentName) ?? componentName;
|
|
69
|
+
dependencies.add(typeName);
|
|
70
|
+
return atomicType(typeName);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const target = resolveLocalRef(spec, ref);
|
|
74
|
+
if (target === undefined || resolvingRefs.has(ref)) return atomicType('any');
|
|
75
|
+
|
|
76
|
+
resolvingRefs.add(ref);
|
|
77
|
+
try {
|
|
78
|
+
return resolve(target);
|
|
79
|
+
} finally {
|
|
80
|
+
resolvingRefs.delete(ref);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveProperty(schema) {
|
|
85
|
+
const type = resolve(schema);
|
|
86
|
+
return requiredMode === 'spec' ? type : nullableType(type);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveDictionaryValue(additionalProperties, properties) {
|
|
90
|
+
const additionalType = additionalProperties === true
|
|
91
|
+
? atomicType('any')
|
|
92
|
+
: resolve(additionalProperties);
|
|
93
|
+
if (additionalType.code === 'any') return additionalType;
|
|
94
|
+
|
|
95
|
+
const memberTypes = properties.map(({type}) => type);
|
|
96
|
+
if (properties.some(({optional}) => optional)) memberTypes.push(atomicType('undefined'));
|
|
97
|
+
return unionType([additionalType, ...memberTypes]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveObject(schema) {
|
|
101
|
+
const requiredProperties = new Set(schema.required ?? []);
|
|
102
|
+
const properties = Object.entries(schema.properties ?? {}).map(([key, propertySchema]) => ({
|
|
103
|
+
key,
|
|
104
|
+
optional: requiredMode !== 'spec' || !requiredProperties.has(key),
|
|
105
|
+
type: resolveProperty(propertySchema),
|
|
106
|
+
}));
|
|
107
|
+
const members = properties.map(({key, optional, type}) =>
|
|
108
|
+
`${quoteKeyIfNeeded(key)}${optional ? '?' : ''}: ${type.code};`
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (hasOwn(schema, 'additionalProperties') && schema.additionalProperties !== false) {
|
|
112
|
+
const valueType = resolveDictionaryValue(schema.additionalProperties, properties);
|
|
113
|
+
members.push(`[key: string]: ${valueType.code};`);
|
|
114
|
+
} else if (properties.length === 0) {
|
|
115
|
+
const valueType = schema.additionalProperties === false ? 'never' : 'any';
|
|
116
|
+
members.push(`[key: string]: ${valueType};`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return atomicType(`{ ${members.join(' ')} }`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveTuple(schema, prefixSchemas, additionalSchema) {
|
|
123
|
+
const minimum = nonNegativeInteger(schema.minItems) ?? 0;
|
|
124
|
+
const maximum = nonNegativeInteger(schema.maxItems);
|
|
125
|
+
if (maximum !== undefined && maximum < minimum) return atomicType('never');
|
|
126
|
+
|
|
127
|
+
const prefixLimit = maximum === undefined
|
|
128
|
+
? prefixSchemas.length
|
|
129
|
+
: Math.min(prefixSchemas.length, maximum);
|
|
130
|
+
const prefix = prefixSchemas.slice(0, prefixLimit).map(resolve);
|
|
131
|
+
const requiredPrefixCount = Math.min(minimum, prefix.length);
|
|
132
|
+
const members = prefix.map((type, index) => tupleMember(type, index >= requiredPrefixCount));
|
|
133
|
+
|
|
134
|
+
const hasAdditionalItems = additionalSchema !== false;
|
|
135
|
+
if (!hasAdditionalItems && minimum > prefix.length) return atomicType('never');
|
|
136
|
+
if (!hasAdditionalItems || maximum === prefix.length) {
|
|
137
|
+
return atomicType(`[${members.join(', ')}]`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const additionalType = additionalSchema === undefined || additionalSchema === true
|
|
141
|
+
? atomicType('any')
|
|
142
|
+
: resolve(additionalSchema);
|
|
143
|
+
const requiredAdditionalCount = Math.max(0, minimum - prefix.length);
|
|
144
|
+
for (let index = 0; index < requiredAdditionalCount; index += 1) {
|
|
145
|
+
members.push(tupleMember(additionalType));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (maximum === undefined) {
|
|
149
|
+
members.push(`...${formatType(additionalType)}[]`);
|
|
150
|
+
} else {
|
|
151
|
+
const optionalCount = Math.max(0, maximum - prefix.length - requiredAdditionalCount);
|
|
152
|
+
for (let index = 0; index < optionalCount; index += 1) {
|
|
153
|
+
members.push(tupleMember(additionalType, true));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return atomicType(`[${members.join(', ')}]`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function resolveArray(schema) {
|
|
161
|
+
if (Array.isArray(schema.prefixItems)) {
|
|
162
|
+
return resolveTuple(schema, schema.prefixItems, schema.items);
|
|
163
|
+
}
|
|
164
|
+
if (Array.isArray(schema.items)) {
|
|
165
|
+
return resolveTuple(schema, schema.items, schema.additionalItems);
|
|
166
|
+
}
|
|
167
|
+
if (schema.items === false) return atomicType('[]');
|
|
168
|
+
|
|
169
|
+
const itemType = schema.items === true || schema.items === undefined
|
|
170
|
+
? atomicType('any')
|
|
171
|
+
: resolve(schema.items);
|
|
172
|
+
return arrayType(itemType);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function resolveDeclaredType(type, schema) {
|
|
176
|
+
if (type === 'array') return resolveArray(schema);
|
|
177
|
+
if (type === 'object') return resolveObject(schema);
|
|
178
|
+
|
|
179
|
+
const primitive = PRIMITIVE_TYPES[type];
|
|
180
|
+
return primitive ? atomicType(primitive, type === 'null') : atomicType('any');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function resolveStructure(schema) {
|
|
184
|
+
if (Array.isArray(schema.type)) {
|
|
185
|
+
return unionType(schema.type.map((type) => resolveDeclaredType(type, schema)));
|
|
186
|
+
}
|
|
187
|
+
if (typeof schema.type === 'string') return resolveDeclaredType(schema.type, schema);
|
|
188
|
+
if (schema.properties || hasOwn(schema, 'additionalProperties') || schema.patternProperties) {
|
|
189
|
+
return resolveObject(schema);
|
|
190
|
+
}
|
|
191
|
+
if (schema.prefixItems || hasOwn(schema, 'items')) return resolveArray(schema);
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function resolve(schema) {
|
|
196
|
+
if (schema === true || schema === undefined || schema === null) return atomicType('any');
|
|
197
|
+
if (schema === false) return atomicType('never');
|
|
198
|
+
|
|
199
|
+
const constraints = [];
|
|
200
|
+
if (schema.$ref) constraints.push(resolveReference(schema.$ref));
|
|
201
|
+
|
|
202
|
+
if (hasOwn(schema, 'const')) {
|
|
203
|
+
constraints.push(literalExpression(schema.const));
|
|
204
|
+
} else if (Array.isArray(schema.enum)) {
|
|
205
|
+
constraints.push(unionType(schema.enum.map(literalExpression)));
|
|
206
|
+
} else {
|
|
207
|
+
const structure = resolveStructure(schema);
|
|
208
|
+
if (structure) constraints.push(structure);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (Array.isArray(schema.allOf)) {
|
|
212
|
+
constraints.push(intersectionType(schema.allOf.map(resolve)));
|
|
213
|
+
}
|
|
214
|
+
if (Array.isArray(schema.oneOf)) {
|
|
215
|
+
constraints.push(unionType(schema.oneOf.map(resolve)));
|
|
216
|
+
}
|
|
217
|
+
if (Array.isArray(schema.anyOf)) {
|
|
218
|
+
constraints.push(unionType(schema.anyOf.map(resolve)));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const type = intersectionType(constraints);
|
|
222
|
+
return schema.nullable ? nullableType(type) : type;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
typeOf: (schema) => resolve(schema).code,
|
|
227
|
+
nullableTypeOf: (schema) => nullableType(resolve(schema)).code,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Resolves an OpenAPI schema to TypeScript and collects component dependencies.
|
|
233
|
+
*/
|
|
234
|
+
export function tsTypeOf(schema, dependencies, typeNames, options) {
|
|
235
|
+
return createTypeResolver({dependencies, typeNames, ...options}).typeOf(schema);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function nullableTsTypeOf(schema, dependencies, typeNames, options) {
|
|
239
|
+
return createTypeResolver({dependencies, typeNames, ...options}).nullableTypeOf(schema);
|
|
240
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Supprime et recrée un dossier (idempotent)
|
|
5
|
+
* @param {string} dir
|
|
6
|
+
*/
|
|
7
|
+
export async function ensureCleanDir(dir) {
|
|
8
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
9
|
+
await fs.mkdir(dir, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const RESERVED = new Set([
|
|
13
|
+
'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do',
|
|
14
|
+
'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import',
|
|
15
|
+
'in', 'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true',
|
|
16
|
+
'try', 'typeof', 'var', 'void', 'while', 'with', 'yield', 'let', 'implements', 'interface',
|
|
17
|
+
'package', 'private', 'protected', 'public', 'static', 'await', 'abstract', 'boolean', 'byte',
|
|
18
|
+
'char', 'double', 'final', 'float', 'goto', 'int', 'long', 'native', 'short', 'synchronized',
|
|
19
|
+
'throws', 'transient', 'volatile',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const STRING_ESCAPES = {
|
|
23
|
+
"'": "\\'",
|
|
24
|
+
'\\': '\\\\',
|
|
25
|
+
'\b': '\\b',
|
|
26
|
+
'\f': '\\f',
|
|
27
|
+
'\n': '\\n',
|
|
28
|
+
'\r': '\\r',
|
|
29
|
+
'\t': '\\t',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function quoteStringLiteral(value) {
|
|
33
|
+
const escaped = String(value).replace(/['\\\u0000-\u001f\u2028\u2029]/g, (character) => {
|
|
34
|
+
const knownEscape = STRING_ESCAPES[character];
|
|
35
|
+
if (knownEscape) return knownEscape;
|
|
36
|
+
return `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`;
|
|
37
|
+
});
|
|
38
|
+
return `'${escaped}'`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Construit un PascalCase à partir d’une liste de tokens
|
|
43
|
+
* @param {string[]} tokens
|
|
44
|
+
*/
|
|
45
|
+
export function pascalCase(tokens) {
|
|
46
|
+
return tokens
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.map((token) => token.replace(/^[^A-Za-z0-9]+|[^A-Za-z0-9]+$/g, ''))
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.map((token) => token.charAt(0).toUpperCase() + token.slice(1))
|
|
51
|
+
.join('');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Nettoie et désambiguïse un nom de type TypeScript, en évitant les mots réservés
|
|
56
|
+
* et collisions via un suffixe numérique.
|
|
57
|
+
* @param {string} original
|
|
58
|
+
* @param {Set<string>} used
|
|
59
|
+
*/
|
|
60
|
+
export function sanitizeTypeName(original, used) {
|
|
61
|
+
const tokens = String(original).split(/[^A-Za-z0-9]+/);
|
|
62
|
+
let name = pascalCase(tokens) || 'Model';
|
|
63
|
+
if (/^[0-9]/.test(name)) name = `_${name}`;
|
|
64
|
+
if (RESERVED.has(name)) name = `${name}Model`;
|
|
65
|
+
|
|
66
|
+
let unique = name;
|
|
67
|
+
let suffix = 2;
|
|
68
|
+
while (used.has(unique)) {
|
|
69
|
+
unique = `${name}${suffix++}`;
|
|
70
|
+
}
|
|
71
|
+
used.add(unique);
|
|
72
|
+
return unique;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Vérifie qu’une clé est un identifiant TS valide ET non réservé
|
|
77
|
+
* @param {string} k
|
|
78
|
+
*/
|
|
79
|
+
export function isValidTsIdentifier(k) {
|
|
80
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(k) && !RESERVED.has(k);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Ajoute des quotes sur une clé si nécessaire (clé non valide en TS)
|
|
85
|
+
* @param {string} k
|
|
86
|
+
*/
|
|
87
|
+
export function quoteKeyIfNeeded(k) {
|
|
88
|
+
return isValidTsIdentifier(k) ? k : quoteStringLiteral(k);
|
|
89
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
import { renderTemplateToFile } from './handlebars.js';
|
|
5
|
+
import { buildModelsFromOpenAPI } from './openapi-to-models.js';
|
|
6
|
+
import { ensureCleanDir } from './utils.js';
|
|
7
|
+
|
|
8
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const templatesDir = path.join(currentDir, 'templates');
|
|
10
|
+
|
|
11
|
+
export async function writeModelFiles({ projectDir, spec, requiredMode, formats, include, exclude, debug, log }) {
|
|
12
|
+
const outputDir = path.resolve(projectDir, 'src', 'models');
|
|
13
|
+
if (debug) {
|
|
14
|
+
log?.debug?.('[meridiane] generate models', {
|
|
15
|
+
outputDir,
|
|
16
|
+
requiredMode,
|
|
17
|
+
formats: Array.isArray(formats) ? formats : [],
|
|
18
|
+
includeCount: include.length,
|
|
19
|
+
excludeCount: exclude.length,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await ensureCleanDir(outputDir);
|
|
24
|
+
|
|
25
|
+
const { models } = buildModelsFromOpenAPI(spec, {
|
|
26
|
+
requiredMode,
|
|
27
|
+
formats: Array.isArray(formats) ? formats : [],
|
|
28
|
+
includeSchemaNames: include,
|
|
29
|
+
excludeSchemaNames: exclude,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
for (const model of models) {
|
|
33
|
+
await renderTemplateToFile({
|
|
34
|
+
templatePath: path.join(templatesDir, model.isAlias ? 'type-alias.hbs' : 'model.hbs'),
|
|
35
|
+
outPath: path.join(outputDir, `${model.name}.ts`),
|
|
36
|
+
ctx: {
|
|
37
|
+
name: model.name,
|
|
38
|
+
props: model.props,
|
|
39
|
+
imports: model.imports,
|
|
40
|
+
extendsTypes: model.extendsTypes || [],
|
|
41
|
+
extendsItem: model.extendsItem === true,
|
|
42
|
+
type: model.type,
|
|
43
|
+
separator: model.imports.length > 0 ? '\n' : '',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await renderTemplateToFile({
|
|
49
|
+
templatePath: path.join(templatesDir, 'index.hbs'),
|
|
50
|
+
outPath: path.join(outputDir, 'index.ts'),
|
|
51
|
+
ctx: { models: models.map(({ name }) => ({ name })) },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (debug) log?.debug?.(`[meridiane] ${models.length} model(s) generated`);
|
|
55
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const SANDBOX_PACKAGE_NAME = '@obsidiane/meridiane-sandbox';
|
|
5
|
+
|
|
6
|
+
async function isFile(filePath) {
|
|
7
|
+
try {
|
|
8
|
+
return (await fs.stat(filePath)).isFile();
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function readPackageName(projectRoot) {
|
|
15
|
+
try {
|
|
16
|
+
const packageJson = JSON.parse(await fs.readFile(path.join(projectRoot, 'package.json'), 'utf8'));
|
|
17
|
+
return typeof packageJson.name === 'string' ? packageJson.name : undefined;
|
|
18
|
+
} catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function isSandboxProject(projectRoot) {
|
|
24
|
+
return (await readPackageName(projectRoot)) === SANDBOX_PACKAGE_NAME;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function resolveDevAppRoot(startDir) {
|
|
28
|
+
if (await isFile(path.join(startDir, 'package.json'))) return startDir;
|
|
29
|
+
throw new Error('No package.json found. Run `meridiane dev` from your Angular app root.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function resolveSandboxAppRoot(startDir) {
|
|
33
|
+
if (await isSandboxProject(startDir)) return startDir;
|
|
34
|
+
|
|
35
|
+
const playgroundRoot = path.join(startDir, 'playground', 'angular-app');
|
|
36
|
+
if (await isSandboxProject(playgroundRoot)) return playgroundRoot;
|
|
37
|
+
|
|
38
|
+
throw new Error(
|
|
39
|
+
'Missing <packageName>. Run from the playground app (`playground/angular-app`) or provide a package name.'
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function resolveSpecSource({useSandboxDefaults, appRoot, specArg}) {
|
|
44
|
+
if (!useSandboxDefaults || specArg) return specArg;
|
|
45
|
+
|
|
46
|
+
const localSpec = path.resolve(appRoot, '..', 'api-platform', 'var', 'openapi.json');
|
|
47
|
+
return (await isFile(localSpec)) ? localSpec : 'http://localhost:8000/api/docs.json';
|
|
48
|
+
}
|
|
@@ -10,12 +10,13 @@ export class ExitCodeError extends Error {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function runCommand(cmd, args, { cwd, env } = {}) {
|
|
13
|
-
return new Promise((resolve) => {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
14
|
const child = spawn(cmd, args, {
|
|
15
15
|
stdio: 'inherit',
|
|
16
16
|
cwd: cwd ?? process.cwd(),
|
|
17
17
|
env: env ?? process.env,
|
|
18
18
|
});
|
|
19
|
-
child.
|
|
19
|
+
child.once('error', reject);
|
|
20
|
+
child.once('exit', (code) => resolve(code ?? 1));
|
|
20
21
|
});
|
|
21
22
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
function splitPackageName(packageName) {
|
|
5
|
+
const normalized = String(packageName || '').trim();
|
|
6
|
+
if (!normalized) throw new Error('Missing packageName');
|
|
7
|
+
if (!normalized.startsWith('@')) return {name: normalized};
|
|
8
|
+
|
|
9
|
+
const [scope, name] = normalized.split('/');
|
|
10
|
+
if (!scope || !name) throw new Error(`Invalid packageName: ${packageName}`);
|
|
11
|
+
return {scope, name};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function findNodeModules(startDir) {
|
|
15
|
+
let currentDir = startDir;
|
|
16
|
+
|
|
17
|
+
while (true) {
|
|
18
|
+
const candidate = path.join(currentDir, 'node_modules');
|
|
19
|
+
try {
|
|
20
|
+
if ((await fs.stat(candidate)).isDirectory()) return candidate;
|
|
21
|
+
} catch {
|
|
22
|
+
// Continue towards the filesystem root.
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const parentDir = path.dirname(currentDir);
|
|
26
|
+
if (parentDir === currentDir) break;
|
|
27
|
+
currentDir = parentDir;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const nodeModules = path.join(startDir, 'node_modules');
|
|
31
|
+
await fs.mkdir(nodeModules, {recursive: true});
|
|
32
|
+
return nodeModules;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function installPackageFromDist({packageName, distDir, appRoot, log}) {
|
|
36
|
+
const nodeModules = await findNodeModules(appRoot);
|
|
37
|
+
const {scope, name} = splitPackageName(packageName);
|
|
38
|
+
const packageDir = scope ? path.join(nodeModules, scope, name) : path.join(nodeModules, name);
|
|
39
|
+
const displayName = scope ? `${scope}/${name}` : name;
|
|
40
|
+
|
|
41
|
+
log?.step?.(`installation locale dans node_modules (${displayName})`);
|
|
42
|
+
|
|
43
|
+
await fs.rm(packageDir, {recursive: true, force: true});
|
|
44
|
+
await fs.mkdir(path.dirname(packageDir), {recursive: true});
|
|
45
|
+
await fs.cp(distDir, packageDir, {
|
|
46
|
+
recursive: true,
|
|
47
|
+
filter: (source) => !source.endsWith('.tgz'),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await invalidateAngularViteCache({appRoot, packageName, log});
|
|
51
|
+
return packageDir;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function invalidateAngularViteCache({appRoot, packageName, log}) {
|
|
55
|
+
const dependencyCaches = await findViteDependencyCaches(appRoot);
|
|
56
|
+
if (dependencyCaches.length === 0) return;
|
|
57
|
+
|
|
58
|
+
const packageCacheName = packageName.replace('/', '_');
|
|
59
|
+
const generatedFiles = ['.js', '.js.map', '.mjs', '.mjs.map'].map((suffix) => `${packageCacheName}${suffix}`);
|
|
60
|
+
let removedFiles = 0;
|
|
61
|
+
|
|
62
|
+
for (const cacheDir of dependencyCaches) {
|
|
63
|
+
for (const fileName of [...generatedFiles, '_metadata.json']) {
|
|
64
|
+
try {
|
|
65
|
+
await fs.rm(path.join(cacheDir, fileName), {force: true});
|
|
66
|
+
removedFiles += 1;
|
|
67
|
+
} catch {
|
|
68
|
+
// A stale cache file may disappear between discovery and deletion.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (removedFiles > 0) {
|
|
74
|
+
log?.info?.(`cache Angular/Vite invalidé (${removedFiles} fichier(s)) pour ${packageName}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function findViteDependencyCaches(appRoot) {
|
|
79
|
+
const cacheRoot = path.join(appRoot, '.angular', 'cache');
|
|
80
|
+
const dependencyCaches = [];
|
|
81
|
+
|
|
82
|
+
async function visit(dir, depth) {
|
|
83
|
+
if (depth > 8) return;
|
|
84
|
+
|
|
85
|
+
let entries;
|
|
86
|
+
try {
|
|
87
|
+
entries = await fs.readdir(dir, {withFileTypes: true});
|
|
88
|
+
} catch {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (path.basename(dir) === 'vite' && entries.some((entry) => entry.isDirectory() && entry.name === 'deps')) {
|
|
93
|
+
dependencyCaches.push(path.join(dir, 'deps'));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
if (!entry.isDirectory() || entry.name === 'node_modules') continue;
|
|
98
|
+
await visit(path.join(dir, entry.name), depth + 1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await visit(cacheRoot, 0);
|
|
103
|
+
return dependencyCaches;
|
|
104
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
function isHttpUrl(value) {
|
|
4
|
+
return /^https?:\/\//i.test(String(value || ''));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
async function fetchJson(url) {
|
|
8
|
+
let response;
|
|
9
|
+
try {
|
|
10
|
+
response = await fetch(url);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
const message = error?.message ?? String(error);
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Spec fetch failed: ${url}\n` +
|
|
15
|
+
`Reason: ${message}\n` +
|
|
16
|
+
`Tip: if your environment cannot access the network, use a local JSON file via --spec ./openapi.json`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
const body = await response.text().catch(() => '');
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Spec fetch failed: ${response.status} ${response.statusText}${body ? `\n${body}` : ''}`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return response.json();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function readOpenApiSpec(specArg) {
|
|
29
|
+
if (!specArg) throw new Error('Missing --spec (url or JSON file path)');
|
|
30
|
+
const spec = String(specArg);
|
|
31
|
+
|
|
32
|
+
if (isHttpUrl(spec)) {
|
|
33
|
+
try {
|
|
34
|
+
return await fetchJson(spec);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
const isDocsJson = /\/api\/docs\.json$/i.test(spec);
|
|
37
|
+
if (!isDocsJson) throw error;
|
|
38
|
+
const fallback = spec.replace(/\/api\/docs\.json$/i, '/api/docs.jsonopenapi');
|
|
39
|
+
try {
|
|
40
|
+
return await fetchJson(fallback);
|
|
41
|
+
} catch (fallbackError) {
|
|
42
|
+
const primaryMessage = error?.message ?? String(error);
|
|
43
|
+
const fallbackMessage = fallbackError?.message ?? String(fallbackError);
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Spec fetch failed (both endpoints tried)\n` +
|
|
46
|
+
`- ${spec}\n ${primaryMessage}\n` +
|
|
47
|
+
`- ${fallback}\n ${fallbackMessage}`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const raw = await fs.readFile(spec, 'utf8');
|
|
55
|
+
return JSON.parse(raw);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error?.code === 'ENOENT') {
|
|
58
|
+
throw new Error(`Spec file not found: ${spec}`);
|
|
59
|
+
}
|
|
60
|
+
if (error?.name === 'SyntaxError') {
|
|
61
|
+
throw new Error(`Invalid JSON in spec file ${spec}: ${error.message}`);
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|