@obsidiane/meridiane 0.0.6 → 0.0.10

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 (47) hide show
  1. package/README.md +19 -31
  2. package/cli.js +64 -44
  3. package/package.json +3 -7
  4. package/templates/_lib_template/package.json +2 -1
  5. package/templates/_lib_template/src/lib/bridge/rest/api-platform.adapter.ts +48 -47
  6. package/templates/_lib_template/src/lib/bridge/rest/query-builder.ts +29 -29
  7. package/templates/_lib_template/src/lib/bridge/sse/eventsource-wrapper.ts +50 -54
  8. package/templates/_lib_template/src/lib/bridge/sse/mercure-topic.mapper.ts +48 -0
  9. package/templates/_lib_template/src/lib/bridge/sse/mercure-url.builder.ts +7 -7
  10. package/templates/_lib_template/src/lib/bridge/sse/mercure.adapter.ts +104 -49
  11. package/templates/_lib_template/src/lib/bridge/sse/ref-count-topic.registry.ts +23 -21
  12. package/templates/_lib_template/src/lib/bridge.types.ts +33 -0
  13. package/templates/_lib_template/src/lib/facades/bridge.facade.ts +116 -0
  14. package/templates/_lib_template/src/lib/facades/facade.factory.ts +21 -19
  15. package/templates/_lib_template/src/lib/facades/facade.interface.ts +13 -18
  16. package/templates/_lib_template/src/lib/facades/resource.facade.ts +42 -56
  17. package/templates/_lib_template/src/lib/interceptors/bridge-debug.interceptor.ts +32 -0
  18. package/templates/_lib_template/src/lib/interceptors/bridge-defaults.interceptor.ts +53 -0
  19. package/templates/_lib_template/src/lib/interceptors/content-type.interceptor.ts +36 -37
  20. package/templates/_lib_template/src/lib/ports/realtime.port.ts +5 -3
  21. package/templates/_lib_template/src/lib/ports/resource-repository.port.ts +14 -15
  22. package/templates/_lib_template/src/lib/provide-bridge.ts +122 -19
  23. package/templates/_lib_template/src/lib/tokens.ts +14 -1
  24. package/templates/_lib_template/src/lib/utils/url.ts +15 -0
  25. package/templates/_lib_template/src/models/index.ts +0 -1
  26. package/templates/_lib_template/src/public-api.ts +3 -2
  27. package/tools/build.js +104 -0
  28. package/tools/core/generate.js +152 -0
  29. package/tools/core/json.js +71 -0
  30. package/tools/core/lib-name.js +15 -0
  31. package/tools/core/logger.js +51 -0
  32. package/tools/core/paths.js +17 -0
  33. package/tools/core/spec.js +59 -0
  34. package/tools/core/standalone.js +117 -0
  35. package/tools/dev.js +218 -0
  36. package/tools/generator/models/handlebars.js +8 -2
  37. package/tools/generator/models/naming.js +21 -15
  38. package/tools/generator/models/openapi-to-models.js +75 -38
  39. package/tools/generator/models/schema-utils.js +84 -66
  40. package/tools/generator/models/templates/model.hbs +3 -3
  41. package/tools/generator/models/utils.js +1 -2
  42. package/models.config.example.js +0 -24
  43. package/templates/_lib_template/src/lib/bridge/credentials.policy.ts +0 -9
  44. package/templates/_lib_template/src/lib/utils/utils.ts +0 -21
  45. package/templates/_lib_template/tsconfig.spec.json +0 -14
  46. package/tools/generate-lib.js +0 -136
  47. package/tools/generate-models.js +0 -121
