@obsidiane/meridiane 2.0.10 → 2.0.12

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 (91) hide show
  1. package/README.md +19 -2
  2. package/cli.js +21 -37
  3. package/package.json +13 -6
  4. package/src/README.md +9 -0
  5. package/{tools/app → src/application}/bridge-workflow.js +29 -23
  6. package/src/application/generate-bridge.js +50 -0
  7. package/{tools/app → src/application}/run-build.js +1 -1
  8. package/{tools/app → src/application}/run-dev.js +30 -36
  9. package/{tools/app → src/application}/run-generate.js +3 -3
  10. package/{tools → src}/domain/bridge-options.js +8 -21
  11. package/src/domain/list-values.js +6 -0
  12. package/src/generators/bridge/write-bridge-files.js +100 -0
  13. package/src/generators/models/interface-schema.js +116 -0
  14. package/src/generators/models/model-selection.js +148 -0
  15. package/src/generators/models/oas-contract.js +170 -0
  16. package/{tools/generator → src/generators}/models/oas-schema-name.js +6 -15
  17. package/src/generators/models/openapi-to-models.js +136 -0
  18. package/{tools/generator → src/generators}/models/schema-utils.js +27 -3
  19. package/src/generators/models/templates/type-alias.hbs +2 -0
  20. package/src/generators/models/type-expression.js +65 -0
  21. package/src/generators/models/type-resolver.js +240 -0
  22. package/src/generators/models/utils.js +89 -0
  23. package/src/generators/models/write-model-files.js +55 -0
  24. package/src/infrastructure/dev-project.js +48 -0
  25. package/{tools/infra → src/infrastructure}/exec.js +3 -2
  26. package/src/infrastructure/local-package-install.js +104 -0
  27. package/src/infrastructure/spec-loader.js +65 -0
  28. package/templates/{_lib_template → angular-bridge}/README.md +9 -0
  29. package/templates/angular-bridge/src/lib/bridge/rest/api-platform.adapter.ts +82 -0
  30. package/templates/angular-bridge/src/lib/bridge/rest/http-request.options.ts +30 -0
  31. package/templates/{_lib_template → angular-bridge}/src/lib/bridge/rest/query-builder.ts +10 -10
  32. package/templates/angular-bridge/src/lib/bridge/sse/mercure-connections.ts +255 -0
  33. package/templates/angular-bridge/src/lib/bridge/sse/mercure-payload.ts +82 -0
  34. package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.builder.ts +39 -0
  35. package/templates/angular-bridge/src/lib/bridge/sse/mercure.adapter.ts +358 -0
  36. package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/ref-count-topic.registry.ts +3 -12
  37. package/templates/angular-bridge/src/lib/bridge/sse/subscription-claims.ts +76 -0
  38. package/templates/angular-bridge/src/lib/bridge-options.ts +59 -0
  39. package/templates/{_lib_template → angular-bridge}/src/lib/facades/bridge.facade.ts +50 -51
  40. package/templates/{_lib_template → angular-bridge}/src/lib/facades/facade.factory.ts +0 -1
  41. package/templates/angular-bridge/src/lib/facades/facade.interface.ts +13 -0
  42. package/templates/{_lib_template → angular-bridge}/src/lib/facades/resource.facade.ts +24 -31
  43. package/templates/angular-bridge/src/lib/interceptors/bearer-auth.interceptor.ts +33 -0
  44. package/templates/angular-bridge/src/lib/interceptors/bridge-defaults.interceptor.ts +65 -0
  45. package/templates/angular-bridge/src/lib/interceptors/content-type.interceptor.ts +37 -0
  46. package/templates/angular-bridge/src/lib/interceptors/scoped.interceptor.ts +9 -0
  47. package/templates/angular-bridge/src/lib/interceptors/singleflight.interceptor.ts +46 -0
  48. package/templates/{_lib_template → angular-bridge}/src/lib/ports/realtime.port.ts +6 -2
  49. package/templates/{_lib_template → angular-bridge}/src/lib/ports/resource-repository.port.ts +7 -7
  50. package/templates/angular-bridge/src/lib/provide-bridge.ts +98 -0
  51. package/templates/angular-bridge/src/lib/utils/iri.ts +6 -0
  52. package/templates/angular-bridge/src/lib/utils/request-url.ts +31 -0
  53. package/templates/{_lib_template → angular-bridge}/src/public-api.ts +2 -0
  54. package/templates/{_lib_template → angular-bridge}/tsconfig.lib.json +13 -2
  55. package/templates/_lib_template/src/lib/bridge/rest/api-platform.adapter.ts +0 -84
  56. package/templates/_lib_template/src/lib/bridge/rest/http-request.options.ts +0 -21
  57. package/templates/_lib_template/src/lib/bridge/sse/mercure-url.builder.ts +0 -17
  58. package/templates/_lib_template/src/lib/bridge/sse/mercure.adapter.ts +0 -806
  59. package/templates/_lib_template/src/lib/facades/facade.interface.ts +0 -31
  60. package/templates/_lib_template/src/lib/interceptors/bridge-defaults.interceptor.ts +0 -53
  61. package/templates/_lib_template/src/lib/interceptors/content-type.interceptor.ts +0 -49
  62. package/templates/_lib_template/src/lib/interceptors/singleflight.interceptor.ts +0 -55
  63. package/templates/_lib_template/src/lib/provide-bridge.ts +0 -186
  64. package/tools/build.js +0 -1
  65. package/tools/dev.js +0 -1
  66. package/tools/domain/paths.js +0 -10
  67. package/tools/generate.js +0 -1
  68. package/tools/generator/models/oas-contract.js +0 -159
  69. package/tools/generator/models/openapi-to-models.js +0 -240
  70. package/tools/generator/models/type-resolver.js +0 -107
  71. package/tools/generator/models/utils.js +0 -62
  72. package/tools/infra/dev-support.js +0 -189
  73. package/tools/infra/generate/bridge.js +0 -212
  74. package/tools/infra/spec-loader.js +0 -59
  75. package/tools/infra/standalone-workspace.js +0 -162
  76. /package/{tools → src}/domain/lib-name.js +0 -0
  77. /package/{tools/generator → src/generators}/models/handlebars.js +0 -0
  78. /package/{tools/generator → src/generators}/models/templates/index.hbs +0 -0
  79. /package/{tools/generator → src/generators}/models/templates/model.hbs +0 -0
  80. /package/{tools/infra → src/infrastructure}/json.js +0 -0
  81. /package/{tools/infra → src/infrastructure}/logger.js +0 -0
  82. /package/templates/{_lib_template → angular-bridge}/ng-package.json +0 -0
  83. /package/templates/{_lib_template → angular-bridge}/package.json +0 -0
  84. /package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/eventsource-wrapper.ts +0 -0
  85. /package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/mercure-topic.mapper.ts +0 -0
  86. /package/templates/{_lib_template → angular-bridge}/src/lib/bridge.types.ts +0 -0
  87. /package/templates/{_lib_template → angular-bridge}/src/lib/interceptors/bridge-debug.interceptor.ts +0 -0
  88. /package/templates/{_lib_template → angular-bridge}/src/lib/tokens.ts +0 -0
  89. /package/templates/{_lib_template → angular-bridge}/src/lib/utils/url.ts +0 -0
  90. /package/templates/{_lib_template → angular-bridge}/src/models/index.ts +0 -0
  91. /package/templates/{_lib_template → angular-bridge}/tsconfig.lib.prod.json +0 -0
