@obsidiane/meridiane 2.0.13 → 3.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.
Files changed (58) hide show
  1. package/README.md +30 -68
  2. package/cli.js +1 -10
  3. package/package.json +1 -1
  4. package/src/application/bridge-workflow.js +23 -66
  5. package/src/application/generate-bridge.js +32 -36
  6. package/src/application/run-build.js +8 -30
  7. package/src/application/run-dev.js +8 -63
  8. package/src/application/run-generate.js +8 -47
  9. package/src/domain/bridge-options.js +10 -63
  10. package/src/generators/bridge/write-bridge-files.js +11 -29
  11. package/src/generators/models/interface-schema.js +3 -20
  12. package/src/generators/models/model-selection.js +67 -106
  13. package/src/generators/models/oas-contract.js +1 -108
  14. package/src/generators/models/oas-schema-name.js +1 -19
  15. package/src/generators/models/openapi-to-models.js +51 -42
  16. package/src/generators/models/schema-utils.js +0 -34
  17. package/src/generators/models/type-resolver.js +2 -20
  18. package/src/generators/models/utils.js +0 -11
  19. package/src/generators/models/write-model-files.js +5 -15
  20. package/src/infrastructure/dev-project.js +0 -33
  21. package/src/infrastructure/files.js +15 -0
  22. package/src/infrastructure/local-package-install.js +2 -2
  23. package/src/infrastructure/logger.js +0 -2
  24. package/templates/angular-bridge/README.md +29 -281
  25. package/templates/angular-bridge/package.json +2 -2
  26. package/templates/angular-bridge/src/lib/bridge/rest/http-request.options.ts +10 -19
  27. package/templates/angular-bridge/src/lib/bridge/sse/mercure-hub.ts +449 -0
  28. package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.ts +28 -0
  29. package/templates/angular-bridge/src/lib/bridge/sse/resource-topic.ts +83 -0
  30. package/templates/angular-bridge/src/lib/bridge-options.ts +21 -17
  31. package/templates/angular-bridge/src/lib/bridge.types.ts +22 -33
  32. package/templates/angular-bridge/src/lib/facades/bridge.facade.ts +58 -134
  33. package/templates/angular-bridge/src/lib/facades/facade.factory.ts +25 -25
  34. package/templates/angular-bridge/src/lib/facades/facade.interface.ts +4 -9
  35. package/templates/angular-bridge/src/lib/facades/resource.facade.ts +38 -61
  36. package/templates/angular-bridge/src/lib/interceptors/bearer-auth.interceptor.ts +8 -12
  37. package/templates/angular-bridge/src/lib/interceptors/bridge-defaults.interceptor.ts +19 -11
  38. package/templates/angular-bridge/src/lib/interceptors/content-type.interceptor.ts +5 -17
  39. package/templates/angular-bridge/src/lib/interceptors/singleflight.interceptor.ts +28 -24
  40. package/templates/angular-bridge/src/lib/ports/realtime.port.ts +34 -53
  41. package/templates/angular-bridge/src/lib/ports/resource-repository.port.ts +31 -21
  42. package/templates/angular-bridge/src/lib/provide-bridge.ts +14 -38
  43. package/templates/angular-bridge/src/lib/tokens.ts +4 -15
  44. package/templates/angular-bridge/src/lib/utils/request-url.ts +4 -6
  45. package/templates/angular-bridge/src/lib/utils/url.ts +32 -9
  46. package/templates/angular-bridge/src/public-api.ts +0 -1
  47. package/templates/angular-bridge/tsconfig.lib.json +0 -3
  48. package/src/infrastructure/json.js +0 -71
  49. package/templates/angular-bridge/src/lib/bridge/rest/api-platform.adapter.ts +0 -82
  50. package/templates/angular-bridge/src/lib/bridge/sse/eventsource-wrapper.ts +0 -70
  51. package/templates/angular-bridge/src/lib/bridge/sse/mercure-connections.ts +0 -255
  52. package/templates/angular-bridge/src/lib/bridge/sse/mercure-payload.ts +0 -82
  53. package/templates/angular-bridge/src/lib/bridge/sse/mercure-topic.mapper.ts +0 -48
  54. package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.builder.ts +0 -39
  55. package/templates/angular-bridge/src/lib/bridge/sse/mercure.adapter.ts +0 -358
  56. package/templates/angular-bridge/src/lib/bridge/sse/ref-count-topic.registry.ts +0 -36
  57. package/templates/angular-bridge/src/lib/bridge/sse/subscription-claims.ts +0 -76
  58. package/templates/angular-bridge/src/lib/utils/iri.ts +0 -6
