@obsidiane/meridiane 2.0.3 → 2.0.4

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 CHANGED
@@ -23,12 +23,17 @@ npm i -g @obsidiane/meridiane
23
23
  # (build standalone + install local dans node_modules)
24
24
  npx meridiane dev <packageName> --spec <url|file> [--formats <mimeTypes>]... [--include <substr>]... [--exclude <substr>]... [--no-models] [--debug]
25
25
 
26
+ # Monorepo: génère uniquement les fichiers (pas de npm pack)
27
+ npx meridiane generate <packageName> --spec <url|file> [--formats <mimeTypes>]... [--include <substr>]... [--exclude <substr>]... [--no-models] [--out <dir>] [--debug]
28
+
26
29
  # CI/CD: génère + build Angular + npm pack (artefact prêt à publier)
27
- npx meridiane build <packageName> --version <semver> --spec <url|file> [--formats <mimeTypes>]... [--include <substr>]... [--exclude <substr>]... [--no-models] [--debug]
30
+ npx meridiane build <packageName> [--version <semver>] --spec <url|file> [--formats <mimeTypes>]... [--include <substr>]... [--exclude <substr>]... [--no-models] [--debug]
28
31
  ```
29
32
 
30
33
  Options :
31
34
  - `--debug` : active des logs supplémentaires (CLI).
35
+ - `--version` : pour `build` et `generate` ; par défaut `0.0.0` si omis (déconseillé pour publier).
36
+ - `--out` : uniquement pour `generate` ; répertoire de sortie (défaut `projects/<libName>`).
32
37
  - `--formats` : peut être répété ou fourni en liste séparée par virgules :
33
38
  - `--formats application/ld+json`
34
39
  - `--formats application/ld+json,application/json`
@@ -40,6 +45,7 @@ Bonnes pratiques :
40
45
  - pas de modèles `*.jsonMergePatch` (PATCH = `Partial<...>`)
41
46
  - noms normalisés (pas de suffixe `.jsonld`)
42
47
  - utiliser `--no-models` si vous voulez uniquement le runtime (pas besoin de `--spec`)
48
+ - le `README.md` du projet (si présent à la racine) est ajouté à la fin du README du bridge
43
49
  - laisser le registry à la CI (`.npmrc`, variables d’environnement, `npm publish --registry …`)
44
50
 
45
51
  Note (repo Meridiane) :
@@ -53,3 +59,11 @@ npm publish dist/backend-bridge
53
59
  ```
54
60
 
55
61
  Note : Meridiane installe le toolchain (`ng-packagr`, `@angular/*`, …) dans `dist/.meridiane-workspace` si nécessaire.
62
+
63
+ ## Structure interne (mainteneurs)
64
+
65
+ - `tools/app/` : orchestration des commandes (use-cases).
66
+ - `tools/domain/` : logique pure (options, naming).
67
+ - `tools/infra/` : I/O (spec, workspace, exec, logs) + génération.
68
+ - `tools/generator/` : génération des modèles TypeScript.
69
+ - `templates/_lib_template/` : runtime Angular du bridge.
package/cli.js CHANGED
@@ -4,7 +4,8 @@ import { Command } from 'commander';
4
4
 
5
5
  import { runDev } from './tools/dev.js';
6
6
  import { runBuild } from './tools/build.js';
7
- import { createLogger } from './tools/core/logger.js';
7
+ import { runGenerate } from './tools/generate.js';
8
+ import { createLogger } from './tools/infra/logger.js';
8
9
 
9
10
  const program = new Command();
10
11
 
@@ -39,10 +40,12 @@ commonOptions(
39
40
  .action(async (packageName, opts) => {
40
41
  const log = createLogger({ debug: !!opts?.debug });
41
42
  try {
42
- await runDev(packageName, opts);
43
+ const exitCode = await runDev(packageName, opts);
44
+ if (typeof exitCode === 'number' && exitCode !== 0) process.exit(exitCode);
43
45
  } catch (err) {
44
46
  log.error(err);
45
- process.exit(1);
47
+ const exitCode = typeof err?.exitCode === 'number' ? err.exitCode : 1;
48
+ process.exit(exitCode);
46
49
  }
47
50
  })
48
51
  );
@@ -51,14 +54,35 @@ commonOptions(
51
54
  program
52
55
  .command('build')
53
56
  .argument('<packageName>', 'NPM package name of the generated bridge (e.g. @acme/backend-bridge)')
54
- .requiredOption('--version <semver>', 'Version to write in the generated bridge package.json (CI/CD)')
57
+ .option('--version <semver>', 'Version to write in the generated bridge package.json (CI/CD)')
55
58
  .action(async (packageName, opts) => {
56
59
  const log = createLogger({ debug: !!opts?.debug });
57
60
  try {
58
- await runBuild(packageName, opts);
61
+ const exitCode = await runBuild(packageName, opts);
62
+ if (typeof exitCode === 'number' && exitCode !== 0) process.exit(exitCode);
59
63
  } catch (err) {
60
64
  log.error(err);
61
- process.exit(1);
65
+ const exitCode = typeof err?.exitCode === 'number' ? err.exitCode : 1;
66
+ process.exit(exitCode);
67
+ }
68
+ })
69
+ );
70
+
71
+ commonOptions(
72
+ program
73
+ .command('generate')
74
+ .argument('<packageName>', 'NPM package name of the generated bridge (e.g. @acme/backend-bridge)')
75
+ .option('--version <semver>', 'Version to write in the generated bridge package.json')
76
+ .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);
62
86
  }
63
87
  })
64
88
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@obsidiane/meridiane",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
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",
@@ -1,6 +1,7 @@
1
1
  import {HttpClient} from '@angular/common/http';
2
2
  import {Observable} from 'rxjs';
3
3
  import {toHttpParams} from './query-builder';