package/README.md CHANGED
@@ -59,10 +59,27 @@ Sorties :
59
59
  ## Notes importantes
60
60
 
61
61
  - fallback automatique `.../api/docs.json` -> `.../api/docs.jsonopenapi` si nécessaire
62
- - mode contract-driven : uniquement les schémas atteignables via `paths` pour les formats sélectionnés
62
+ - mode contract-driven : uniquement les schémas atteignables via `paths` et `webhooks` pour les formats sélectionnés
63
63
  - modèles `*jsonMergePatch*` non générés (`PATCH` typé `Partial<T>`)
64
64
  - Meridiane ne publie pas : publication via `npm publish` côté CI
65
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) :
70
+
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.
77
+
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.
82
+
66
83
  ## Spécifique à ce repo
67
84
 
68
- Dans ce monorepo, `meridiane dev` peut être exécuté sans `packageName` et cible alors `@obsidiane/bridge-sandbox` (app `apps/sandbox`).
85
+ Dans ce monorepo, `meridiane dev` peut être exécuté sans `packageName` et cible alors `@obsidiane/bridge-sandbox` (app `playground/angular-app`).
package/cli.js CHANGED
@@ -2,10 +2,10 @@
2
2
  import process from 'node:process';
3
3
  import { Command } from 'commander';