package/README.md CHANGED
@@ -1,85 +1,47 @@
1
1
  # @obsidiane/meridiane
2
2
 
3
- CLI pour générer un bridge Angular (runtime + models TypeScript) depuis une spec OpenAPI (API Platform + Mercure).
3
+ Génère, depuis le contrat OpenAPI d'API Platform, une librairie Angular (« le bridge ») :
4
4
 
5
- Documentation complète du repo : `docs/guide-bridge.md`.
5
+ - les **modèles TypeScript** de vos ressources ;
6
+ - un **client HTTP** aux conventions API Platform (JSON-LD, Hydra, merge-patch) ;
7
+ - le **temps réel Mercure 1.0** : abonnement à une ressource ou à un item, connexions partagées, autorisation par cookie.
6
8
 
7
- ## Installation
9
+ Le bridge gère la connexion ; la gestion des données reste dans votre application. Il est compatible avec Angular 20, 21 et 22.
8
10
 
9
- ```bash
10
- npm i -D @obsidiane/meridiane
11
- ```
12
-
13
- ## Commandes
14
-
15
- ### `meridiane generate <packageName>`
16
-
17
- Génère uniquement les sources du bridge dans le workspace courant.
11
+ ## En bref
18
12
 
19
13
  ```bash
20
- npx meridiane generate @acme/backend-bridge --spec ./openapi.json
14
+ npx @obsidiane/meridiane@3 generate @app/bridge --spec http://localhost:8000/api/docs.json --out bridge
21
15
  ```
22
16
 
