@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 +15 -1
- package/cli.js +30 -6
- package/package.json +1 -1
- package/templates/_lib_template/src/lib/bridge/rest/api-platform.adapter.ts +3 -21
- package/templates/_lib_template/src/lib/bridge/rest/http-request.options.ts +21 -0
- package/templates/_lib_template/src/lib/facades/bridge.facade.ts +3 -11
- package/tools/app/bridge-workflow.js +85 -0
- package/tools/app/run-build.js +41 -0
- package/tools/app/run-dev.js +82 -0
- package/tools/app/run-generate.js +58 -0
- package/tools/build.js +1 -105
- package/tools/dev.js +1 -219
- package/tools/domain/bridge-options.js +85 -0
- package/tools/generate.js +1 -0
- package/tools/infra/dev-support.js +112 -0
- package/tools/infra/exec.js +21 -0
- package/tools/{core/generate.js → infra/generate/bridge.js} +72 -28
- package/tools/{core/standalone.js → infra/standalone-workspace.js} +19 -8
- /package/tools/{core → domain}/lib-name.js +0 -0
- /package/tools/{core → domain}/paths.js +0 -0
- /package/tools/{core → infra}/json.js +0 -0
- /package/tools/{core → infra}/logger.js +0 -0
- /package/tools/{core/spec.js → infra/spec-loader.js} +0 -0
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 {
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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,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
|
|
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
|
|
79
|
+
const {method, url} = req;
|
|
79
80
|
const targetUrl = this.resolveUrl(url);
|
|
80
81
|
|
|
81
|
-
const mergedOptions
|
|
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
|
-
|
|
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
|
-
|
|
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 '
|
|
7
|
-
import {renderTemplateToFile} from '
|
|
8
|
-
import {ensureCleanDir} from '
|
|
9
|
-
import {buildModelsFromOpenAPI} from '
|
|
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, '
|
|
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({
|
|
61
|
+
async function generateLibrary({projectDir, libName, packageName, version, debug, log, projectReadmePath}) {
|
|
33
62
|
const tplDir = path.resolve(pkgRoot, 'templates/_lib_template');
|
|
34
|
-
const targetDir =
|
|
35
|
-
|
|
36
|
-
if (debug)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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({
|
|
64
|
-
const outDir = path.resolve(
|
|
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
|
-
|
|
69
|
-
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)
|
|
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
|
-
|
|
145
|
-
|
|
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(
|
|
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
|
|
153
|
-
ngPkg.dest = path.relative(
|
|
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)
|
|
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({
|
|
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
|
|
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
|