4
4
 
5
- import { runDev } from './tools/dev.js';
6
- import { runBuild } from './tools/build.js';
7
- import { runGenerate } from './tools/generate.js';
8
- import { createLogger } from './tools/infra/logger.js';
5
+ import { runBuild } from './src/application/run-build.js';
6
+ import { runDev } from './src/application/run-dev.js';
7
+ import { runGenerate } from './src/application/run-generate.js';
8
+ import { createLogger } from './src/infrastructure/logger.js';
9
9
 
10
10
  const program = new Command();
11
11
 
@@ -30,6 +30,20 @@ function commonOptions(cmd) {
30
30
  .option('--debug', 'Enable debug logs');
31
31
  }
32
32
 
33
+ function action(run) {
34
+ return async (packageName, opts) => {
35
+ const log = createLogger({debug: !!opts?.debug});
36
+
37
+ try {
38
+ const exitCode = await run(packageName, opts);
39
+ if (typeof exitCode === 'number' && exitCode !== 0) process.exit(exitCode);
40
+ } catch (error) {
41
+ log.error(error);
42
+ process.exit(typeof error?.exitCode === 'number' ? error.exitCode : 1);
43
+ }
44
+ };
45
+ }
46
+
33
47
  commonOptions(
34
48
  program
35
49
  .command('dev')
@@ -37,17 +51,7 @@ commonOptions(
37
51
  '[packageName]',
38
52
  'NPM package name of the generated bridge (e.g. @acme/backend-bridge). Optional in this repo (defaults to @obsidiane/bridge-sandbox).'
39
53
  )
40
- .action(async (packageName, opts) => {
41
- const log = createLogger({ debug: !!opts?.debug });
42
- try {
43
- const exitCode = await runDev(packageName, opts);
44
- if (typeof exitCode === 'number' && exitCode !== 0) process.exit(exitCode);
45
- } catch (err) {
46
- log.error(err);
47
- const exitCode = typeof err?.exitCode === 'number' ? err.exitCode : 1;
48
- process.exit(exitCode);
49
- }
50
- })
54
+ .action(action(runDev))
51
55
  );
52
56
 