23
- Sortie par défaut : `projects/<libName>/` (modifiable via `--out`).
17
+ ```ts
18
+ // app.config.ts
19
+ provideBridge({
20
+ baseUrl: '/',
21
+ mercure: {hubUrl: '/.well-known/mercure', authorize: '/api/mercure/authorization'},
22
+ });
24
23
 
25
- ### `meridiane dev [packageName]`
24
+ // project.repository.ts
25
+ private readonly projects = inject(FacadeFactory).create<Project>({url: '/api/projects'});
26
26
 
27
- Génération + build standalone + `npm pack` + installation locale dans `node_modules`.
28
-
29
- ```bash
30
- npx meridiane dev @acme/backend-bridge --spec http://localhost:8000/api/docs.json
27
+ list$ = this.projects.getCollection$({page: 1, itemsPerPage: 20}); // Collection<Project>
28
+ changes$ = this.projects.watch$(); // Project | ResourceDeletion, en direct
31
29
  ```
32
30
 
33
- Sorties :
34
- - `dist/<libName>/` + `dist/<libName>/*.tgz`
35
- - `node_modules/<packageName>/`
36
-
37
- ### `meridiane build <packageName>`
38
-
39
- Génération + build standalone + `npm pack` (artefact publiable).
40
-
41
- ```bash
42
- npx meridiane build @acme/backend-bridge --version 1.2.3 --spec https://staging.example/api/docs.json
43
- ```
44
-
45
- Sorties :
46
- - `dist/<libName>/`
47
- - `dist/<libName>/*.tgz`
48
-
49
- ## Options
50
-
51
- - `--spec <url|file>` : URL OpenAPI ou fichier JSON local (requis sauf `--no-models`)
52
- - `--formats <mimeTypes>` : répétable ou liste `,` ; défaut `application/ld+json`
53
- - `--include <substr>` / `--exclude <substr>` : filtres de noms de schémas (répétables, support `,`)
54
- - `--no-models` : runtime only
55
- - `--version <semver>` : `build` et `generate` (défaut `0.0.0`)
56
- - `--out <dir>` : uniquement `generate`
57
- - `--debug` : logs détaillés
58
-
59
- ## Notes importantes
60
-
61
- - fallback automatique `.../api/docs.json` -> `.../api/docs.jsonopenapi` si nécessaire
62
- - mode contract-driven : uniquement les schémas atteignables via `paths` et `webhooks` pour les formats sélectionnés
63
- - modèles `*jsonMergePatch*` non générés (`PATCH` typé `Partial<T>`)
64
- - Meridiane ne publie pas : publication via `npm publish` côté CI
65
-
66
- ## Couverture des schémas
67
-
68
- Le compilateur suit les types d’[OpenAPI 3.0](https://spec.openapis.org/oas/v3.0.4.html) et le dialecte
69
- [JSON Schema 2020-12 d’OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0.html#schema-object) :
31
+ ## Commandes
70
32
 
71
- - primitives, unions de `type`, objets, propriétés requises, dictionnaires et schémas booléens ;
72
- - tableaux homogènes, tuples `prefixItems`, tuples historiques et bornes de longueur des tuples ;
73
- - `enum` et `const`, y compris les valeurs JSON structurées ;
74
- - `allOf`, `oneOf`, `anyOf`, `nullable` et références locales vers `components.schemas` ou `$defs` ;
75
- - schémas racine objets sous forme d’interfaces et autres schémas sous forme d’aliases TypeScript ;
76
- - contrats atteignables depuis les bodies, paramètres, en-têtes, callbacks et webhooks.
33
+ | Commande | Produit |
34
+ |---|---|
35
+ | `generate <package>` | les sources du bridge dans le projet (`--out`) |
36
+ | `build <package>` | un package compilé et son `.tgz` dans `dist/`, prêt à publier |
37
+ | `dev <package>` | le package compilé, installé dans `node_modules` |
77
38
 
78
- Les règles de validation sans équivalent structurel TypeScript (`pattern`, bornes numériques,
79
- `uniqueItems`, `contains`, `not`, conditions, fermeture exacte des propriétés, etc.) restent à la
80
- charge de la validation runtime. Les références externes doivent être préalablement regroupées dans
81
- le document OpenAPI fourni à Meridiane.
39
+ Options : `--spec`, `--include`, `--exclude`, `--no-models`, `--version`, `--out`, `--debug`. Node.js ≥ 24.
82
40
 
83
- ## Spécifique à ce repo
41
+ ## Documentation
84
42
 
85
- Dans ce monorepo, `meridiane dev` peut être exécuté sans `packageName` et cible alors `@obsidiane/bridge-sandbox` (app `playground/angular-app`).
43
+ - [Démarrage](https://github.com/obsidiane-lab/meridiane/blob/master/docs/demarrage.md)
44
+ - [Configuration (cookies, auth, Mercure)](https://github.com/obsidiane-lab/meridiane/blob/master/docs/configuration.md)
45
+ - [HTTP](https://github.com/obsidiane-lab/meridiane/blob/master/docs/http.md) · [Temps réel](https://github.com/obsidiane-lab/meridiane/blob/master/docs/temps-reel.md) · [Backend](https://github.com/obsidiane-lab/meridiane/blob/master/docs/backend.md)
46
+ - [Recettes](https://github.com/obsidiane-lab/meridiane/blob/master/docs/recettes.md) · [Référence](https://github.com/obsidiane-lab/meridiane/blob/master/docs/reference.md) · [CLI](https://github.com/obsidiane-lab/meridiane/blob/master/docs/cli.md)
47
+ - [Dépannage](https://github.com/obsidiane-lab/meridiane/blob/master/docs/depannage.md) · [Migration 2.x → 3.0](https://github.com/obsidiane-lab/meridiane/blob/master/docs/migration-3.0.md)
package/cli.js CHANGED
@@ -18,12 +18,6 @@ function collect(value, previous = []) {
18
18
  function commonOptions(cmd) {
19
19
  return cmd
20
20
  .option('--spec <urlOrFile>', 'OpenAPI spec source (URL or local JSON file)')
21
- .option(
22
- '--formats <mimeTypes>',
23
- 'Generate only selected media types (repeatable, supports commas). Enables contract-driven mode from paths.',
24
- collect,
25
- []
26
- )
27
21
  .option('--include <substr>', 'Include schema names containing substring (repeatable, supports commas)', collect, [])
28
22
  .option('--exclude <substr>', 'Exclude schema names containing substring (repeatable, supports commas)', collect, [])
29
23
  .option('--no-models', 'Skip models generation (then --spec is not required)')
@@ -47,10 +41,7 @@ function action(run) {
47
41
  commonOptions(
48
42
  program
49
43
  .command('dev')
50
- .argument(
51
- '[packageName]',
52
- 'NPM package name of the generated bridge (e.g. @acme/backend-bridge). Optional in this repo (defaults to @obsidiane/bridge-sandbox).'
53
- )
44
+ .argument('<packageName>', 'NPM package name of the generated bridge (e.g. @acme/backend-bridge)')
54
45
  .action(action(runDev))
55
46
  );
56
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@obsidiane/meridiane",
3
- "version": "2.0.13",
3
+ "version": "3.0.0",
4
4
  "description": "Générateur de librairie Angular et modèles TypeScript (OpenAPI) pour un bridge Symfony/API Platform + Mercure.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -3,75 +3,42 @@ import path from 'node:path';
3
3
  import fs from 'node:fs/promises';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
6
- import { generateBridgeWorkspace } from './generate-bridge.js';
7
- import { readOpenApiSpec } from '../infrastructure/spec-loader.js';
6
+ import { generateBridge } from './generate-bridge.js';
8
7
  import { ExitCodeError, runCommand } from '../infrastructure/exec.js';
8
+ import { ensureCleanDir } from '../infrastructure/files.js';
9
9
 
10
10
  const ngPackagrPath = fileURLToPath(import.meta.resolve('ng-packagr/src/cli/main.js'));
11
+ /** `node_modules` holding the CLI's Angular toolchain (wherever npx installed it). */
12
+ const toolchainModules = path.resolve(fileURLToPath(import.meta.resolve('@angular/core/package.json')), '../../..');
11
13
 
