@obsidiane/meridiane 2.0.0 → 2.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/package.json +1 -1
- package/templates/_lib_template/README.md +221 -0
- package/templates/_lib_template/ng-package.json +3 -0
- package/templates/_lib_template/src/lib/interceptors/singleflight.interceptor.ts +55 -0
- package/templates/_lib_template/src/lib/provide-bridge.ts +10 -0
- package/tools/core/generate.js +27 -19
package/package.json
CHANGED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# __PACKAGE_NAME__
|
|
2
|
+
|
|
3
|
+
Bridge Angular (runtime + models TypeScript) pour une API Platform (Hydra/JSON-LD), avec support Mercure/SSE optionnel.
|
|
4
|
+
|
|
5
|
+
Le package expose une API Angular volontairement minimaliste :
|
|
6
|
+
- `provideBridge()` pour configurer le bridge (HTTP + tokens + Mercure) ;
|
|
7
|
+
- `FacadeFactory` / `ResourceFacade<T>` pour une API orientée ressource ;
|
|
8
|
+
- `BridgeFacade` pour des appels ad-hoc.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm i __PACKAGE_NAME__
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Le bridge est conçu pour être installé dans une application Angular. Les dépendances Angular et RxJS sont des `peerDependencies`.
|
|
17
|
+
|
|
18
|
+
## Compatibilité
|
|
19
|
+
|
|
20
|
+
- Angular `@angular/*` `^20.1.0`
|
|
21
|
+
- RxJS `^7.8.0`
|
|
22
|
+
|
|
23
|
+
## Démarrage rapide
|
|
24
|
+
|
|
25
|
+
Configurez le bridge au démarrage de l’application avec `provideBridge()` (requis).
|
|
26
|
+
|
|
27
|
+
### Application standalone (recommandé)
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// app.config.ts (ou main.ts)
|
|
31
|
+
import {ApplicationConfig} from '@angular/core';
|
|
32
|
+
import {provideBridge} from '__PACKAGE_NAME__';
|
|
33
|
+
|
|
34
|
+
export const appConfig: ApplicationConfig = {
|
|
35
|
+
providers: [
|
|
36
|
+
provideBridge({
|
|
37
|
+
baseUrl: 'https://api.example.com',
|
|
38
|
+
mercure: {hubUrl: 'https://api.example.com/.well-known/mercure'},
|
|
39
|
+
}),
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Application NgModule
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import {NgModule} from '@angular/core';
|
|
48
|
+
import {provideBridge} from '__PACKAGE_NAME__';
|
|
49
|
+
|
|
50
|
+
@NgModule({
|
|
51
|
+
providers: [
|
|
52
|
+
provideBridge({baseUrl: 'https://api.example.com'}),
|
|
53
|
+
],
|
|
54
|
+
})
|
|
55
|
+
export class AppModule {}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
|
|
60
|
+
`provideBridge({ ... })` accepte notamment :
|
|
61
|
+
- `baseUrl` (requis) ;
|
|
62
|
+
- `auth` (Bearer ou interceptor custom) ;
|
|
63
|
+
- `mercure` (hubUrl + options SSE) ;
|
|
64
|
+
- `defaults` (headers/timeout/retries) ;
|
|
65
|
+
- `singleFlight` (déduplication “in-flight” des requêtes HTTP identiques `GET/HEAD/OPTIONS`) ;
|
|
66
|
+
- `debug` (logs runtime) ;
|
|
67
|
+
- `extraInterceptors` (interceptors Angular additionnels).
|
|
68
|
+
|
|
69
|
+
### Cookies / `withCredentials`
|
|
70
|
+
|
|
71
|
+
Le bridge peut envoyer des cookies (sessions) côté HTTP et SSE.
|
|
72
|
+
|
|
73
|
+
Le comportement `withCredentials` par défaut est déduit de `mercure.init` :
|
|
74
|
+
- `credentials: 'include'` (défaut) → cookies envoyés
|
|
75
|
+
- `credentials: 'omit'` → cookies non envoyés
|
|
76
|
+
|
|
77
|
+
Vous pouvez aussi surcharger au cas par cas via `opts.withCredentials` sur les appels HTTP.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import {provideBridge} from '__PACKAGE_NAME__';
|
|
81
|
+
|
|
82
|
+
provideBridge({
|
|
83
|
+
baseUrl: 'https://api.example.com',
|
|
84
|
+
mercure: {
|
|
85
|
+
hubUrl: 'https://api.example.com/.well-known/mercure',
|
|
86
|
+
init: {credentials: 'omit'},
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Auth (Bearer)
|
|
92
|
+
|
|
93
|
+
`auth` accepte :
|
|
94
|
+
- une string (token Bearer),
|
|
95
|
+
- `{ type: 'bearer', token }`,
|
|
96
|
+
- `{ type: 'bearer', getToken }` (sync ou async),
|
|
97
|
+
- ou un `HttpInterceptorFn` custom.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import {provideBridge} from '__PACKAGE_NAME__';
|
|
101
|
+
|
|
102
|
+
provideBridge({
|
|
103
|
+
baseUrl: 'https://api.example.com',
|
|
104
|
+
auth: {type: 'bearer', getToken: () => localStorage.getItem('token') ?? undefined},
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Defaults (headers / timeout / retries)
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import {provideBridge} from '__PACKAGE_NAME__';
|
|
112
|
+
|
|
113
|
+
provideBridge({
|
|
114
|
+
baseUrl: 'https://api.example.com',
|
|
115
|
+
defaults: {
|
|
116
|
+
headers: {'X-Requested-With': 'fetch'},
|
|
117
|
+
timeoutMs: 15_000,
|
|
118
|
+
retries: {count: 2, delayMs: 250, methods: ['GET']},
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Debug
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import {provideBridge} from '__PACKAGE_NAME__';
|
|
127
|
+
|
|
128
|
+
provideBridge({baseUrl: 'https://api.example.com', debug: true});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Appeler l’API
|
|
132
|
+
|
|
133
|
+
### Style orienté ressource (`FacadeFactory` + `ResourceFacade<T>`)
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import {inject} from '@angular/core';
|
|
137
|
+
import {FacadeFactory, ResourceFacade, Item} from '__PACKAGE_NAME__';
|
|
138
|
+
|
|
139
|
+
type Book = Item & {title?: string};
|
|
140
|
+
|
|
141
|
+
export class BooksService {
|
|
142
|
+
private readonly factory = inject(FacadeFactory);
|
|
143
|
+
readonly books: ResourceFacade<Book> = this.factory.create<Book>({url: '/api/books'});
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Exemples :
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
// collection Hydra
|
|
151
|
+
this.books.getCollection$({page: 1, itemsPerPage: 20, filters: {title: 'Dune'}});
|
|
152
|
+
|
|
153
|
+
// item (souvent via @id)
|
|
154
|
+
this.books.get$(book['@id']!);
|
|
155
|
+
|
|
156
|
+
// write
|
|
157
|
+
this.books.post$({title: 'Neuromancer'});
|
|
158
|
+
this.books.patch$(book['@id']!, {title: 'Count Zero'});
|
|
159
|
+
this.books.delete$(book['@id']!);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Style ad-hoc (`BridgeFacade`)
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import {inject} from '@angular/core';
|
|
166
|
+
import {BridgeFacade} from '__PACKAGE_NAME__';
|
|
167
|
+
|
|
168
|
+
export class HealthService {
|
|
169
|
+
private readonly bridge = inject(BridgeFacade);
|
|
170
|
+
getHealth$() {
|
|
171
|
+
return this.bridge.get$<{status: string}>('/health');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Realtime (Mercure/SSE)
|
|
177
|
+
|
|
178
|
+
Le realtime est inactif tant que `mercure.hubUrl` n’est pas fourni à `provideBridge()`.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
provideBridge({
|
|
182
|
+
baseUrl: 'https://api.example.com',
|
|
183
|
+
mercure: {hubUrl: 'https://api.example.com/.well-known/mercure', topicMode: 'url'},
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Concurrence
|
|
188
|
+
|
|
189
|
+
Le bridge maintient une seule connexion SSE (une seule `EventSource`) et mutualise les topics :
|
|
190
|
+
- regarder plusieurs fois la même ressource ne crée pas plusieurs connexions ;
|
|
191
|
+
- un même topic est dédoublonné et géré par ref-count (unsubscribe effectif quand plus personne n’écoute).
|
|
192
|
+
|
|
193
|
+
Côté HTTP, le bridge déduplique les requêtes identiques tant qu’elles sont en cours (single-flight, activé par défaut) :
|
|
194
|
+
- `GET` / `HEAD` / `OPTIONS` : deux appels identiques partagent le même appel réseau et reçoivent la même réponse ;
|
|
195
|
+
- les méthodes avec body (`POST`/`PUT`/`PATCH`/`DELETE`) ne sont pas dédupliquées.
|
|
196
|
+
|
|
197
|
+
Ce n’est pas un cache : une fois la requête terminée, un nouvel appel identique relance un nouvel appel réseau.
|
|
198
|
+
|
|
199
|
+
Pour désactiver :
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
provideBridge({baseUrl: 'https://api.example.com', singleFlight: false});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
API :
|
|
206
|
+
- `ResourceFacade<T>.watch$(iri | iri[])` / `unwatch(iri | iri[])`
|
|
207
|
+
- `ResourceFacade<T>.watchSubResource$(iri | iri[], 'field.path')`
|
|
208
|
+
- `BridgeFacade.watch$(iri | iri[])` / `unwatch(iri | iri[])`
|
|
209
|
+
|
|
210
|
+
Note SSR : la connexion SSE ne s’ouvre que dans le navigateur.
|
|
211
|
+
|
|
212
|
+
## Models TypeScript
|
|
213
|
+
|
|
214
|
+
Les models générés (si présents) sont exportés au même niveau que le runtime :
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import type {Item} from '__PACKAGE_NAME__';
|
|
218
|
+
// import type {Book} from '__PACKAGE_NAME__';
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
En JSON-LD (`application/ld+json`), l’IRI est typiquement dans `model['@id']`.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {HttpEvent, HttpInterceptorFn, HttpRequest} from '@angular/common/http';
|
|
2
|
+
import {finalize, Observable, shareReplay} from 'rxjs';
|
|
3
|
+
|
|
4
|
+
export type SingleFlightMode = 'off' | 'safe';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MODE: SingleFlightMode = 'safe';
|
|
7
|
+
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
|
|
8
|
+
|
|
9
|
+
export function createSingleFlightInterceptor(mode: SingleFlightMode = DEFAULT_MODE): HttpInterceptorFn {
|
|
10
|
+
const inflight = new Map<string, Observable<HttpEvent<unknown>>>();
|
|
11
|
+
|
|
12
|
+
return (req, next) => {
|
|
13
|
+
if (mode === 'off') {
|
|
14
|
+
return next(req);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const method = req.method.toUpperCase();
|
|
18
|
+
if (!SAFE_METHODS.has(method)) {
|
|
19
|
+
return next(req);
|
|
20
|
+
}
|
|
21
|
+
if (req.reportProgress === true) {
|
|
22
|
+
return next(req);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const key = computeKey(req);
|
|
26
|
+
const existing = inflight.get(key);
|
|
27
|
+
if (existing) {
|
|
28
|
+
return existing;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const shared$ = next(req).pipe(
|
|
32
|
+
finalize(() => inflight.delete(key)),
|
|
33
|
+
shareReplay({bufferSize: 1, refCount: true})
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
inflight.set(key, shared$);
|
|
37
|
+
return shared$;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function computeKey(req: HttpRequest<unknown>): string {
|
|
42
|
+
const auth = req.headers.get('Authorization') ?? '';
|
|
43
|
+
const accept = req.headers.get('Accept') ?? '';
|
|
44
|
+
const contentType = req.headers.get('Content-Type') ?? '';
|
|
45
|
+
const creds = req.withCredentials ? '1' : '0';
|
|
46
|
+
return [
|
|
47
|
+
req.method.toUpperCase(),
|
|
48
|
+
req.urlWithParams,
|
|
49
|
+
`rt=${req.responseType}`,
|
|
50
|
+
`wc=${creds}`,
|
|
51
|
+
`a=${auth}`,
|
|
52
|
+
`acc=${accept}`,
|
|
53
|
+
`ct=${contentType}`,
|
|
54
|
+
].join('::');
|
|
55
|
+
}
|
|
@@ -13,6 +13,7 @@ import {contentTypeInterceptor} from './interceptors/content-type.interceptor';
|
|
|
13
13
|
import {bridgeDefaultsInterceptor} from './interceptors/bridge-defaults.interceptor';
|
|
14
14
|
import {bridgeDebugInterceptor} from './interceptors/bridge-debug.interceptor';
|
|
15
15
|
import {from, switchMap} from 'rxjs';
|
|
16
|
+
import {createSingleFlightInterceptor} from './interceptors/singleflight.interceptor';
|
|
16
17
|
|
|
17
18
|
export type BridgeAuth =
|
|
18
19
|
| string
|
|
@@ -37,6 +38,13 @@ export interface BridgeOptions {
|
|
|
37
38
|
mercure?: BridgeMercureOptions;
|
|
38
39
|
/** Default HTTP behaviour (headers, timeout, retries). */
|
|
39
40
|
defaults?: BridgeDefaults;
|
|
41
|
+
/**
|
|
42
|
+
* De-duplicates in-flight HTTP requests.
|
|
43
|
+
*
|
|
44
|
+
* - `true` (default): single-flight for safe methods (`GET/HEAD/OPTIONS`)
|
|
45
|
+
* - `false`: disabled (each call triggers a new request)
|
|
46
|
+
*/
|
|
47
|
+
singleFlight?: boolean;
|
|
40
48
|
/** Enables debug logging via the debug interceptor and console logger. */
|
|
41
49
|
debug?: boolean;
|
|
42
50
|
/** Extra `HttpInterceptorFn` applied after bridge interceptors. */
|
|
@@ -50,6 +58,7 @@ export function provideBridge(opts: BridgeOptions): EnvironmentProviders {
|
|
|
50
58
|
auth,
|
|
51
59
|
mercure,
|
|
52
60
|
defaults,
|
|
61
|
+
singleFlight = true,
|
|
53
62
|
debug = false,
|
|
54
63
|
extraInterceptors = [],
|
|
55
64
|
} = opts;
|
|
@@ -69,6 +78,7 @@ export function provideBridge(opts: BridgeOptions): EnvironmentProviders {
|
|
|
69
78
|
contentTypeInterceptor,
|
|
70
79
|
bridgeDefaultsInterceptor,
|
|
71
80
|
...createAuthInterceptors(auth),
|
|
81
|
+
...(singleFlight ? [createSingleFlightInterceptor('safe')] : [createSingleFlightInterceptor('off')]),
|
|
72
82
|
...(debug ? [bridgeDebugInterceptor] : []),
|
|
73
83
|
...extraInterceptors,
|
|
74
84
|
];
|
package/tools/core/generate.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
-
import {
|
|
2
|
+
import {existsSync} from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import {fileURLToPath} from 'node:url';
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import {readJson, writeJsonIfChanged} from './json.js';
|
|
7
|
+
import {renderTemplateToFile} from '../generator/models/handlebars.js';
|
|
8
|
+
import {ensureCleanDir} from '../generator/models/utils.js';
|
|
9
|
+
import {buildModelsFromOpenAPI} from '../generator/models/openapi-to-models.js';
|
|
10
10
|
|
|
11
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
12
|
const __dirname = path.dirname(__filename);
|
|
@@ -14,7 +14,7 @@ const __dirname = path.dirname(__filename);
|
|
|
14
14
|
const pkgRoot = path.resolve(__dirname, '../..'); // packages/meridiane
|
|
15
15
|
|
|
16
16
|
async function replacePlaceholdersInDir(dir, placeholders) {
|
|
17
|
-
const entries = await fs.readdir(dir, {
|
|
17
|
+
const entries = await fs.readdir(dir, {withFileTypes: true});
|
|
18
18
|
for (const entry of entries) {
|
|
19
19
|
const fullPath = path.join(dir, entry.name);
|
|
20
20
|
if (entry.isDirectory()) {
|
|
@@ -29,17 +29,25 @@ async function replacePlaceholdersInDir(dir, placeholders) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
async function generateLibrary({
|
|
32
|
+
async function generateLibrary({cwd, libName, packageName, version, debug}) {
|
|
33
33
|
const tplDir = path.resolve(pkgRoot, 'templates/_lib_template');
|
|
34
34
|
const targetDir = path.resolve(cwd, 'projects', libName);
|
|
35
35
|
|
|
36
|
-
if (debug) console.log('[meridiane] generate lib', {
|
|
36
|
+
if (debug) console.log('[meridiane] generate lib', {
|
|
37
|
+
libName,
|
|
38
|
+
packageName,
|
|
39
|
+
version,
|
|
40
|
+
targetDir: path.relative(cwd, targetDir)
|
|
41
|
+
});
|
|
37
42
|
|
|
38
|
-
await fs.rm(targetDir, {
|
|
39
|
-
await fs.mkdir(targetDir, {
|
|
40
|
-
await fs.cp(tplDir, targetDir, {
|
|
43
|
+
await fs.rm(targetDir, {recursive: true, force: true});
|
|
44
|
+
await fs.mkdir(targetDir, {recursive: true});
|
|
45
|
+
await fs.cp(tplDir, targetDir, {recursive: true});
|
|
41
46
|
|
|
42
|
-
await replacePlaceholdersInDir(targetDir, {
|
|
47
|
+
await replacePlaceholdersInDir(targetDir, {
|
|
48
|
+
'__LIB_NAME__': libName,
|
|
49
|
+
'__PACKAGE_NAME__': packageName,
|
|
50
|
+
});
|
|
43
51
|
|
|
44
52
|
const libPackageJsonPath = path.join(targetDir, 'package.json');
|
|
45
53
|
if (existsSync(libPackageJsonPath)) {
|
|
@@ -52,7 +60,7 @@ async function generateLibrary({ cwd, libName, packageName, version, debug }) {
|
|
|
52
60
|
// angular.json / workspace patching is intentionally not done here (Meridiane is standalone).
|
|
53
61
|
}
|
|
54
62
|
|
|
55
|
-
async function generateModels({
|
|
63
|
+
async function generateModels({cwd, libName, spec, requiredMode, formats, include, exclude, debug}) {
|
|
56
64
|
const outDir = path.resolve(cwd, 'projects', libName, 'src', 'models');
|
|
57
65
|
const templatesDir = path.resolve(pkgRoot, 'tools', 'generator', 'models', 'templates');
|
|
58
66
|
|
|
@@ -68,7 +76,7 @@ async function generateModels({ cwd, libName, spec, requiredMode, formats, inclu
|
|
|
68
76
|
await ensureCleanDir(outDir);
|
|
69
77
|
|
|
70
78
|
// Write models + index.ts.
|
|
71
|
-
const {
|
|
79
|
+
const {models} = buildModelsFromOpenAPI(spec, {
|
|
72
80
|
requiredMode,
|
|
73
81
|
formats: Array.isArray(formats) ? formats : [],
|
|
74
82
|
includeSchemaNames: include,
|
|
@@ -92,7 +100,7 @@ async function generateModels({ cwd, libName, spec, requiredMode, formats, inclu
|
|
|
92
100
|
await renderTemplateToFile({
|
|
93
101
|
templatePath: path.join(templatesDir, 'index.hbs'),
|
|
94
102
|
outPath: path.join(outDir, 'index.ts'),
|
|
95
|
-
ctx: {
|
|
103
|
+
ctx: {models: models.map((m) => ({name: m.name}))},
|
|
96
104
|
});
|
|
97
105
|
|
|
98
106
|
if (debug) console.log(`[meridiane] ${models.length} model(s) generated`);
|
|
@@ -134,7 +142,7 @@ export async function generateBridgeWorkspace(params) {
|
|
|
134
142
|
} = params;
|
|
135
143
|
|
|
136
144
|
log?.step?.(`génération de la librairie (projects/${libName})`);
|
|
137
|
-
await generateLibrary({
|
|
145
|
+
await generateLibrary({cwd, libName, packageName, version, debug});
|
|
138
146
|
|
|
139
147
|
// If a dist root is provided, force ng-packagr output to that directory.
|
|
140
148
|
if (distRoot) {
|
|
@@ -144,7 +152,7 @@ export async function generateBridgeWorkspace(params) {
|
|
|
144
152
|
const projectDir = path.dirname(ngPackagePath);
|
|
145
153
|
ngPkg.dest = path.relative(projectDir, desiredDestAbs).split(path.sep).join('/');
|
|
146
154
|
await writeJsonIfChanged(ngPackagePath, ngPkg);
|
|
147
|
-
log?.debug?.('ng-package.json dest overridden', {
|
|
155
|
+
log?.debug?.('ng-package.json dest overridden', {dest: ngPkg.dest});
|
|
148
156
|
}
|
|
149
157
|
|
|
150
158
|
if (noModels) {
|
|
@@ -155,6 +163,6 @@ export async function generateBridgeWorkspace(params) {
|
|
|
155
163
|
|
|
156
164
|
if (!spec) throw new Error('Internal error: spec is required when noModels=false');
|
|
157
165
|
log?.step?.('génération des models (OpenAPI)');
|
|
158
|
-
await generateModels({
|
|
166
|
+
await generateModels({cwd, libName, spec, requiredMode, formats, include, exclude, debug});
|
|
159
167
|
log?.success?.('génération terminée');
|
|
160
168
|
}
|