53
57
  commonOptions(
@@ -55,17 +59,7 @@ commonOptions(
55
59
  .command('build')
56
60
  .argument('<packageName>', 'NPM package name of the generated bridge (e.g. @acme/backend-bridge)')
57
61
  .option('--version <semver>', 'Version to write in the generated bridge package.json (CI/CD)')
58
- .action(async (packageName, opts) => {
59
- const log = createLogger({ debug: !!opts?.debug });
60
- try {
61
- const exitCode = await runBuild(packageName, opts);
62
- if (typeof exitCode === 'number' && exitCode !== 0) process.exit(exitCode);
63
- } catch (err) {
64
- log.error(err);
65
- const exitCode = typeof err?.exitCode === 'number' ? err.exitCode : 1;
66
- process.exit(exitCode);
67
- }
68
- })
62
+ .action(action(runBuild))
69
63
  );
70
64
 
71
65
  commonOptions(
@@ -74,17 +68,7 @@ commonOptions(
74
68
  .argument('<packageName>', 'NPM package name of the generated bridge (e.g. @acme/backend-bridge)')
75
69
  .option('--version <semver>', 'Version to write in the generated bridge package.json')
76
70
  .option('--out <dir>', 'Output directory (default: projects/<libName>)')
77
- .action(async (packageName, opts) => {
78
- const log = createLogger({ debug: !!opts?.debug });
79
- try {
80
- const exitCode = await runGenerate(packageName, opts);
81
- if (typeof exitCode === 'number' && exitCode !== 0) process.exit(exitCode);
82
- } catch (err) {
83
- log.error(err);
84
- const exitCode = typeof err?.exitCode === 'number' ? err.exitCode : 1;
85
- process.exit(exitCode);
86
- }
87
- })
71
+ .action(action(runGenerate))
88
72
  );
89
73
 
90
74
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@obsidiane/meridiane",
3
- "version": "2.0.10",
3
+ "version": "2.0.12",
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",
7
- "url": "https://github.com/obsidiane-lab/meridiane"
7
+ "url": "git+https://github.com/obsidiane-lab/meridiane.git"
8
8
  },
9
9
  "type": "module",
10
10
  "publishConfig": {
@@ -18,16 +18,23 @@
18
18
  },
19
19
  "files": [
20
20
  "cli.js",
21
- "tools/**",
21
+ "src/**",
22
22
  "templates/**",
23
23
  "README.md"
24
24
  ],
25
25
  "engines": {
26
- "node": ">=20.19"
26
+ "node": ">=24.0.0"
27
27
  },
28
28
  "dependencies": {
29
+ "@angular/common": "20.3.29",
30
+ "@angular/compiler": "20.3.29",
31
+ "@angular/compiler-cli": "20.3.29",
32
+ "@angular/core": "20.3.29",
29
33
  "commander": "^14.0.3",
30
- "handlebars": "^4.7.8",
31
- "tslib": "^2.3.0"
34
+ "handlebars": "^4.7.9",
35
+ "ng-packagr": "20.3.2",
36
+ "rxjs": "7.8.2",
37
+ "tslib": "^2.8.1",
38
+ "typescript": "5.9.3"
32
39
  }
33
40
  }
package/src/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # Sources internes du générateur
2
+
3
+ - `application/` orchestre les workflows du CLI ;
4
+ - `domain/` contient les règles et la normalisation des options ;
5
+ - `infrastructure/` gère fichiers, processus, specs, workspaces et installation locale ;
6
+ - `generators/bridge/` écrit les fichiers du runtime Angular ;
7
+ - `generators/models/` transforme OpenAPI et écrit les modèles TypeScript.
8
+
9
+ Le runtime Angular généré se trouve dans `../templates/angular-bridge/`.
@@ -1,11 +1,13 @@
1
1
  import process from 'node:process';
2
2
  import path from 'node:path';
3
3
  import fs from 'node:fs/promises';
4
+ import { fileURLToPath } from 'node:url';
4
5
 
5
- import { readOpenApiSpec } from '../infra/spec-loader.js';
6
- import { generateBridgeWorkspace } from '../infra/generate/bridge.js';
7
- import { ensureStandaloneWorkspace, ensureToolchainInstalled } from '../infra/standalone-workspace.js';
8
- import { ExitCodeError, runCommand } from '../infra/exec.js';
6
+ import { generateBridgeWorkspace } from './generate-bridge.js';
7
+ import { readOpenApiSpec } from '../infrastructure/spec-loader.js';
8
+ import { ExitCodeError, runCommand } from '../infrastructure/exec.js';
9
+
10
+ const ngPackagrPath = fileURLToPath(import.meta.resolve('ng-packagr/src/cli/main.js'));
9
11
 