4
+ import {buildHttpRequestOptions} from './http-request.options';
4
5
  import {
5
6
  Collection,
6
7
  HttpCallOptions,
@@ -68,29 +69,10 @@ export class ApiPlatformRestRepository<T extends Item> implements ResourceReposi
68
69
 
69
70
  request$<R = unknown, B = unknown>(req: HttpRequestConfig<B>): Observable<R> {
70
71
  // Low-level escape hatch for non-standard endpoints (custom controllers, uploads, etc.).
71
- const {
72
- method,
73
- url,
74
- query,
75
- body,
76
- headers,
77
- responseType,
78
- withCredentials,
79
- options = {},
80
- } = req;
72
+ const {method, url} = req;
81
73
 
82
74
  const targetUrl = this.resolveUrl(url ?? this.resourcePath);
83
- const mergedOptions: Record<string, unknown> = {...options};
84
-
85
- mergedOptions['responseType'] = (responseType ?? (mergedOptions['responseType'] as any) ?? 'json') as any;
86
-
87
- mergedOptions['withCredentials'] = withCredentials ?? (mergedOptions['withCredentials'] as any) ?? this.withCredentialsDefault;
88
-
89
- if (headers) mergedOptions['headers'] = headers;
90
- if (query) mergedOptions['params'] = toHttpParams(query);
91
- if (body !== undefined) mergedOptions['body'] = body;
92
-
93
- mergedOptions['observe'] = 'body';
75
+ const mergedOptions = buildHttpRequestOptions(req, {withCredentialsDefault: this.withCredentialsDefault});
94
76
  return this.http.request<R>(method, targetUrl, mergedOptions as {observe: 'body'});
95
77
  }
96
78
 
@@ -0,0 +1,21 @@
1
+ import {HttpRequestConfig} from '../../ports/resource-repository.port';
2
+ import {toHttpParams} from './query-builder';
3
+
4
+ export function buildHttpRequestOptions(
5
+ req: HttpRequestConfig,
6
+ {withCredentialsDefault}: {withCredentialsDefault: boolean}
7
+ ): Record<string, unknown> {
8
+ const {query, body, headers, responseType, withCredentials, options = {}} = req;
9
+ const mergedOptions: Record<string, unknown> = {...options};
10
+
11
+ if (headers) mergedOptions['headers'] = headers;
12
+ if (query) mergedOptions['params'] = toHttpParams(query);
13
+ if (body !== undefined) mergedOptions['body'] = body;
14
+
15
+ mergedOptions['responseType'] = (responseType ?? (mergedOptions['responseType'] as any) ?? 'json') as any;
16
+ mergedOptions['withCredentials'] =
17
+ withCredentials ?? (mergedOptions['withCredentials'] as any) ?? withCredentialsDefault;
18
+ mergedOptions['observe'] = 'body';
19
+
20
+ return mergedOptions;
21
+ }
@@ -3,6 +3,7 @@ import {HttpClient} from '@angular/common/http';
3
3
  import {Observable} from 'rxjs';
4
4
  import {filter, map, share} from 'rxjs/operators';
5
5
  import {toHttpParams} from '../bridge/rest/query-builder';
6
+ import {buildHttpRequestOptions} from '../bridge/rest/http-request.options';
6
7
  import {MercureRealtimeAdapter} from '../bridge/sse/mercure.adapter';
7
8
  import {API_BASE_URL, BRIDGE_WITH_CREDENTIALS} from '../tokens';
8
9
  import {AnyQuery, Collection, HttpCallOptions, HttpRequestConfig, Iri, IriRequired, Item} from '../ports/resource-repository.port';
@@ -75,19 +76,10 @@ export class BridgeFacade {
75
76
  }
76
77
 
77
78
  request$<R = unknown, B = unknown>(req: HttpRequestConfig<B>): Observable<R> {
78
- const {method, url, query, body, headers, responseType, withCredentials, options = {}} = req;
79
+ const {method, url} = req;
79
80
  const targetUrl = this.resolveUrl(url);
80
81
 
81
- const mergedOptions: Record<string, unknown> = {...options};
82
- if (headers) mergedOptions['headers'] = headers;
83
- if (query) mergedOptions['params'] = toHttpParams(query);
84
- if (body !== undefined) mergedOptions['body'] = body;
85
-
86
- mergedOptions['responseType'] = (responseType ?? (mergedOptions['responseType'] as any) ?? 'json') as any;
87
- mergedOptions['withCredentials'] =
88
- withCredentials ?? (mergedOptions['withCredentials'] as any) ?? this.withCredentialsDefault;
89
- mergedOptions['observe'] = 'body';
90
-
82
+ const mergedOptions = buildHttpRequestOptions(req, {withCredentialsDefault: this.withCredentialsDefault});
91
83
  return this.http.request<R>(method, targetUrl, mergedOptions as {observe: 'body'});
92
84
  }
93
85
 
@@ -0,0 +1,85 @@
1
+ import process from 'node:process';
2
+ import path from 'node:path';
3
+
4
+ import { readOpenApiSpec } from '../infra/spec-loader.js';
5
+ import { generateBridgeWorkspace } from '../infra/generate/bridge.js';
6
+ import { ensureStandaloneWorkspace, ensureToolchainInstalled } from '../infra/standalone-workspace.js';
7
+ import { ExitCodeError, runCommand } from '../infra/exec.js';
8
+
9
+ export async function runBridgeWorkflow({
10
+ repoRoot,
11
+ libName,
12
+ packageName,
13
+ version,
14
+ noModels,
15
+ spec,
16
+ specSource,
17
+ requiredMode,
18
+ formats,
19
+ include,
20
+ exclude,
21
+ debug,
22
+ log,
23
+ tsconfigPath,
24
+ packRelativeTo,
25
+ projectReadmePath,
26
+ }) {
27
+ const { workspaceRoot, distRoot, npmCacheDir, requireFromWs } = await ensureStandaloneWorkspace({ repoRoot, log });
28
+ log?.info?.(`workspace: ${workspaceRoot}`);
29
+
30
+ let resolvedSpec = spec;
31
+ if (!noModels) {
32
+ if (!resolvedSpec) {
33
+ if (!specSource) throw new Error('Missing --spec (url or JSON file path)');
34
+ log?.step?.('lecture de la spec OpenAPI');
35
+ resolvedSpec = await readOpenApiSpec(specSource);
36
+ log?.success?.('spec chargée');
37
+ }
38
+ }
39
+
40
+ await ensureToolchainInstalled({ workspaceRoot, requireFromWs, npmCacheDir, log });
41
+
42
+ await generateBridgeWorkspace({
43
+ cwd: workspaceRoot,
44
+ libName,
45
+ packageName,
46
+ version,
47
+ noModels,
48
+ spec: resolvedSpec,
49
+ requiredMode,
50
+ formats,
51
+ include,
52
+ exclude,
53
+ debug,
54
+ log,
55
+ distRoot,
56
+ projectReadmePath,
57
+ });
58
+
59
+ const ngPackageJsonPath = path.join(workspaceRoot, 'projects', libName, 'ng-package.json');
60
+ const tsconfigAbsPath = path.join(workspaceRoot, 'projects', libName, tsconfigPath);
61
+
62
+ log?.step?.('ng-packagr');
63
+ const ngPackagrPath = requireFromWs.resolve('ng-packagr/src/cli/main.js');
64
+ const packagrCode = await runCommand(process.execPath, [ngPackagrPath, '-p', ngPackageJsonPath, '-c', tsconfigAbsPath], {
65
+ cwd: workspaceRoot,
66
+ });
67
+ if (packagrCode !== 0) throw new ExitCodeError(`ng-packagr failed (exit ${packagrCode})`, packagrCode);
68
+
69
+ const distDir = path.join(distRoot, libName);
70
+ const packRel = packRelativeTo ? path.relative(packRelativeTo, distDir) : distDir;
71
+ log?.step?.(`npm pack (${packRel})`);
72
+ const packCode = await runCommand('npm', ['pack'], {
73
+ cwd: distDir,
74
+ env: {
75
+ ...process.env,
76
+ npm_config_cache: npmCacheDir,
77
+ npm_config_update_notifier: 'false',
78
+ npm_config_fund: 'false',
79
+ npm_config_audit: 'false',
80
+ },
81
+ });
82
+ if (packCode !== 0) throw new ExitCodeError(`npm pack failed (exit ${packCode})`, packCode);
83
+
84
+ return { workspaceRoot, distRoot, distDir, npmCacheDir };
85
+ }
@@ -0,0 +1,41 @@
1
+ import process from 'node:process';
2
+ import path from 'node:path';
3
+
4
+ import { normalizeBuildOptions } from '../domain/bridge-options.js';
5
+ import { createLogger } from '../infra/logger.js';
6
+ import { runBridgeWorkflow } from './bridge-workflow.js';
7
+
8
+ export async function runBuild(packageName, opts) {
9
+ const cwd = process.cwd();
10
+ const config = normalizeBuildOptions(packageName, opts);
11
+ const log = createLogger({ debug: config.debug });
12
+ const projectReadmePath = path.join(cwd, 'README.md');
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}`);
20
+
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)}`);
40
+ return 0;
41
+ }
@@ -0,0 +1,82 @@
1
+ import process from 'node:process';
2
+ import path from 'node:path';
3
+
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';
14
+ import { runBridgeWorkflow } from './bridge-workflow.js';
15
+
16
+ export async function runDev(packageName, opts) {
17
+ const startCwd = process.cwd();
18
+ const config = normalizeDevOptions(packageName, opts);
19
+ const log = createLogger({ debug: config.debug });
20
+
21
+ const appRoot = config.useSandboxDefaults ? await resolveSandboxAppRoot(startCwd) : await resolveDevAppRoot(startCwd);
22
+ 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
+ }
49
+
50
+ log.title('dev — build standalone + install local');
51
+ log.info(`app: ${appRoot}`);
52
+ log.info(`package: ${config.packageName}@${config.version}`);
53
+ log.info(`lib: ${config.libName}`);
54
+ if (config.useSandboxDefaults) log.info('defaults sandbox appliqués');
55
+ if (noModels) log.info('models: désactivés (--no-models)');
56
+ else log.info(`spec: ${specSource} | formats: ${config.formats.join(', ')} | nullableMode: ${config.requiredMode}`);
57
+
58
+ const { distDir } = await runBridgeWorkflow({
59
+ repoRoot: appRoot,
60
+ libName: config.libName,
61
+ packageName: config.packageName,
62
+ version: config.version,
63
+ noModels,
64
+ spec,
65
+ specSource,
66
+ requiredMode: config.requiredMode,
67
+ formats: config.formats,
68
+ include: config.include,
69
+ exclude: config.exclude,
70
+ debug: config.debug,
71
+ log,
72
+ tsconfigPath: 'tsconfig.lib.json',
73
+ packRelativeTo: appRoot,
74
+ projectReadmePath,
75
+ });
76
+
77
+ await findLatestTgz(distDir);
78
+ await installPackageFromDist({ packageName: config.packageName, distDir, appRoot, log });
79
+
80
+ log.success('bridge installé (node_modules) ✅');
81
+ return 0;
82
+ }
@@ -0,0 +1,58 @@
1
+ import process from 'node:process';
2
+ import path from 'node:path';
3
+
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';
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
+ }
13
+
14
+ export async function runGenerate(packageName, opts) {
15
+ const cwd = process.cwd();
16
+ const config = normalizeGenerateOptions(packageName, opts);
17
+ 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
+ }
38
+
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
+ });
55
+
56
+ log.success(`bridge généré: ${path.relative(cwd, projectDir)}`);
57
+ return 0;
58
+ }
package/tools/build.js CHANGED
@@ -1,105 +1 @@
1
- #!/usr/bin/env node
2
- import process from 'node:process';
3
- import { spawn } from 'node:child_process';
4
- import path from 'node:path';
5
-
6
- import { deriveLibName } from './core/lib-name.js';
7
- import { splitListArgs } from './core/paths.js';
8
- import { readOpenApiSpec } from './core/spec.js';
9
- import { createLogger } from './core/logger.js';
10
- import { generateBridgeWorkspace } from './core/generate.js';
11
- import { ensureStandaloneWorkspace, ensureToolchainInstalled } from './core/standalone.js';
12
-
13
- function run(cmd, args, { cwd, env } = {}) {
14
- return new Promise((resolve) => {
15
- const child = spawn(cmd, args, {
16
- stdio: 'inherit',
17
- cwd: cwd ?? process.cwd(),
18
- env: env ?? process.env,
19
- });
20
- child.on('exit', (code) => resolve(code ?? 0));
21
- });
22
- }
23
-
24
- export async function runBuild(packageName, opts) {
25
- const cwd = process.cwd();
26
- const debug = !!opts.debug;
27
- const log = createLogger({ debug });
28
-
29
- const version = opts.version;
30
- if (!version) throw new Error('Missing --version');
31
-
32
- const noModels = opts.models === false;
33
- const formats = splitListArgs(opts.formats);
34
- const include = splitListArgs(opts.include);
35
- const exclude = splitListArgs(opts.exclude);
36
-
37
- const libName = deriveLibName(packageName);
38
- const requiredMode = 'spec';
39
- const effectiveFormats = formats.length > 0 ? formats : ['application/ld+json'];
40
-
41
- // Standalone mode: works from any repo (backend pipeline or otherwise).
42
- log.title('build — standalone');
43
- log.info(`repo: ${cwd}`);
44
- log.info(`package: ${packageName}@${version}`);
45
- log.info(`lib: ${libName}`);
46
- if (noModels) log.info('models: désactivés (--no-models)');
47
- else log.info(`spec: ${opts.spec} | formats: ${effectiveFormats.join(', ')} | nullableMode: ${requiredMode}`);
48
-
49
- const { workspaceRoot, distRoot, npmCacheDir, requireFromWs } = await ensureStandaloneWorkspace({ repoRoot: cwd, log });
50
- log.info(`workspace: ${workspaceRoot}`);
51
-
52
- let spec;
53
- if (!noModels) {
54
- log.step('lecture de la spec OpenAPI');
55
- spec = await readOpenApiSpec(opts.spec);
56
- log.success('spec chargée');
57
- }
58
-
59
- await ensureToolchainInstalled({ workspaceRoot, requireFromWs, npmCacheDir, log });
60
-
61
- await generateBridgeWorkspace({
62
- cwd: workspaceRoot,
63
- libName,
64
- packageName,
65
- version,
66
- noModels,
67
- spec,
68
- requiredMode,
69
- formats: effectiveFormats,
70
- include,
71
- exclude,
72
- debug,
73
- log,
74
- distRoot,
75
- });
76
-
77
- const ngPackageJsonPath = path.join(workspaceRoot, 'projects', libName, 'ng-package.json');
78
- const tsconfigProdPath = path.join(workspaceRoot, 'projects', libName, 'tsconfig.lib.prod.json');
79
-
80
- log.step('ng-packagr');
81
- const ngPackagrPath = requireFromWs.resolve('ng-packagr/src/cli/main.js');
82
- const packagrCode = await run(process.execPath, [ngPackagrPath, '-p', ngPackageJsonPath, '-c', tsconfigProdPath], {
83
- cwd: workspaceRoot,
84
- });
85
- if (packagrCode !== 0) process.exit(packagrCode);
86
-
87
- const distDir = path.join(distRoot, libName);
88
- log.step(`npm pack (${path.relative(cwd, distDir)})`);
89
- const packCode = await run('npm', ['pack'], {
90
- cwd: distDir,
91
- env: {
92
- ...process.env,
93
- npm_config_cache: npmCacheDir,
94
- npm_config_update_notifier: 'false',
95
- npm_config_fund: 'false',
96
- npm_config_audit: 'false',
97
- },
98
- });
99
- if (packCode === 0) log.success(`package prêt: ${path.relative(cwd, distDir)}`);
100
- process.exit(packCode);
101
- }
102
-
103
- if (import.meta.url === `file://${process.argv[1]}`) {
104
- throw new Error('This module is not meant to be executed directly.');
105
- }
1
+ export { runBuild } from './app/run-build.js';
package/tools/dev.js CHANGED
@@ -1,219 +1 @@
1
- #!/usr/bin/env node
2
- import process from 'node:process';
3
- import fs from 'node:fs/promises';
4
- import path from 'node:path';
5
- import { spawn } from 'node:child_process';
6
-
7
- import { deriveLibName } from './core/lib-name.js';
8
- import { splitListArgs } from './core/paths.js';
9
- import { readOpenApiSpec } from './core/spec.js';
10
- import { createLogger } from './core/logger.js';
11
- import { generateBridgeWorkspace } from './core/generate.js';
12
- import { ensureStandaloneWorkspace, ensureToolchainInstalled } from './core/standalone.js';
13
-
14
- function run(cmd, args, { cwd, env } = {}) {
15
- return new Promise((resolve) => {
16
- const child = spawn(cmd, args, {
17
- stdio: 'inherit',
18
- cwd: cwd ?? process.cwd(),
19
- env: env ?? process.env,
20
- });
21
- child.on('exit', (code) => resolve(code ?? 0));
22
- });
23
- }
24
-
25
- async function pathIsFile(p) {
26
- try {
27
- const st = await fs.stat(p);
28
- return st.isFile();
29
- } catch {
30
- return false;
31
- }
32
- }
33
-
34
- async function readPackageJsonName(dir) {
35
- try {
36
- const raw = await fs.readFile(path.join(dir, 'package.json'), 'utf8');
37
- const parsed = JSON.parse(raw);
38
- return typeof parsed?.name === 'string' ? parsed.name : undefined;
39
- } catch {
40
- return undefined;
41
- }
42
- }
43
-
44
- async function resolveDevAppRoot(startCwd) {
45
- if (await pathIsFile(path.join(startCwd, 'package.json'))) return startCwd;
46
- throw new Error('No package.json found. Run `meridiane dev` from your Angular app root.');
47
- }
48
-
49
- async function isMeridianeSandboxApp(appRoot) {
50
- const name = await readPackageJsonName(appRoot);
51
- return name === '@obsidiane/meridiane-sandbox';
52
- }
53
-
54
- async function resolveSandboxAppRoot(startCwd) {
55
- // Allow running from apps/sandbox directly
56
- if (await isMeridianeSandboxApp(startCwd)) return startCwd;
57
-
58
- // Allow running from this repo root (monorepo) and target apps/sandbox
59
- const sandboxApp = path.join(startCwd, 'apps', 'sandbox');
60
- if (await pathIsFile(path.join(sandboxApp, 'package.json')) && (await isMeridianeSandboxApp(sandboxApp))) {
61
- return sandboxApp;
62
- }
63
-
64
- throw new Error('Missing <packageName>. Run from the sandbox app (`apps/sandbox`) or provide a package name.');
65
- }
66
-
67
- async function findLatestTgz(distDir) {
68
- const entries = await fs.readdir(distDir, { withFileTypes: true }).catch(() => []);
69
- const tgzFiles = [];
70
- for (const e of entries) {
71
- if (!e.isFile()) continue;
72
- if (!e.name.endsWith('.tgz')) continue;
73
- const full = path.join(distDir, e.name);
74
- const st = await fs.stat(full);
75
- tgzFiles.push({ full, mtimeMs: st.mtimeMs });
76
- }
77
- tgzFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
78
- return tgzFiles[0]?.full;
79
- }
80
-
81
- function splitPackageName(packageName) {
82
- const s = String(packageName || '').trim();
83
- if (!s) throw new Error('Missing packageName');
84
- if (s.startsWith('@')) {
85
- const [scope, name] = s.split('/');
86
- if (!scope || !name) throw new Error(`Invalid packageName: ${packageName}`);
87
- return { scope, name };
88
- }
89
- return { scope: undefined, name: s };
90
- }
91
-
92
- async function resolveNodeModulesRoot(startDir) {
93
- let cur = startDir;
94
- for (;;) {
95
- const candidate = path.join(cur, 'node_modules');
96
- try {
97
- const st = await fs.stat(candidate);
98
- if (st.isDirectory()) return candidate;
99
- } catch {
100
- // ignore
101
- }
102
- const parent = path.dirname(cur);
103
- if (parent === cur) break;
104
- cur = parent;
105
- }
106
-
107
- const nm = path.join(startDir, 'node_modules');
108
- await fs.mkdir(nm, { recursive: true });
109
- return nm;
110
- }
111
-
112
- async function installPackageFromDist({ packageName, distDir, appRoot, log }) {
113
- const nmRoot = await resolveNodeModulesRoot(appRoot);
114
- const { scope, name } = splitPackageName(packageName);
115
- const pkgDir = scope ? path.join(nmRoot, scope, name) : path.join(nmRoot, name);
116
-
117
- log?.step?.(`installation locale dans node_modules (${scope ? `${scope}/${name}` : name})`);
118
-
119
- await fs.rm(pkgDir, { recursive: true, force: true });
120
- await fs.mkdir(path.dirname(pkgDir), { recursive: true });
121
-
122
- await fs.cp(distDir, pkgDir, {
123
- recursive: true,
124
- filter: (src) => !src.endsWith('.tgz'),
125
- });
126
-
127
- return pkgDir;
128
- }
129
-
130
- export async function runDev(packageName, opts) {
131
- const startCwd = process.cwd();
132
- const debug = !!opts.debug;
133
- const log = createLogger({ debug });
134
-
135
- const useSandboxDefaults = packageName === undefined;
136
- const appRoot = useSandboxDefaults ? await resolveSandboxAppRoot(startCwd) : await resolveDevAppRoot(startCwd);
137
-
138
- const effectivePackageName = packageName ?? '@obsidiane/bridge-sandbox';
139
- const libName = deriveLibName(effectivePackageName);
140
-
141
- const noModels = opts.models === false;
142
- const formats = splitListArgs(opts.formats);
143
- const include = splitListArgs(opts.include);
144
- const exclude = splitListArgs(opts.exclude);
145
-
146
- const version = '0.0.0-dev';
147
- const requiredMode = 'all';
148
- const effectiveFormats = formats.length > 0 ? formats : ['application/ld+json'];
149
- const specSource = useSandboxDefaults ? (opts.spec ?? 'http://localhost:8000/api/docs.json') : opts.spec;
150
-
151
- log.title('dev — build standalone + install local');
152
- log.info(`app: ${appRoot}`);
153
- log.info(`package: ${effectivePackageName}@${version}`);
154
- log.info(`lib: ${libName}`);
155
- if (useSandboxDefaults) log.info('defaults sandbox appliqués');
156
- if (noModels) log.info('models: désactivés (--no-models)');
157
- else log.info(`spec: ${specSource} | formats: ${effectiveFormats.join(', ')} | nullableMode: ${requiredMode}`);
158
-
159
- const { workspaceRoot, distRoot, npmCacheDir, requireFromWs } = await ensureStandaloneWorkspace({ repoRoot: appRoot, log });
160
- log.info(`workspace: ${workspaceRoot}`);
161
-
162
- let spec;
163
- if (!noModels) {
164
- log.step('lecture de la spec OpenAPI');
165
- spec = await readOpenApiSpec(specSource);
166
- log.success('spec chargée');
167
- }
168
-
169
- await ensureToolchainInstalled({ workspaceRoot, requireFromWs, npmCacheDir, log });
170
-
171
- await generateBridgeWorkspace({
172
- cwd: workspaceRoot,
173
- libName,
174
- packageName: effectivePackageName,
175
- version,
176
- noModels,
177
- spec,
178
- requiredMode,
179
- formats: effectiveFormats,
180
- include,
181
- exclude,
182
- debug,
183
- log,
184
- distRoot,
185
- });
186
-
187
- const ngPackageJsonPath = path.join(workspaceRoot, 'projects', libName, 'ng-package.json');
188
- const tsconfigDevPath = path.join(workspaceRoot, 'projects', libName, 'tsconfig.lib.json');
189
-
190
- log.step('ng-packagr');
191
- const ngPackagrPath = requireFromWs.resolve('ng-packagr/src/cli/main.js');
192
- const packagrCode = await run(process.execPath, [ngPackagrPath, '-p', ngPackageJsonPath, '-c', tsconfigDevPath], {
193
- cwd: workspaceRoot,
194
- });
195
- if (packagrCode !== 0) process.exit(packagrCode);
196
-
197
- const distDir = path.join(distRoot, libName);
198
- log.step(`npm pack (${path.relative(appRoot, distDir)})`);
199
- const packCode = await run('npm', ['pack'], {
200
- cwd: distDir,
201
- env: {
202
- ...process.env,
203
- npm_config_cache: npmCacheDir,
204
- npm_config_update_notifier: 'false',
205
- npm_config_fund: 'false',
206
- npm_config_audit: 'false',
207
- },
208
- });
209
- if (packCode !== 0) process.exit(packCode);
210
-
211
- await findLatestTgz(distDir); // keep producing the tgz in dist for inspection/use
212
- await installPackageFromDist({ packageName: effectivePackageName, distDir, appRoot, log });
213
-
214
- log.success('bridge installé (node_modules) ✅');
215
- }
216
-
217
- if (import.meta.url === `file://${process.argv[1]}`) {
218
- throw new Error('This module is not meant to be executed directly.');
219
- }
1
+ export { runDev } from './app/run-dev.js';
@@ -0,0 +1,85 @@
1
+ import { deriveLibName } from './lib-name.js';
2
+ import { splitListArgs } from './paths.js';
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
+ export function normalizeBuildOptions(packageName, opts) {
27
+ const common = normalizeCommonOptions(opts);
28
+ const normalizedPackageName = String(packageName || '').trim();
29
+ if (!normalizedPackageName) throw new Error('Missing packageName');
30
+
31
+ return {
32
+ mode: 'build',
33
+ packageName: normalizedPackageName,
34
+ libName: deriveLibName(normalizedPackageName),
35
+ version: normalizeVersion(opts?.version),
36
+ requiredMode: 'spec',
37
+ formats: normalizeFormats(common.formats),
38
+ include: common.include,
39
+ exclude: common.exclude,
40
+ noModels: common.noModels,
41
+ debug: common.debug,
42
+ };
43
+ }
44
+
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() : '';
51
+
52
+ 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,
64
+ };
65
+ }
66
+
67
+ export function normalizeDevOptions(packageName, opts) {
68
+ const common = normalizeCommonOptions(opts);
69
+ const useSandboxDefaults = packageName === undefined;
70
+ const effectivePackageName = String(packageName ?? SANDBOX_PACKAGE_NAME).trim();
71
+
72
+ return {
73
+ mode: 'dev',
74
+ useSandboxDefaults,
75
+ packageName: effectivePackageName,
76
+ libName: deriveLibName(effectivePackageName),
77
+ version: '0.0.0-dev',
78
+ requiredMode: 'all',
79
+ formats: normalizeFormats(common.formats),
80
+ include: common.include,
81
+ exclude: common.exclude,
82
+ noModels: common.noModels,
83
+ debug: common.debug,
84
+ };
85
+ }
@@ -0,0 +1 @@
1
+ export { runGenerate } from './app/run-generate.js';
@@ -0,0 +1,112 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ async function pathIsFile(p) {
5
+ try {
6
+ const st = await fs.stat(p);
7
+ return st.isFile();
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ async function readPackageJsonName(dir) {
14
+ try {
15
+ const raw = await fs.readFile(path.join(dir, 'package.json'), 'utf8');
16
+ const parsed = JSON.parse(raw);
17
+ return typeof parsed?.name === 'string' ? parsed.name : undefined;
18
+ } catch {
19
+ return undefined;
20
+ }
21
+ }
22
+
23
+ async function isMeridianeSandboxApp(appRoot) {
24
+ const name = await readPackageJsonName(appRoot);
25
+ return name === '@obsidiane/meridiane-sandbox';
26
+ }
27
+
28
+ export async function resolveDevAppRoot(startCwd) {
29
+ if (await pathIsFile(path.join(startCwd, 'package.json'))) return startCwd;
30
+ throw new Error('No package.json found. Run `meridiane dev` from your Angular app root.');
31
+ }
32
+
33
+ export async function resolveSandboxAppRoot(startCwd) {
34
+ if (await isMeridianeSandboxApp(startCwd)) return startCwd;
35
+
36
+ const sandboxApp = path.join(startCwd, 'apps', 'sandbox');
37
+ if (await pathIsFile(path.join(sandboxApp, 'package.json')) && (await isMeridianeSandboxApp(sandboxApp))) {
38
+ return sandboxApp;
39
+ }
40
+
41
+ throw new Error('Missing <packageName>. Run from the sandbox app (`apps/sandbox`) or provide a package name.');
42
+ }
43
+
44
+ export async function resolveSpecSource({ useSandboxDefaults, appRoot, specArg }) {
45
+ if (!useSandboxDefaults) return specArg;
46
+ if (specArg) return specArg;
47
+ const localSpec = path.resolve(appRoot, '..', 'backend', 'var', 'openapi.json');
48
+ return (await pathIsFile(localSpec)) ? localSpec : 'http://localhost:8000/api/docs.json';
49
+ }
50
+
51
+ export async function findLatestTgz(distDir) {
52
+ const entries = await fs.readdir(distDir, { withFileTypes: true }).catch(() => []);
53
+ const tgzFiles = [];
54
+ for (const e of entries) {
55
+ if (!e.isFile()) continue;
56
+ if (!e.name.endsWith('.tgz')) continue;
57
+ const full = path.join(distDir, e.name);
58
+ const st = await fs.stat(full);
59
+ tgzFiles.push({ full, mtimeMs: st.mtimeMs });
60
+ }
61
+ tgzFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
62
+ return tgzFiles[0]?.full;
63
+ }
64
+
65
+ function splitPackageName(packageName) {
66
+ const s = String(packageName || '').trim();
67
+ if (!s) throw new Error('Missing packageName');
68
+ if (s.startsWith('@')) {
69
+ const [scope, name] = s.split('/');
70
+ if (!scope || !name) throw new Error(`Invalid packageName: ${packageName}`);
71
+ return { scope, name };
72
+ }
73
+ return { scope: undefined, name: s };
74
+ }
75
+
76
+ async function resolveNodeModulesRoot(startDir) {
77
+ let cur = startDir;
78
+ for (;;) {
79
+ const candidate = path.join(cur, 'node_modules');
80
+ try {
81
+ const st = await fs.stat(candidate);
82
+ if (st.isDirectory()) return candidate;
83
+ } catch {
84
+ // ignore
85
+ }
86
+ const parent = path.dirname(cur);
87
+ if (parent === cur) break;
88
+ cur = parent;
89
+ }
90
+
91
+ const nm = path.join(startDir, 'node_modules');
92
+ await fs.mkdir(nm, { recursive: true });
93
+ return nm;
94
+ }
95
+
96
+ export async function installPackageFromDist({ packageName, distDir, appRoot, log }) {
97
+ const nmRoot = await resolveNodeModulesRoot(appRoot);
98
+ const { scope, name } = splitPackageName(packageName);
99
+ const pkgDir = scope ? path.join(nmRoot, scope, name) : path.join(nmRoot, name);
100
+
101
+ log?.step?.(`installation locale dans node_modules (${scope ? `${scope}/${name}` : name})`);
102
+
103
+ await fs.rm(pkgDir, { recursive: true, force: true });
104
+ await fs.mkdir(path.dirname(pkgDir), { recursive: true });
105
+
106
+ await fs.cp(distDir, pkgDir, {
107
+ recursive: true,
108
+ filter: (src) => !src.endsWith('.tgz'),
109
+ });
110
+
111
+ return pkgDir;
112
+ }
@@ -0,0 +1,21 @@
1
+ import process from 'node:process';
2
+ import { spawn } from 'node:child_process';
3
+
4
+ export class ExitCodeError extends Error {
5
+ constructor(message, exitCode) {
6
+ super(message);
7
+ this.name = 'ExitCodeError';
8
+ this.exitCode = exitCode;
9
+ }
10
+ }
11
+
12
+ export function runCommand(cmd, args, { cwd, env } = {}) {
13
+ return new Promise((resolve) => {
14
+ const child = spawn(cmd, args, {
15
+ stdio: 'inherit',
16
+ cwd: cwd ?? process.cwd(),
17
+ env: env ?? process.env,
18
+ });
19
+ child.on('exit', (code) => resolve(code ?? 0));
20
+ });
21
+ }
@@ -3,15 +3,44 @@ import {existsSync} from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import {fileURLToPath} from 'node:url';
5
5
 
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';
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);
13
13
 
14
- const pkgRoot = path.resolve(__dirname, '../..'); // packages/meridiane
14
+ const pkgRoot = path.resolve(__dirname, '../../..'); // packages/meridiane
15
+
16
+ async function readTextIfExists(filePath) {
17
+ try {
18
+ return await fs.readFile(filePath, 'utf8');
19
+ } catch {
20
+ return undefined;
21
+ }
22
+ }
23
+
24
+ async function appendProjectReadme({targetReadmePath, projectReadmePath, debug, log}) {
25
+ if (!projectReadmePath) return;
26
+
27
+ const projectReadme = await readTextIfExists(projectReadmePath);
28
+ if (!projectReadme || projectReadme.trim().length === 0) return;
29
+
30
+ const targetReadme = await readTextIfExists(targetReadmePath);
31
+ if (!targetReadme || targetReadme.trim().length === 0) {
32
+ await fs.writeFile(targetReadmePath, projectReadme);
33
+ if (debug) log?.debug?.('[meridiane] project README copied', {source: projectReadmePath});
34
+ return;
35
+ }
36
+
37
+ const combined =
38
+ `${targetReadme.trimEnd()}\n\n---\n\n` +
39
+ `## Documentation du projet\n\n` +
40
+ `${projectReadme.trimStart()}`;
41
+ await fs.writeFile(targetReadmePath, combined);
42
+ if (debug) log?.debug?.('[meridiane] project README appended', {source: projectReadmePath});
43
+ }
15
44
 
16
45
  async function replacePlaceholdersInDir(dir, placeholders) {
17
46
  const entries = await fs.readdir(dir, {withFileTypes: true});
@@ -29,16 +58,18 @@ async function replacePlaceholdersInDir(dir, placeholders) {
29
58
  }
30
59
  }
31
60
 
32
- async function generateLibrary({cwd, libName, packageName, version, debug}) {
61
+ async function generateLibrary({projectDir, libName, packageName, version, debug, log, projectReadmePath}) {
33
62
  const tplDir = path.resolve(pkgRoot, 'templates/_lib_template');
34
- const targetDir = path.resolve(cwd, 'projects', libName);
35
-
36
- if (debug) console.log('[meridiane] generate lib', {
37
- libName,
38
- packageName,
39
- version,
40
- targetDir: path.relative(cwd, targetDir)
41
- });
63
+ const targetDir = projectDir;
64
+
65
+ if (debug) {
66
+ log?.debug?.('[meridiane] generate lib', {
67
+ libName,
68
+ packageName,
69
+ version,
70
+ targetDir
71
+ });
72
+ }
42
73
 
43
74
  await fs.rm(targetDir, {recursive: true, force: true});
44
75
  await fs.mkdir(targetDir, {recursive: true});
@@ -57,21 +88,29 @@ async function generateLibrary({cwd, libName, packageName, version, debug}) {
57
88
  await writeJsonIfChanged(libPackageJsonPath, libPkg);
58
89
  }
59
90
 
60
- // angular.json / workspace patching is intentionally not done here (Meridiane is standalone).
91
+ await appendProjectReadme({
92
+ targetReadmePath: path.join(targetDir, 'README.md'),
93
+ projectReadmePath,
94
+ debug,
95
+ log,
96
+ });
97
+
98
+ // angular.json / workspace patching is intentionally not done here.
61
99
  }
62
100
 
63
- async function generateModels({cwd, libName, spec, requiredMode, formats, include, exclude, debug}) {
64
- const outDir = path.resolve(cwd, 'projects', libName, 'src', 'models');
101
+ async function generateModels({projectDir, spec, requiredMode, formats, include, exclude, debug, log}) {
102
+ const outDir = path.resolve(projectDir, 'src', 'models');
65
103
  const templatesDir = path.resolve(pkgRoot, 'tools', 'generator', 'models', 'templates');
66
104
 
67
- if (debug)
68
- console.log('[meridiane] generate models', {
69
- outDir: path.relative(cwd, outDir),
105
+ if (debug) {
106
+ log?.debug?.('[meridiane] generate models', {
107
+ outDir,
70
108
  requiredMode,
71
109
  formats: Array.isArray(formats) ? formats : [],
72
110
  includeCount: include.length,
73
111
  excludeCount: exclude.length,
74
112
  });
113
+ }
75
114
 
76
115
  await ensureCleanDir(outDir);
77
116
 
@@ -103,7 +142,7 @@ async function generateModels({cwd, libName, spec, requiredMode, formats, includ
103
142
  ctx: {models: models.map((m) => ({name: m.name}))},
104
143
  });
105
144
 
106
- if (debug) console.log(`[meridiane] ${models.length} model(s) generated`);
145
+ if (debug) log?.debug?.(`[meridiane] ${models.length} model(s) generated`);
107
146
  }
108
147
 
109
148
  /**
@@ -122,6 +161,8 @@ async function generateModels({cwd, libName, spec, requiredMode, formats, includ
122
161
  * log?: { step?: (msg: string) => void, info?: (msg: string) => void, success?: (msg: string) => void, debug?: (msg: string, data?: any) => void },
123
162
  * workspaceMode?: 'angular'|'standalone',
124
163
  * distRoot?: string,
164
+ * projectDir?: string,
165
+ * projectReadmePath?: string,
125
166
  * }} params
126
167
  */
127
168
  export async function generateBridgeWorkspace(params) {
@@ -139,30 +180,33 @@ export async function generateBridgeWorkspace(params) {
139
180
  debug,
140
181
  log,
141
182
  distRoot,
183
+ projectDir: projectDirInput,
184
+ projectReadmePath,
142
185
  } = params;
143
186
 
144
- log?.step?.(`génération de la librairie (projects/${libName})`);
145
- await generateLibrary({cwd, libName, packageName, version, debug});
187
+ const projectDir = projectDirInput ?? path.resolve(cwd, 'projects', libName);
188
+ log?.step?.(`génération de la librairie (${path.relative(cwd, projectDir)})`);
189
+ await generateLibrary({projectDir, libName, packageName, version, debug, log, projectReadmePath});
146
190
 
147
191
  // If a dist root is provided, force ng-packagr output to that directory.
148
192
  if (distRoot) {
149
- const ngPackagePath = path.resolve(cwd, 'projects', libName, 'ng-package.json');
193
+ const ngPackagePath = path.resolve(projectDir, 'ng-package.json');
150
194
  const ngPkg = await readJson(ngPackagePath);
151
195
  const desiredDestAbs = path.resolve(distRoot, libName);
152
- const projectDir = path.dirname(ngPackagePath);
153
- ngPkg.dest = path.relative(projectDir, desiredDestAbs).split(path.sep).join('/');
196
+ const projectRoot = path.dirname(ngPackagePath);
197
+ ngPkg.dest = path.relative(projectRoot, desiredDestAbs).split(path.sep).join('/');
154
198
  await writeJsonIfChanged(ngPackagePath, ngPkg);
155
199
  log?.debug?.('ng-package.json dest overridden', {dest: ngPkg.dest});
156
200
  }
157
201
 
158
202
  if (noModels) {
159
203
  log?.info?.('models ignorés (--no-models)');
160
- if (debug) console.log('[meridiane] --no-models enabled, skipping models generation');
204
+ if (debug) log?.debug?.('[meridiane] --no-models enabled, skipping models generation');
161
205
  return;
162
206
  }
163
207
 
164
208
  if (!spec) throw new Error('Internal error: spec is required when noModels=false');
165
209
  log?.step?.('génération des models (OpenAPI)');
166
- await generateModels({cwd, libName, spec, requiredMode, formats, include, exclude, debug});
210
+ await generateModels({projectDir, spec, requiredMode, formats, include, exclude, debug, log});
167
211
  log?.success?.('génération terminée');
168
212
  }
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { createRequire } from 'node:module';
4
4
 
5
+ import { runCommand } from './exec.js';
5
6
  import { writeJsonIfChanged } from './json.js';
6
7
 
7
8
  async function ensureDir(p) {
@@ -62,7 +63,8 @@ export async function ensureStandaloneWorkspace({ repoRoot, log }) {
62
63
  await ensureDir(distRoot);
63
64
  await ensureDir(workspaceRoot);
64
65
 
65
- const angularVersion = '20.1.7';
66
+ const angularVersion = (await detectAngularVersion(repoRoot)) ?? '20.1.7';
67
+ log?.debug?.('[meridiane] angular version', { angularVersion });
66
68
  await writeJsonIfChanged(path.join(workspaceRoot, 'package.json'), toolchainPackageJson({ angularVersion }));
67
69
  await writeJsonIfChanged(path.join(workspaceRoot, 'tsconfig.json'), rootTsconfigJson());
68
70
 
@@ -74,6 +76,21 @@ export async function ensureStandaloneWorkspace({ repoRoot, log }) {
74
76
  return { workspaceRoot, distRoot, npmCacheDir, requireFromWs };
75
77
  }
76
78
 
79
+ async function detectAngularVersion(repoRoot) {
80
+ const pkgPath = path.join(repoRoot, 'package.json');
81
+ try {
82
+ const raw = await fs.readFile(pkgPath, 'utf8');
83
+ const parsed = JSON.parse(raw);
84
+ const deps = parsed?.dependencies || {};
85
+ const devDeps = parsed?.devDependencies || {};
86
+ const peerDeps = parsed?.peerDependencies || {};
87
+ const v = deps['@angular/core'] ?? devDeps['@angular/core'] ?? peerDeps['@angular/core'];
88
+ return typeof v === 'string' && v.trim().length > 0 ? v.trim() : undefined;
89
+ } catch {
90
+ return undefined;
91
+ }
92
+ }
93
+
77
94
  export async function ensureToolchainInstalled({ workspaceRoot, requireFromWs, npmCacheDir, log }) {
78
95
  // Prefer using existing (hoisted) deps during local dev; in CI this will trigger installation.
79
96
  const canResolve = (id) => {
@@ -101,12 +118,7 @@ export async function ensureToolchainInstalled({ workspaceRoot, requireFromWs, n
101
118
  // We avoid generating a lockfile in the backend repo by keeping everything under dist/.
102
119
  const npmCmd = 'npm';
103
120
  const args = ['install', '--silent', '--no-progress'];
104
- const { spawn } = await import('node:child_process');
105
-
106
- const code = await new Promise((resolve) => {
107
- const child = spawn(npmCmd, args, { cwd: workspaceRoot, stdio: 'inherit', env });
108
- child.on('exit', (c) => resolve(c ?? 0));
109
- });
121
+ const code = await runCommand(npmCmd, args, { cwd: workspaceRoot, env });
110
122
 
111
123
  if (code !== 0) throw new Error(`Toolchain install failed (npm exit ${code})`);
112
124
  if (!(await exists(path.join(workspaceRoot, 'node_modules')))) {
@@ -114,4 +126,3 @@ export async function ensureToolchainInstalled({ workspaceRoot, requireFromWs, n
114
126
  }
115
127
  return true;
116
128
  }
117
-
File without changes
File without changes
File without changes
File without changes
File without changes