12
- export async function runBridgeWorkflow({
13
- repoRoot,
14
- libName,
15
- packageName,
16
- version,
17
- noModels,
18
- spec,
19
- specSource,
20
- requiredMode,
21
- formats,
22
- include,
23
- exclude,
24
- debug,
25
- log,
26
- tsconfigPath,
27
- packRelativeTo,
28
- projectReadmePath,
29
- }) {
14
+ /** Generates the bridge in an isolated workspace under `dist/`, builds it with ng-packagr and packs it. */
15
+ export async function buildBridgePackage({ config, repoRoot, tsconfigPath, log }) {
30
16
  const distRoot = path.join(repoRoot, 'dist');
31
17
  const buildRoot = path.join(distRoot, '.meridiane-workspace');
32
- const npmCacheDir = path.join(buildRoot, '.npm-cache');
33
- await fs.rm(buildRoot, { recursive: true, force: true });
34
- await fs.mkdir(buildRoot, { recursive: true });
35
- log?.info?.(`workspace: ${buildRoot}`);
36
-
37
- const resolvedSpec = await loadSpecIfNeeded({ noModels, spec, specSource, log });
38
-
39
- await generateBridgeWorkspace({
40
- cwd: buildRoot,
41
- libName,
42
- packageName,
43
- version,
44
- noModels,
45
- spec: resolvedSpec,
46
- requiredMode,
47
- formats,
48
- include,
49
- exclude,
50
- debug,
51
- log,
52
- distRoot,
53
- projectReadmePath,
54
- });
55
-
56
- const projectDir = path.join(buildRoot, 'projects', libName);
57
- const ngPackageJsonPath = path.join(projectDir, 'ng-package.json');
58
- const tsconfigAbsPath = path.join(projectDir, tsconfigPath);
59
-
60
- log?.step?.('ng-packagr');
61
- const packagrCode = await runCommand(process.execPath, [ngPackagrPath, '-p', ngPackageJsonPath, '-c', tsconfigAbsPath], {
62
- cwd: buildRoot,
63
- });
18
+ const projectDir = path.join(buildRoot, 'projects', config.libName);
19
+ await ensureCleanDir(buildRoot);
20
+ // The bridge compiles against the CLI's Angular and RxJS, not the dependencies of the current project.
21
+ await fs.symlink(toolchainModules, path.join(buildRoot, 'node_modules'), 'junction');
22
+ log.info(`workspace: ${path.relative(repoRoot, buildRoot)}`);
23
+
24
+ await generateBridge({ config, root: repoRoot, projectDir, distRoot, log });
25
+
26
+ log.step('ng-packagr');
27
+ const packagrCode = await runCommand(
28
+ process.execPath,
29
+ [ngPackagrPath, '-p', path.join(projectDir, 'ng-package.json'), '-c', path.join(projectDir, tsconfigPath)],
30
+ { cwd: buildRoot }
31
+ );
64
32
  if (packagrCode !== 0) throw new ExitCodeError(`ng-packagr failed (exit ${packagrCode})`, packagrCode);
65
33
 
66
- const distDir = path.join(distRoot, libName);
67
- const packRel = packRelativeTo ? path.relative(packRelativeTo, distDir) : distDir;
68
- log?.step?.(`npm pack (${packRel})`);
34
+ const distDir = path.join(distRoot, config.libName);
35
+ log.step(`npm pack (${path.relative(repoRoot, distDir)})`);
69
36
  await cleanupPackedTarballs(distDir);
70
37
  const packCode = await runCommand('npm', ['pack'], {
71
38
  cwd: distDir,
72
39
  env: {
73
40
  ...process.env,
74
- npm_config_cache: npmCacheDir,
41
+ npm_config_cache: path.join(buildRoot, '.npm-cache'),
75
42
  npm_config_update_notifier: 'false',
76
43
  npm_config_fund: 'false',
77
44
  npm_config_audit: 'false',
@@ -82,16 +49,6 @@ export async function runBridgeWorkflow({
82
49
  return { distDir };
83
50
  }
84
51
 
85
- async function loadSpecIfNeeded({ noModels, spec, specSource, log }) {
86
- if (noModels || spec) return spec;
87
- if (!specSource) throw new Error('Missing --spec (url or JSON file path)');
88
-
89
- log?.step?.('lecture de la spec OpenAPI');
90
- const loadedSpec = await readOpenApiSpec(specSource);
91
- log?.success?.('spec chargée');
92
- return loadedSpec;
93
- }
94
-
95
52
  async function cleanupPackedTarballs(distDir) {
96
53
  const entries = await fs.readdir(distDir, {withFileTypes: true});
97
54
  const tarballs = entries.filter((entry) => entry.isFile() && entry.name.endsWith('.tgz'));
@@ -2,49 +2,45 @@ import path from 'node:path';
2
2
 
3
3
  import { configureBridgeOutput, writeBridgeFiles } from '../generators/bridge/write-bridge-files.js';
4
4
  import { writeModelFiles } from '../generators/models/write-model-files.js';
5
+ import { readOpenApiSpec } from '../infrastructure/spec-loader.js';
5
6
 
6
- /** Generates the Angular bridge sources in a workspace project. */
7
- export async function generateBridgeWorkspace({
8
- cwd,
9
- libName,
10
- packageName,
11
- version,
12
- noModels,
13
- spec,
14
- requiredMode,
15
- formats,
16
- include,
17
- exclude,
18
- debug,
19
- log,
20
- distRoot,
21
- projectDir: configuredProjectDir,
22
- projectReadmePath,
23
- }) {
24
- const projectDir = configuredProjectDir ?? path.resolve(cwd, 'projects', libName);
7
+ /** Logs the command title and its configuration. */
8
+ export function logConfig(log, title, config, details) {
9
+ log.title(title);
10
+ for (const [label, value] of Object.entries(details)) log.info(`${label}: ${value}`);
11
+ log.info(`package: ${config.packageName}@${config.version}`);
12
+ log.info(`lib: ${config.libName}`);
13
+ log.info(config.noModels ? 'models: désactivés (--no-models)' : `spec: ${config.specSource}`);
14
+ }
15
+
16
+ /**
17
+ * Generates the Angular bridge sources (library files and models) in `projectDir`.
18
+ * The project README of `root` is appended to the bridge README.
19
+ */
20
+ export async function generateBridge({ config, root, projectDir, distRoot, log }) {
21
+ const spec = config.noModels ? undefined : await loadSpec(config.specSource, log);
25
22
 
26
- log?.step?.(`génération de la librairie (${path.relative(cwd, projectDir)})`);
23
+ log.step(`génération de la librairie (${path.relative(root, projectDir)})`);
27
24
  await writeBridgeFiles({
28
25
  projectDir,
29
- libName,
30
- packageName,
31
- version,
32
- projectReadmePath,
33
- debug,
26
+ libName: config.libName,
27
+ packageName: config.packageName,
28
+ version: config.version,
29
+ projectReadmePath: path.join(root, 'README.md'),
34
30
  log,
35
31
  });
32
+ if (distRoot) await configureBridgeOutput({ projectDir, distRoot, libName: config.libName, log });
36
33
 
37
- if (distRoot) await configureBridgeOutput({ projectDir, distRoot, libName, log });
38
-
39
- if (noModels) {
40
- log?.info?.('models ignorés (--no-models)');
41
- if (debug) log?.debug?.('[meridiane] --no-models enabled, skipping models generation');
42
- return;
43
- }
34
+ if (!spec) return;
35
+ log.step('génération des models (OpenAPI)');
36
+ await writeModelFiles({ projectDir, spec, include: config.include, exclude: config.exclude, log });
37
+ }
44
38
 
45
- if (!spec) throw new Error('Internal error: spec is required when noModels=false');
39
+ async function loadSpec(specSource, log) {
40
+ if (!specSource) throw new Error('Missing --spec (url or JSON file path)');
46
41
 
47
- log?.step?.('génération des models (OpenAPI)');
48
- await writeModelFiles({ projectDir, spec, requiredMode, formats, include, exclude, debug, log });
49
- log?.success?.('génération terminée');
42
+ log.step('lecture de la spec OpenAPI');
43
+ const spec = await readOpenApiSpec(specSource);
44
+ log.success('spec chargée');
45
+ return spec;
50
46
  }
@@ -1,41 +1,19 @@
1
1
  import process from 'node:process';
2
2
  import path from 'node:path';
3
3
 
4
- import { normalizeBuildOptions } from '../domain/bridge-options.js';
4
+ import { normalizeOptions } from '../domain/bridge-options.js';
5
5
  import { createLogger } from '../infrastructure/logger.js';
6
- import { runBridgeWorkflow } from './bridge-workflow.js';
6
+ import { buildBridgePackage } from './bridge-workflow.js';
7
+ import { logConfig } from './generate-bridge.js';
7
8
 
8
9
  export async function runBuild(packageName, opts) {
9
- const cwd = process.cwd();
10
- const config = normalizeBuildOptions(packageName, opts);
10
+ const repoRoot = process.cwd();
11
+ const config = normalizeOptions(packageName, opts);
11
12
  const log = createLogger({ debug: config.debug });
12
- const projectReadmePath = path.join(cwd, 'README.md');
13
13
 
14
- log.title('build — standalone');
15
- log.info(`repo: ${cwd}`);
16
- log.info(`package: ${config.packageName}@${config.version}`);
17
- log.info(`lib: ${config.libName}`);
18
- if (config.noModels) log.info('models: désactivés (--no-models)');
19
- else log.info(`spec: ${opts.spec} | formats: ${config.formats.join(', ')} | nullableMode: ${config.requiredMode}`);
14
+ logConfig(log, 'build — standalone', config, { repo: repoRoot });
15
+ const { distDir } = await buildBridgePackage({ config, repoRoot, tsconfigPath: 'tsconfig.lib.prod.json', log });
20
16
 
21
- const { distDir } = await runBridgeWorkflow({
22
- repoRoot: cwd,
23
- libName: config.libName,
24
- packageName: config.packageName,
25
- version: config.version,
26
- noModels: config.noModels,
27
- specSource: opts.spec,
28
- requiredMode: config.requiredMode,
29
- formats: config.formats,
30
- include: config.include,
31
- exclude: config.exclude,
32
- debug: config.debug,
33
- log,
34
- tsconfigPath: 'tsconfig.lib.prod.json',
35
- packRelativeTo: cwd,
36
- projectReadmePath,
37
- });
38
-
39
- log.success(`package prêt: ${path.relative(cwd, distDir)}`);
17
+ log.success(`package prêt: ${path.relative(repoRoot, distDir)}`);
40
18
  return 0;
41
19
  }
@@ -1,74 +1,19 @@
1
1
  import process from 'node:process';
2
- import path from 'node:path';
3
2
 
4
- import { normalizeDevOptions } from '../domain/bridge-options.js';
3
+ import { normalizeOptions } from '../domain/bridge-options.js';
5
4
  import { createLogger } from '../infrastructure/logger.js';
6
- import {resolveDevAppRoot, resolveSandboxAppRoot, resolveSpecSource} from '../infrastructure/dev-project.js';
5
+ import {resolveDevAppRoot} from '../infrastructure/dev-project.js';
7
6
  import {installPackageFromDist} from '../infrastructure/local-package-install.js';
8
- import { readOpenApiSpec } from '../infrastructure/spec-loader.js';
9
- import { runBridgeWorkflow } from './bridge-workflow.js';
10
-
11
- async function loadDevSpec({config, opts, appRoot, log}) {
12
- if (config.noModels) return {noModels: true};
13
-
14
- const specSource = await resolveSpecSource({
15
- useSandboxDefaults: config.useSandboxDefaults,
16
- appRoot,
17
- specArg: opts.spec,
18
- });
19
-
20
- if (!specSource) throw new Error('Missing --spec (url or JSON file path)');
21
-
22
- try {
23
- return {noModels: false, specSource, spec: await readOpenApiSpec(specSource)};
24
- } catch (error) {
25
- const canSkipModels = config.useSandboxDefaults && opts.spec == null;
26
- if (!canSkipModels) throw error;
27
-
28
- log.warn(
29
- 'spec OpenAPI indisponible (backend non démarré ?) → génération des models ignorée. ' +
30
- 'Démarrez le backend ou passez `--spec ./openapi.json` (ou `--no-models`).'
31
- );
32
- return {noModels: true, specSource};
33
- }
34
- }
7
+ import { buildBridgePackage } from './bridge-workflow.js';
8
+ import { logConfig } from './generate-bridge.js';
35
9
 
36
10
  export async function runDev(packageName, opts) {
37
- const startCwd = process.cwd();
38
- const config = normalizeDevOptions(packageName, opts);
11
+ const config = { ...normalizeOptions(packageName, opts), version: '0.0.0-dev' };
39
12
  const log = createLogger({ debug: config.debug });
13
+ const appRoot = await resolveDevAppRoot(process.cwd());
40
14
 
41
- const appRoot = config.useSandboxDefaults ? await resolveSandboxAppRoot(startCwd) : await resolveDevAppRoot(startCwd);
42
- const projectReadmePath = path.join(appRoot, 'README.md');
43
- const {noModels, specSource, spec} = await loadDevSpec({config, opts, appRoot, log});
44
-
45
- log.title('dev — build standalone + install local');
46
- log.info(`app: ${appRoot}`);
47
- log.info(`package: ${config.packageName}@${config.version}`);
48
- log.info(`lib: ${config.libName}`);
49
- if (config.useSandboxDefaults) log.info('defaults sandbox appliqués');
50
- if (noModels) log.info('models: désactivés (--no-models)');
51
- else log.info(`spec: ${specSource} | formats: ${config.formats.join(', ')} | nullableMode: ${config.requiredMode}`);
52
-
53
- const { distDir } = await runBridgeWorkflow({
54
- repoRoot: appRoot,
55
- libName: config.libName,
56
- packageName: config.packageName,
57
- version: config.version,
58
- noModels,
59
- spec,
60
- specSource,
61
- requiredMode: config.requiredMode,
62
- formats: config.formats,
63
- include: config.include,
64
- exclude: config.exclude,
65
- debug: config.debug,
66
- log,
67
- tsconfigPath: 'tsconfig.lib.json',
68
- packRelativeTo: appRoot,
69
- projectReadmePath,
70
- });
71
-
15
+ logConfig(log, 'dev — build standalone + install local', config, { app: appRoot });
16
+ const { distDir } = await buildBridgePackage({ config, repoRoot: appRoot, tsconfigPath: 'tsconfig.lib.json', log });
72
17
  await installPackageFromDist({ packageName: config.packageName, distDir, appRoot, log });
73
18
 
74
19
  log.success('bridge installé (node_modules) ✅');
@@ -1,58 +1,19 @@
1
1
  import process from 'node:process';
2
2
  import path from 'node:path';
3
3
 
4
- import { normalizeGenerateOptions } from '../domain/bridge-options.js';
5
- import { generateBridgeWorkspace } from './generate-bridge.js';
4
+ import { normalizeOptions } from '../domain/bridge-options.js';
6
5
  import { createLogger } from '../infrastructure/logger.js';
7
- import { readOpenApiSpec } from '../infrastructure/spec-loader.js';
8
-
9
- function resolveProjectDir({ cwd, libName, outDir }) {
10
- if (outDir && outDir.length > 0) return path.resolve(cwd, outDir);
11
- return path.resolve(cwd, 'projects', libName);
12
- }
6
+ import { generateBridge, logConfig } from './generate-bridge.js';
13
7
 
14
8
  export async function runGenerate(packageName, opts) {
15
- const cwd = process.cwd();
16
- const config = normalizeGenerateOptions(packageName, opts);
9
+ const root = process.cwd();
10
+ const config = normalizeOptions(packageName, opts);
17
11
  const log = createLogger({ debug: config.debug });
18
- const projectReadmePath = path.join(cwd, 'README.md');
19
-
20
- const projectDir = resolveProjectDir({ cwd, libName: config.libName, outDir: config.outDir });
21
- const specSource = opts?.spec;
22
-
23
- log.title('generate — workspace files');
24
- log.info(`repo: ${cwd}`);
25
- log.info(`package: ${config.packageName}@${config.version}`);
26
- log.info(`lib: ${config.libName}`);
27
- log.info(`target: ${path.relative(cwd, projectDir)}`);
28
- if (config.noModels) log.info('models: désactivés (--no-models)');
29
- else log.info(`spec: ${specSource} | formats: ${config.formats.join(', ')} | nullableMode: ${config.requiredMode}`);
30
-
31
- let spec;
32
- if (!config.noModels) {
33
- if (!specSource) throw new Error('Missing --spec (url or JSON file path)');
34
- log.step('lecture de la spec OpenAPI');
35
- spec = await readOpenApiSpec(specSource);
36
- log.success('spec chargée');
37
- }
12
+ const projectDir = path.resolve(root, config.outDir || path.join('projects', config.libName));
38
13
 
39
- await generateBridgeWorkspace({
40
- cwd,
41
- projectDir,
42
- libName: config.libName,
43
- packageName: config.packageName,
44
- version: config.version,
45
- noModels: config.noModels,
46
- spec,
47
- requiredMode: config.requiredMode,
48
- formats: config.formats,
49
- include: config.include,
50
- exclude: config.exclude,
51
- debug: config.debug,
52
- log,
53
- projectReadmePath,
54
- });
14
+ logConfig(log, 'generate — workspace files', config, { repo: root, target: path.relative(root, projectDir) });
15
+ await generateBridge({ config, root, projectDir, log });
55
16
 
56
- log.success(`bridge généré: ${path.relative(cwd, projectDir)}`);
17
+ log.success(`bridge généré: ${path.relative(root, projectDir)}`);
57
18
  return 0;
58
19
  }
@@ -1,72 +1,19 @@
1
1
  import { deriveLibName } from './lib-name.js';
2
2
  import {splitListArgs} from './list-values.js';
3
3
 
4
- const DEFAULT_FORMATS = ['application/ld+json'];
5
- const SANDBOX_PACKAGE_NAME = '@obsidiane/bridge-sandbox';
6
-
7
- function normalizeCommonOptions(opts) {
8
- const noModels = opts?.models === false;
9
- const formats = splitListArgs(opts?.formats);
10
- const include = splitListArgs(opts?.include);
11
- const exclude = splitListArgs(opts?.exclude);
12
- const debug = !!opts?.debug;
13
-
14
- return { noModels, formats, include, exclude, debug };
15
- }
16
-
17
- function normalizeFormats(formats) {
18
- return formats.length > 0 ? formats : [...DEFAULT_FORMATS];
19
- }
20
-
21
- function normalizeVersion(version) {
22
- const raw = typeof version === 'string' ? version.trim() : '';
23
- return raw || '0.0.0';
24
- }
25
-
26
- function normalizeNamedOptions(packageName, opts) {
27
- const common = normalizeCommonOptions(opts);
28
- const normalizedPackageName = String(packageName || '').trim();
29
- if (!normalizedPackageName) throw new Error('Missing packageName');
4
+ /** Normalizes the arguments of `generate`, `build` and `dev` into one configuration. */
5
+ export function normalizeOptions(packageName, opts = {}) {
6
+ const normalizedPackageName = String(packageName ?? '').trim();
30
7
 
31
8
  return {
32
9
  packageName: normalizedPackageName,
33
10
  libName: deriveLibName(normalizedPackageName),
34
- version: normalizeVersion(opts?.version),
35
- requiredMode: 'spec',
36
- formats: normalizeFormats(common.formats),
37
- include: common.include,
38
- exclude: common.exclude,
39
- noModels: common.noModels,
40
- debug: common.debug,
41
- };
42
- }
43
-
44
- export function normalizeBuildOptions(packageName, opts) {
45
- return normalizeNamedOptions(packageName, opts);
46
- }
47
-
48
- export function normalizeGenerateOptions(packageName, opts) {
49
- return {
50
- ...normalizeNamedOptions(packageName, opts),
51
- outDir: typeof opts?.out === 'string' ? opts.out.trim() : '',
52
- };
53
- }
54
-
55
- export function normalizeDevOptions(packageName, opts) {
56
- const common = normalizeCommonOptions(opts);
57
- const useSandboxDefaults = packageName === undefined;
58
- const effectivePackageName = String(packageName ?? SANDBOX_PACKAGE_NAME).trim();
59
-
60
- return {
61
- useSandboxDefaults,
62
- packageName: effectivePackageName,
63
- libName: deriveLibName(effectivePackageName),
64
- version: '0.0.0-dev',
65
- requiredMode: 'all',
66
- formats: normalizeFormats(common.formats),
67
- include: common.include,
68
- exclude: common.exclude,
69
- noModels: common.noModels,
70
- debug: common.debug,
11
+ version: (typeof opts.version === 'string' && opts.version.trim()) || '0.0.0',
12
+ specSource: opts.spec,
13
+ include: splitListArgs(opts.include),
14
+ exclude: splitListArgs(opts.exclude),
15
+ noModels: opts.models === false,
16
+ debug: !!opts.debug,
17
+ outDir: typeof opts.out === 'string' ? opts.out.trim() : '',
71
18
  };
72
19
  }