10
12
  export async function runBridgeWorkflow({
11
13
  repoRoot,
@@ -25,23 +27,17 @@ export async function runBridgeWorkflow({
25
27
  packRelativeTo,
26
28
  projectReadmePath,
27
29
  }) {
28
- const { workspaceRoot, distRoot, npmCacheDir, requireFromWs } = await ensureStandaloneWorkspace({ repoRoot, log });
29
- log?.info?.(`workspace: ${workspaceRoot}`);
30
-
31
- let resolvedSpec = spec;
32
- if (!noModels) {
33
- if (!resolvedSpec) {
34
- if (!specSource) throw new Error('Missing --spec (url or JSON file path)');
35
- log?.step?.('lecture de la spec OpenAPI');
36
- resolvedSpec = await readOpenApiSpec(specSource);
37
- log?.success?.('spec chargée');
38
- }
39
- }
30
+ const distRoot = path.join(repoRoot, 'dist');
31
+ 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}`);
40
36
 
41
- await ensureToolchainInstalled({ workspaceRoot, requireFromWs, npmCacheDir, log });
37
+ const resolvedSpec = await loadSpecIfNeeded({ noModels, spec, specSource, log });
42
38
 
43
39
  await generateBridgeWorkspace({
44
- cwd: workspaceRoot,
40
+ cwd: buildRoot,
45
41
  libName,
46
42
  packageName,
47
43
  version,
@@ -57,13 +53,13 @@ export async function runBridgeWorkflow({
57
53
  projectReadmePath,
58
54
  });
59
55
 
60
- const ngPackageJsonPath = path.join(workspaceRoot, 'projects', libName, 'ng-package.json');
61
- const tsconfigAbsPath = path.join(workspaceRoot, 'projects', libName, tsconfigPath);
56
+ const projectDir = path.join(buildRoot, 'projects', libName);
57
+ const ngPackageJsonPath = path.join(projectDir, 'ng-package.json');
58
+ const tsconfigAbsPath = path.join(projectDir, tsconfigPath);
62
59
 
63
60
  log?.step?.('ng-packagr');
64
- const ngPackagrPath = requireFromWs.resolve('ng-packagr/src/cli/main.js');
65
61
  const packagrCode = await runCommand(process.execPath, [ngPackagrPath, '-p', ngPackageJsonPath, '-c', tsconfigAbsPath], {
66
- cwd: workspaceRoot,
62
+ cwd: buildRoot,
67
63
  });
68
64
  if (packagrCode !== 0) throw new ExitCodeError(`ng-packagr failed (exit ${packagrCode})`, packagrCode);
69
65
 
@@ -83,7 +79,17 @@ export async function runBridgeWorkflow({
83
79
  });
84
80
  if (packCode !== 0) throw new ExitCodeError(`npm pack failed (exit ${packCode})`, packCode);
85
81
 
86
- return { workspaceRoot, distRoot, distDir, npmCacheDir };
82
+ return { distDir };
83
+ }
84
+
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;
87
93
  }
88
94
 
89
95
  async function cleanupPackedTarballs(distDir) {
@@ -0,0 +1,50 @@
1
+ import path from 'node:path';
2
+
3
+ import { configureBridgeOutput, writeBridgeFiles } from '../generators/bridge/write-bridge-files.js';
4
+ import { writeModelFiles } from '../generators/models/write-model-files.js';
5
+
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);
25
+
26
+ log?.step?.(`génération de la librairie (${path.relative(cwd, projectDir)})`);
27
+ await writeBridgeFiles({
28
+ projectDir,
29
+ libName,
30
+ packageName,
31
+ version,
32
+ projectReadmePath,
33
+ debug,
34
+ log,
35
+ });
36
+
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
+ }
44
+
45
+ if (!spec) throw new Error('Internal error: spec is required when noModels=false');
46
+
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');
50
+ }
@@ -2,7 +2,7 @@ import process from 'node:process';
2
2
  import path from 'node:path';
3
3
 
4
4
  import { normalizeBuildOptions } from '../domain/bridge-options.js';
5
- import { createLogger } from '../infra/logger.js';
5
+ import { createLogger } from '../infrastructure/logger.js';
6
6
  import { runBridgeWorkflow } from './bridge-workflow.js';
7
7
 
8
8
  export async function runBuild(packageName, opts) {
@@ -2,17 +2,37 @@ import process from 'node:process';
2
2
  import path from 'node:path';
3
3
 
4
4
  import { normalizeDevOptions } from '../domain/bridge-options.js';
5
- import { createLogger } from '../infra/logger.js';
6
- import {
7
- findLatestTgz,
8
- installPackageFromDist,
9
- resolveDevAppRoot,
10
- resolveSandboxAppRoot,
11
- resolveSpecSource,
12
- } from '../infra/dev-support.js';
13
- import { readOpenApiSpec } from '../infra/spec-loader.js';
5
+ import { createLogger } from '../infrastructure/logger.js';
6
+ import {resolveDevAppRoot, resolveSandboxAppRoot, resolveSpecSource} from '../infrastructure/dev-project.js';
7
+ import {installPackageFromDist} from '../infrastructure/local-package-install.js';
8
+ import { readOpenApiSpec } from '../infrastructure/spec-loader.js';
14
9
  import { runBridgeWorkflow } from './bridge-workflow.js';
15
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
+ }
35
+
16
36
  export async function runDev(packageName, opts) {
17
37
  const startCwd = process.cwd();
18
38
  const config = normalizeDevOptions(packageName, opts);
@@ -20,32 +40,7 @@ export async function runDev(packageName, opts) {
20
40
 
21
41
  const appRoot = config.useSandboxDefaults ? await resolveSandboxAppRoot(startCwd) : await resolveDevAppRoot(startCwd);
22
42
  const projectReadmePath = path.join(appRoot, 'README.md');
23
-
24
- const specSource = config.noModels
25
- ? undefined
26
- : await resolveSpecSource({
27
- useSandboxDefaults: config.useSandboxDefaults,
28
- appRoot,
29
- specArg: opts.spec,
30
- });
31
- let noModels = config.noModels;
32
- let spec;
33
- if (!noModels) {
34
- if (!specSource) throw new Error('Missing --spec (url or JSON file path)');
35
- try {
36
- spec = await readOpenApiSpec(specSource);
37
- } catch (err) {
38
- if (config.useSandboxDefaults && opts.spec == null) {
39
- noModels = true;
40
- log.warn(
41
- "spec OpenAPI indisponible (backend non démarré ?) → génération des models ignorée. " +
42
- 'Démarrez le backend ou passez `--spec ./openapi.json` (ou `--no-models`).'
43
- );
44
- } else {
45
- throw err;
46
- }
47
- }
48
- }
43
+ const {noModels, specSource, spec} = await loadDevSpec({config, opts, appRoot, log});
49
44
 
50
45
  log.title('dev — build standalone + install local');
51
46
  log.info(`app: ${appRoot}`);
@@ -74,7 +69,6 @@ export async function runDev(packageName, opts) {
74
69
  projectReadmePath,
75
70
  });
76
71
 
77
- await findLatestTgz(distDir);
78
72
  await installPackageFromDist({ packageName: config.packageName, distDir, appRoot, log });
79
73
 
80
74
  log.success('bridge installé (node_modules) ✅');
@@ -2,9 +2,9 @@ import process from 'node:process';
2
2
  import path from 'node:path';
3
3
 
4
4
  import { normalizeGenerateOptions } from '../domain/bridge-options.js';
5
- import { createLogger } from '../infra/logger.js';
6
- import { readOpenApiSpec } from '../infra/spec-loader.js';
7
- import { generateBridgeWorkspace } from '../infra/generate/bridge.js';
5
+ import { generateBridgeWorkspace } from './generate-bridge.js';
6
+ import { createLogger } from '../infrastructure/logger.js';
7
+ import { readOpenApiSpec } from '../infrastructure/spec-loader.js';
8
8
 
9
9
  function resolveProjectDir({ cwd, libName, outDir }) {
10
10
  if (outDir && outDir.length > 0) return path.resolve(cwd, outDir);
@@ -1,5 +1,5 @@
1
1
  import { deriveLibName } from './lib-name.js';
2
- import { splitListArgs } from './paths.js';
2
+ import {splitListArgs} from './list-values.js';
3
3
 
4
4
  const DEFAULT_FORMATS = ['application/ld+json'];
5
5
  const SANDBOX_PACKAGE_NAME = '@obsidiane/bridge-sandbox';
@@ -23,13 +23,12 @@ function normalizeVersion(version) {
23
23
  return raw || '0.0.0';
24
24
  }
25
25
 
26
- export function normalizeBuildOptions(packageName, opts) {
26
+ function normalizeNamedOptions(packageName, opts) {
27
27
  const common = normalizeCommonOptions(opts);
28
28
  const normalizedPackageName = String(packageName || '').trim();
29
29
  if (!normalizedPackageName) throw new Error('Missing packageName');
30
30
 
31
31
  return {
32
- mode: 'build',
33
32
  packageName: normalizedPackageName,
34
33
  libName: deriveLibName(normalizedPackageName),
35
34
  version: normalizeVersion(opts?.version),
@@ -42,25 +41,14 @@ export function normalizeBuildOptions(packageName, opts) {
42
41
  };
43
42
  }
44
43
 
45
- export function normalizeGenerateOptions(packageName, opts) {
46
- const common = normalizeCommonOptions(opts);
47
- const normalizedPackageName = String(packageName || '').trim();
48
- if (!normalizedPackageName) throw new Error('Missing packageName');
49
-
50
- const outDir = typeof opts?.out === 'string' ? opts.out.trim() : '';
44
+ export function normalizeBuildOptions(packageName, opts) {
45
+ return normalizeNamedOptions(packageName, opts);
46
+ }
51
47
 
48
+ export function normalizeGenerateOptions(packageName, opts) {
52
49
  return {
53
- mode: 'generate',
54
- packageName: normalizedPackageName,
55
- libName: deriveLibName(normalizedPackageName),
56
- version: normalizeVersion(opts?.version),
57
- requiredMode: 'spec',
58
- formats: normalizeFormats(common.formats),
59
- include: common.include,
60
- exclude: common.exclude,
61
- noModels: common.noModels,
62
- debug: common.debug,
63
- outDir,
50
+ ...normalizeNamedOptions(packageName, opts),
51
+ outDir: typeof opts?.out === 'string' ? opts.out.trim() : '',
64
52
  };
65
53
  }
66
54
 
@@ -70,7 +58,6 @@ export function normalizeDevOptions(packageName, opts) {
70
58
  const effectivePackageName = String(packageName ?? SANDBOX_PACKAGE_NAME).trim();
71
59
 
72
60
  return {
73
- mode: 'dev',
74
61
  useSandboxDefaults,
75
62
  packageName: effectivePackageName,
76
63
  libName: deriveLibName(effectivePackageName),
@@ -0,0 +1,6 @@
1
+ export function splitListArgs(values) {
2
+ return (Array.isArray(values) ? values : [])
3
+ .flatMap((value) => String(value).split(','))
4
+ .map((value) => value.trim())
5
+ .filter(Boolean);
6
+ }
@@ -0,0 +1,100 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { readJson, writeJsonIfChanged } from '../../infrastructure/json.js';
6
+
7
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
8
+ const packageRoot = path.resolve(currentDir, '../../..');
9
+ const bridgeTemplateDir = path.join(packageRoot, 'templates', 'angular-bridge');
10
+
11
+ export async function writeBridgeFiles({
12
+ projectDir,
13
+ libName,
14
+ packageName,
15
+ version,
16
+ projectReadmePath,
17
+ debug,
18
+ log,
19
+ }) {
20
+ if (debug) {
21
+ log?.debug?.('[meridiane] generate lib', {
22
+ libName,
23
+ packageName,
24
+ version,
25
+ targetDir: projectDir,
26
+ });
27
+ }
28
+
29
+ await fs.rm(projectDir, { recursive: true, force: true });
30
+ await fs.mkdir(projectDir, { recursive: true });
31
+ await fs.cp(bridgeTemplateDir, projectDir, { recursive: true });
32
+
33
+ await replacePlaceholders(projectDir, {
34
+ '__LIB_NAME__': libName,
35
+ '__PACKAGE_NAME__': packageName,
36
+ });
37
+
38
+ const packageJsonPath = path.join(projectDir, 'package.json');
39
+ const packageJson = await readJson(packageJsonPath);
40
+ packageJson.name = packageName;
41
+ packageJson.version = version;
42
+ await writeJsonIfChanged(packageJsonPath, packageJson);
43
+
44
+ await appendProjectReadme(path.join(projectDir, 'README.md'), projectReadmePath, debug, log);
45
+ }
46
+
47
+ export async function configureBridgeOutput({ projectDir, distRoot, libName, log }) {
48
+ const ngPackagePath = path.join(projectDir, 'ng-package.json');
49
+ const ngPackage = await readJson(ngPackagePath);
50
+ const outputDir = path.join(distRoot, libName);
51
+
52
+ ngPackage.dest = path.relative(projectDir, outputDir).split(path.sep).join('/');
53
+ await writeJsonIfChanged(ngPackagePath, ngPackage);
54
+ log?.debug?.('ng-package.json dest overridden', { dest: ngPackage.dest });
55
+ }
56
+
57
+ async function replacePlaceholders(directory, placeholders) {
58
+ const entries = await fs.readdir(directory, { withFileTypes: true });
59
+
60
+ for (const entry of entries) {
61
+ const filePath = path.join(directory, entry.name);
62
+ if (entry.isDirectory()) {
63
+ await replacePlaceholders(filePath, placeholders);
64
+ continue;
65
+ }
66
+ if (!entry.isFile()) continue;
67
+
68
+ const content = await fs.readFile(filePath, 'utf8');
69
+ const rendered = Object.entries(placeholders)
70
+ .reduce((value, [placeholder, replacement]) => value.replaceAll(placeholder, replacement), content);
71
+ if (rendered !== content) await fs.writeFile(filePath, rendered);
72
+ }
73
+ }
74
+
75
+ async function appendProjectReadme(targetPath, sourcePath, debug, log) {
76
+ if (!sourcePath) return;
77
+
78
+ const projectReadme = await readText(sourcePath);
79
+ if (!projectReadme?.trim()) return;
80
+
81
+ const bridgeReadme = await readText(targetPath);
82
+ if (!bridgeReadme?.trim()) {
83
+ await fs.writeFile(targetPath, projectReadme);
84
+ if (debug) log?.debug?.('[meridiane] project README copied', { source: sourcePath });
85
+ return;
86
+ }
87
+
88
+ const combinedReadme =
89
+ `${bridgeReadme.trimEnd()}\n\n---\n\n## Documentation du projet\n\n${projectReadme.trimStart()}`;
90
+ await fs.writeFile(targetPath, combinedReadme);
91
+ if (debug) log?.debug?.('[meridiane] project README appended', { source: sourcePath });
92
+ }
93
+
94
+ async function readText(filePath) {
95
+ try {
96
+ return await fs.readFile(filePath, 'utf8');
97
+ } catch {
98
+ return undefined;
99
+ }
100
+ }