package/tools/build.js ADDED
@@ -0,0 +1,104 @@
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 { normalizePreset, 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 preset = normalizePreset(opts.preset);
34
+ const include = splitListArgs(opts.include);
35
+ const exclude = splitListArgs(opts.exclude);
36
+
37
+ const libName = deriveLibName(packageName);
38
+ const requiredMode = 'spec';
39
+
40
+ // Standalone mode (native): works from any repo (backend pipeline or otherwise).
41
+ log.title('build — standalone');
42
+ log.info(`repo: ${cwd}`);
43
+ log.info(`package: ${packageName}@${version}`);
44
+ log.info(`lib: ${libName}`);
45
+ if (noModels) log.info('models: désactivés (--no-models)');
46
+ else log.info(`spec: ${opts.spec} | preset: ${preset}`);
47
+
48
+ const { workspaceRoot, distRoot, npmCacheDir, requireFromWs } = await ensureStandaloneWorkspace({ repoRoot: cwd, log });
49
+ log.info(`workspace: ${workspaceRoot}`);
50
+
51
+ let spec;
52
+ if (!noModels) {
53
+ log.step('lecture de la spec OpenAPI');
54
+ spec = await readOpenApiSpec(opts.spec);
55
+ log.success('spec chargée');
56
+ }
57
+
58
+ await ensureToolchainInstalled({ workspaceRoot, requireFromWs, npmCacheDir, log });
59
+
60
+ await generateBridgeWorkspace({
61
+ cwd: workspaceRoot,
62
+ libName,
63
+ packageName,
64
+ version,
65
+ noModels,
66
+ spec,
67
+ requiredMode,
68
+ preset,
69
+ include,
70
+ exclude,
71
+ debug,
72
+ log,
73
+ distRoot,
74
+ });
75
+
76
+ const ngPackageJsonPath = path.join(workspaceRoot, 'projects', libName, 'ng-package.json');
77
+ const tsconfigProdPath = path.join(workspaceRoot, 'projects', libName, 'tsconfig.lib.prod.json');
78
+
79
+ log.step('ng-packagr');
80
+ const ngPackagrPath = requireFromWs.resolve('ng-packagr/src/cli/main.js');
81
+ const packagrCode = await run(process.execPath, [ngPackagrPath, '-p', ngPackageJsonPath, '-c', tsconfigProdPath], {
82
+ cwd: workspaceRoot,
83
+ });
84
+ if (packagrCode !== 0) process.exit(packagrCode);
85
+
86
+ const distDir = path.join(distRoot, libName);
87
+ log.step(`npm pack (${path.relative(cwd, distDir)})`);
88
+ const packCode = await run('npm', ['pack'], {
89
+ cwd: distDir,
90
+ env: {
91
+ ...process.env,
92
+ npm_config_cache: npmCacheDir,
93
+ npm_config_update_notifier: 'false',
94
+ npm_config_fund: 'false',
95
+ npm_config_audit: 'false',
96
+ },
97
+ });
98
+ if (packCode === 0) log.success(`package prêt: ${path.relative(cwd, distDir)}`);
99
+ process.exit(packCode);
100
+ }
101
+
102
+ if (import.meta.url === `file://${process.argv[1]}`) {
103
+ throw new Error('This module is not meant to be executed directly.');
104
+ }
@@ -0,0 +1,152 @@
1
+ import fs from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
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';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+
14
+ const pkgRoot = path.resolve(__dirname, '../..'); // packages/meridiane
15
+
16
+ async function replacePlaceholdersInDir(dir, placeholders) {
17
+ const entries = await fs.readdir(dir, { withFileTypes: true });
18
+ for (const entry of entries) {
19
+ const fullPath = path.join(dir, entry.name);
20
+ if (entry.isDirectory()) {
21
+ await replacePlaceholdersInDir(fullPath, placeholders);
22
+ } else if (entry.isFile()) {
23
+ let content = await fs.readFile(fullPath, 'utf8');
24
+ for (const [key, value] of Object.entries(placeholders)) {
25
+ content = content.replaceAll(key, value);
26
+ }
27
+ await fs.writeFile(fullPath, content);
28
+ }
29
+ }
30
+ }
31
+
32
+ async function generateLibrary({ cwd, libName, packageName, version, debug }) {
33
+ 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', { libName, packageName, version, targetDir: path.relative(cwd, targetDir) });
37
+
38
+ await fs.rm(targetDir, { recursive: true, force: true });
39
+ await fs.mkdir(targetDir, { recursive: true });
40
+ await fs.cp(tplDir, targetDir, { recursive: true });
41
+
42
+ await replacePlaceholdersInDir(targetDir, { '__LIB_NAME__': libName });
43
+
44
+ const libPackageJsonPath = path.join(targetDir, 'package.json');
45
+ if (existsSync(libPackageJsonPath)) {
46
+ const libPkg = await readJson(libPackageJsonPath);
47
+ libPkg.name = packageName;
48
+ libPkg.version = version;
49
+ await writeJsonIfChanged(libPackageJsonPath, libPkg);
50
+ }
51
+
52
+ // angular.json / workspace patching is intentionally not done here (Meridiane is standalone).
53
+ }
54
+
55
+ async function generateModels({ cwd, libName, spec, requiredMode, preset, include, exclude, debug }) {
56
+ const outDir = path.resolve(cwd, 'projects', libName, 'src', 'models');
57
+ const templatesDir = path.resolve(pkgRoot, 'tools', 'generator', 'models', 'templates');
58
+
59
+ if (debug) console.log('[meridiane] generate models', { outDir: path.relative(cwd, outDir), requiredMode, preset, includeCount: include.length, excludeCount: exclude.length });
60
+
61
+ await ensureCleanDir(outDir);
62
+
63
+ // Write models + index.ts.
64
+ const { models } = buildModelsFromOpenAPI(spec, {
65
+ requiredMode,
66
+ preset,
67
+ includeSchemaNames: include,
68
+ excludeSchemaNames: exclude,
69
+ });
70
+
71
+ for (const m of models) {
72
+ await renderTemplateToFile({
73
+ templatePath: path.join(templatesDir, 'model.hbs'),
74
+ outPath: path.join(outDir, `${m.name}.ts`),
75
+ ctx: {
76
+ name: m.name,
77
+ props: m.props,
78
+ imports: m.imports,
79
+ extendsTypes: m.extendsTypes || [],
80
+ },
81
+ });
82
+ }
83
+
84
+ await renderTemplateToFile({
85
+ templatePath: path.join(templatesDir, 'index.hbs'),
86
+ outPath: path.join(outDir, 'index.ts'),
87
+ ctx: { models: models.map((m) => ({ name: m.name })) },
88
+ });
89
+
90
+ if (debug) console.log(`[meridiane] ${models.length} model(s) generated`);
91
+ }
92
+
93
+ /**
94
+ * @param {{
95
+ * cwd: string,
96
+ * libName: string,
97
+ * packageName: string,
98
+ * version: string,
99
+ * noModels: boolean,
100
+ * spec?: any,
101
+ * requiredMode: 'all-optional'|'spec',
102
+ * preset: 'all'|'native',
103
+ * include: string[],
104
+ * exclude: string[],
105
+ * debug: boolean,
106
+ * log?: { step?: (msg: string) => void, info?: (msg: string) => void, success?: (msg: string) => void, debug?: (msg: string, data?: any) => void },
107
+ * workspaceMode?: 'angular'|'standalone',
108
+ * distRoot?: string,
109
+ * }} params
110
+ */
111
+ export async function generateBridgeWorkspace(params) {
112
+ const {
113
+ cwd,
114
+ libName,
115
+ packageName,
116
+ version,
117
+ noModels,
118
+ spec,
119
+ requiredMode,
120
+ preset,
121
+ include,
122
+ exclude,
123
+ debug,
124
+ log,
125
+ distRoot,
126
+ } = params;
127
+
128
+ log?.step?.(`génération de la librairie (projects/${libName})`);
129
+ await generateLibrary({ cwd, libName, packageName, version, debug });
130
+
131
+ // If a dist root is provided, force ng-packagr output to that directory.
132
+ if (distRoot) {
133
+ const ngPackagePath = path.resolve(cwd, 'projects', libName, 'ng-package.json');
134
+ const ngPkg = await readJson(ngPackagePath);
135
+ const desiredDestAbs = path.resolve(distRoot, libName);
136
+ const projectDir = path.dirname(ngPackagePath);
137
+ ngPkg.dest = path.relative(projectDir, desiredDestAbs).split(path.sep).join('/');
138
+ await writeJsonIfChanged(ngPackagePath, ngPkg);
139
+ log?.debug?.('ng-package.json dest overridden', { dest: ngPkg.dest });
140
+ }
141
+
142
+ if (noModels) {
143
+ log?.info?.('models ignorés (--no-models)');
144
+ if (debug) console.log('[meridiane] --no-models enabled, skipping models generation');
145
+ return;
146
+ }
147
+
148
+ if (!spec) throw new Error('Internal error: spec is required when noModels=false');
149
+ log?.step?.('génération des models (OpenAPI)');
150
+ await generateModels({ cwd, libName, spec, requiredMode, preset, include, exclude, debug });
151
+ log?.success?.('génération terminée');
152
+ }
@@ -0,0 +1,71 @@
1
+ import fs from 'node:fs/promises';
2
+
3
+ function stripJsonComments(input) {
4
+ let out = '';
5
+ let i = 0;
6
+ let inString = false;
7
+ let stringQuote = '"';
8
+ while (i < input.length) {
9
+ const ch = input[i];
10
+
11
+ if (inString) {
12
+ out += ch;
13
+ if (ch === '\\') {
14
+ // skip escaped char
15
+ i += 2;
16
+ continue;
17
+ }
18
+ if (ch === stringQuote) inString = false;
19
+ i += 1;
20
+ continue;
21
+ }
22
+
23
+ if (ch === '"' || ch === "'") {
24
+ inString = true;
25
+ stringQuote = ch;
26
+ out += ch;
27
+ i += 1;
28
+ continue;
29
+ }
30
+
31
+ // line comment
32
+ if (ch === '/' && input[i + 1] === '/') {
33
+ i += 2;
34
+ while (i < input.length && input[i] !== '\n') i += 1;
35
+ continue;
36
+ }
37
+
38
+ // block comment
39
+ if (ch === '/' && input[i + 1] === '*') {
40
+ i += 2;
41
+ while (i < input.length && !(input[i] === '*' && input[i + 1] === '/')) i += 1;
42
+ i += 2;
43
+ continue;
44
+ }
45
+
46
+ out += ch;
47
+ i += 1;
48
+ }
49
+ return out;
50
+ }
51
+
52
+ export async function readJson(filePath) {
53
+ const raw = await fs.readFile(filePath, 'utf8');
54
+ try {
55
+ return JSON.parse(raw);
56
+ } catch {
57
+ return JSON.parse(stripJsonComments(raw));
58
+ }
59
+ }
60
+
61
+ export async function writeJsonIfChanged(filePath, data) {
62
+ const next = JSON.stringify(data, null, 2) + '\n';
63
+ try {
64
+ const prev = await fs.readFile(filePath, 'utf8');
65
+ if (prev === next) return false;
66
+ } catch {
67
+ // ignore (file missing / unreadable)
68
+ }
69
+ await fs.writeFile(filePath, next, 'utf8');
70
+ return true;
71
+ }
@@ -0,0 +1,15 @@
1
+ export function deriveLibName(packageName) {
2
+ const raw = String(packageName || '').trim();
3
+ if (!raw) throw new Error('Missing packageName');
4
+
5
+ const base = raw.startsWith('@') ? (raw.split('/')[1] || '') : raw;
6
+ const normalized = base
7
+ .toLowerCase()
8
+ .replace(/[^a-z0-9-]+/g, '-')
9
+ .replace(/-+/g, '-')
10
+ .replace(/^-|-$/g, '');
11
+
12
+ if (!normalized) throw new Error(`Invalid packageName: ${packageName}`);
13
+ return normalized;
14
+ }
15
+
@@ -0,0 +1,51 @@
1
+ import process from 'node:process';
2
+
3
+ function color(code, text) {
4
+ if (!process.stdout.isTTY) return text;
5
+ return `\u001b[${code}m${text}\u001b[0m`;
6
+ }
7
+
8
+ function line(prefix, text) {
9
+ return `${prefix} ${text}`;
10
+ }
11
+
12
+ export function createLogger({ debug = false } = {}) {
13
+ const prefix = color('35', 'meridiane');
14
+
15
+ return {
16
+ debugEnabled: debug,
17
+
18
+ title(text) {
19
+ console.log(line('🌙', `${prefix} ${text}`));
20
+ },
21
+
22
+ step(text) {
23
+ console.log(line('🔧', `${prefix} ${text}`));
24
+ },
25
+
26
+ info(text) {
27
+ console.log(line('ℹ️', `${prefix} ${text}`));
28
+ },
29
+
30
+ success(text) {
31
+ console.log(line('✅', `${prefix} ${text}`));
32
+ },
33
+
34
+ warn(text) {
35
+ console.warn(line('⚠️', `${prefix} ${text}`));
36
+ },
37
+
38
+ debug(text, data) {
39
+ if (!debug) return;
40
+ if (data === undefined) console.log(line('🐛', `${prefix} ${text}`));
41
+ else console.log(line('🐛', `${prefix} ${text} ${JSON.stringify(data, null, 2)}`));
42
+ },
43
+
44
+ error(err) {
45
+ const msg = err?.message ?? String(err);
46
+ console.error(line('❌', `${prefix} ${msg}`));
47
+ if (debug && err?.stack) console.error(err.stack);
48
+ },
49
+ };
50
+ }
51
+
@@ -0,0 +1,17 @@
1
+ export function normalizePreset(presetOption) {
2
+ if (presetOption === undefined) return 'all';
3
+ if (presetOption === true) return 'native';
4
+ if (presetOption === 'native' || presetOption === 'all') return presetOption;
5
+ throw new Error(`Invalid --preset value: ${presetOption} (expected: native|all)`);
6
+ }
7
+
8
+ export function splitListArgs(values) {
9
+ const out = [];
10
+ for (const v of Array.isArray(values) ? values : []) {
11
+ for (const part of String(v).split(',')) {
12
+ const s = part.trim();
13
+ if (s) out.push(s);
14
+ }
15
+ }
16
+ return out;
17
+ }
@@ -0,0 +1,59 @@
1
+ import fs from 'node:fs/promises';
2
+
3
+ function isHttpUrl(v) {
4
+ return /^https?:\/\//i.test(String(v || ''));
5
+ }
6
+
7
+ async function fetchJson(url) {
8
+ let res;
9
+ try {
10
+ res = await fetch(url);
11
+ } catch (err) {
12
+ const msg = err?.message ?? String(err);
13
+ throw new Error(
14
+ `Spec fetch failed: ${url}\n` +
15
+ `Reason: ${msg}\n` +
16
+ `Tip: if your environment cannot access the network, use a local JSON file via --spec ./openapi.json`
17
+ );
18
+ }
19
+ if (!res.ok) {
20
+ const body = await res.text().catch(() => '');
21
+ throw new Error(`Spec fetch failed: ${res.status} ${res.statusText}${body ? `\n${body}` : ''}`);
22
+ }
23
+ return await res.json();
24
+ }
25
+
26
+ export async function readOpenApiSpec(specArg) {
27
+ if (!specArg) throw new Error('Missing --spec (url or JSON file path)');
28
+ const spec = String(specArg);
29
+
30
+ if (isHttpUrl(spec)) {
31
+ try {
32
+ return await fetchJson(spec);
33
+ } catch (err) {
34
+ const isDocsJson = /\/api\/docs\.json$/i.test(spec);
35
+ if (!isDocsJson) throw err;
36
+ const fallback = spec.replace(/\/api\/docs\.json$/i, '/api/docs.jsonopenapi');
37
+ try {
38
+ return await fetchJson(fallback);
39
+ } catch (err2) {
40
+ const m1 = err?.message ?? String(err);
41
+ const m2 = err2?.message ?? String(err2);
42
+ throw new Error(`Spec fetch failed (both endpoints tried)\n- ${spec}\n ${m1}\n- ${fallback}\n ${m2}`);
43
+ }
44
+ }
45
+ }
46
+
47
+ try {
48
+ const raw = await fs.readFile(spec, 'utf8');
49
+ return JSON.parse(raw);
50
+ } catch (err) {
51
+ if (err?.code === 'ENOENT') {
52
+ throw new Error(`Spec file not found: ${spec}`);
53
+ }
54
+ if (err?.name === 'SyntaxError') {
55
+ throw new Error(`Invalid JSON in spec file ${spec}: ${err.message}`);
56
+ }
57
+ throw err;
58
+ }
59
+ }
@@ -0,0 +1,117 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+
5
+ import { writeJsonIfChanged } from './json.js';
6
+
7
+ async function ensureDir(p) {
8
+ await fs.mkdir(p, { recursive: true });
9
+ }
10
+
11
+ async function exists(p) {
12
+ try {
13
+ await fs.stat(p);
14
+ return true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ function toolchainPackageJson({ angularVersion }) {
21
+ const v = angularVersion;
22
+ return {
23
+ name: 'meridiane-standalone-workspace',
24
+ private: true,
25
+ description: 'Temporary workspace created by @obsidiane/meridiane (standalone build).',
26
+ devDependencies: {
27
+ '@angular/common': v,
28
+ '@angular/compiler': v,
29
+ '@angular/compiler-cli': v,
30
+ '@angular/core': v,
31
+ 'ng-packagr': '20.1.0',
32
+ rxjs: '7.8.2',
33
+ tslib: '2.8.1',
34
+ typescript: '5.8.3',
35
+ },
36
+ };
37
+ }
38
+
39
+ function rootTsconfigJson() {
40
+ return {
41
+ compilerOptions: {
42
+ target: 'ES2022',
43
+ module: 'ES2022',
44
+ moduleResolution: 'Node',
45
+ lib: ['ES2022', 'DOM'],
46
+ strict: true,
47
+ skipLibCheck: true,
48
+ experimentalDecorators: true,
49
+ importHelpers: true,
50
+ },
51
+ angularCompilerOptions: {
52
+ compilationMode: 'partial',
53
+ strictTemplates: true,
54
+ },
55
+ };
56
+ }
57
+
58
+ export async function ensureStandaloneWorkspace({ repoRoot, log }) {
59
+ const distRoot = path.join(repoRoot, 'dist');
60
+ const workspaceRoot = path.join(distRoot, '.meridiane-workspace');
61
+
62
+ await ensureDir(distRoot);
63
+ await ensureDir(workspaceRoot);
64
+
65
+ const angularVersion = '20.1.7';
66
+ await writeJsonIfChanged(path.join(workspaceRoot, 'package.json'), toolchainPackageJson({ angularVersion }));
67
+ await writeJsonIfChanged(path.join(workspaceRoot, 'tsconfig.json'), rootTsconfigJson());
68
+
69
+ const npmCacheDir = path.join(workspaceRoot, '.npm-cache');
70
+ await ensureDir(npmCacheDir);
71
+
72
+ const requireFromWs = createRequire(path.join(workspaceRoot, 'package.json'));
73
+
74
+ return { workspaceRoot, distRoot, npmCacheDir, requireFromWs };
75
+ }
76
+
77
+ export async function ensureToolchainInstalled({ workspaceRoot, requireFromWs, npmCacheDir, log }) {
78
+ // Prefer using existing (hoisted) deps during local dev; in CI this will trigger installation.
79
+ const canResolve = (id) => {
80
+ try {
81
+ requireFromWs.resolve(id);
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ };
87
+
88
+ if (canResolve('ng-packagr/package.json')) return false;
89
+
90
+ log?.step?.('installation du toolchain (Angular + ng-packagr)');
91
+
92
+ // Use npm with a local cache to avoid writing to ~/.npm.
93
+ const env = {
94
+ ...process.env,
95
+ npm_config_cache: npmCacheDir,
96
+ npm_config_update_notifier: 'false',
97
+ npm_config_fund: 'false',
98
+ npm_config_audit: 'false',
99
+ };
100
+
101
+ // We avoid generating a lockfile in the backend repo by keeping everything under dist/.
102
+ const npmCmd = 'npm';
103
+ 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
+ });
110
+
111
+ if (code !== 0) throw new Error(`Toolchain install failed (npm exit ${code})`);
112
+ if (!(await exists(path.join(workspaceRoot, 'node_modules')))) {
113
+ throw new Error('Toolchain install failed: node_modules missing');
114
+ }
115
+ return true;
116
+ }
117
+