@obsidiane/meridiane 2.0.10 → 2.0.11
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 +19 -2
- package/cli.js +21 -37
- package/package.json +3 -3
- package/src/README.md +9 -0
- package/{tools/app → src/application}/bridge-workflow.js +15 -13
- package/src/application/generate-bridge.js +50 -0
- package/{tools/app → src/application}/run-build.js +1 -1
- package/{tools/app → src/application}/run-dev.js +30 -36
- package/{tools/app → src/application}/run-generate.js +3 -3
- package/{tools → src}/domain/bridge-options.js +8 -21
- package/src/domain/list-values.js +6 -0
- package/src/generators/bridge/write-bridge-files.js +100 -0
- package/src/generators/models/interface-schema.js +116 -0
- package/src/generators/models/model-selection.js +148 -0
- package/src/generators/models/oas-contract.js +170 -0
- package/{tools/generator → src/generators}/models/oas-schema-name.js +6 -15
- package/src/generators/models/openapi-to-models.js +136 -0
- package/{tools/generator → src/generators}/models/schema-utils.js +27 -3
- package/src/generators/models/templates/type-alias.hbs +2 -0
- package/src/generators/models/type-expression.js +65 -0
- package/src/generators/models/type-resolver.js +240 -0
- package/src/generators/models/utils.js +89 -0
- package/src/generators/models/write-model-files.js +55 -0
- package/src/infrastructure/dev-project.js +48 -0
- package/{tools/infra → src/infrastructure}/exec.js +3 -2
- package/src/infrastructure/local-package-install.js +104 -0
- package/src/infrastructure/spec-loader.js +65 -0
- package/{tools/infra → src/infrastructure}/standalone-workspace.js +31 -29
- package/templates/{_lib_template → angular-bridge}/README.md +9 -0
- package/templates/angular-bridge/src/lib/bridge/rest/api-platform.adapter.ts +82 -0
- package/templates/angular-bridge/src/lib/bridge/rest/http-request.options.ts +30 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/bridge/rest/query-builder.ts +10 -10
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-connections.ts +255 -0
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-payload.ts +82 -0
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.builder.ts +39 -0
- package/templates/angular-bridge/src/lib/bridge/sse/mercure.adapter.ts +358 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/ref-count-topic.registry.ts +3 -12
- package/templates/angular-bridge/src/lib/bridge/sse/subscription-claims.ts +76 -0
- package/templates/angular-bridge/src/lib/bridge-options.ts +59 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/facades/bridge.facade.ts +50 -51
- package/templates/{_lib_template → angular-bridge}/src/lib/facades/facade.factory.ts +0 -1
- package/templates/angular-bridge/src/lib/facades/facade.interface.ts +13 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/facades/resource.facade.ts +24 -31
- package/templates/angular-bridge/src/lib/interceptors/bearer-auth.interceptor.ts +33 -0
- package/templates/angular-bridge/src/lib/interceptors/bridge-defaults.interceptor.ts +65 -0
- package/templates/angular-bridge/src/lib/interceptors/content-type.interceptor.ts +37 -0
- package/templates/angular-bridge/src/lib/interceptors/scoped.interceptor.ts +9 -0
- package/templates/angular-bridge/src/lib/interceptors/singleflight.interceptor.ts +46 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/ports/realtime.port.ts +6 -2
- package/templates/{_lib_template → angular-bridge}/src/lib/ports/resource-repository.port.ts +7 -7
- package/templates/angular-bridge/src/lib/provide-bridge.ts +98 -0
- package/templates/angular-bridge/src/lib/utils/iri.ts +6 -0
- package/templates/angular-bridge/src/lib/utils/request-url.ts +31 -0
- package/templates/{_lib_template → angular-bridge}/src/public-api.ts +2 -0
- package/templates/_lib_template/src/lib/bridge/rest/api-platform.adapter.ts +0 -84
- package/templates/_lib_template/src/lib/bridge/rest/http-request.options.ts +0 -21
- package/templates/_lib_template/src/lib/bridge/sse/mercure-url.builder.ts +0 -17
- package/templates/_lib_template/src/lib/bridge/sse/mercure.adapter.ts +0 -806
- package/templates/_lib_template/src/lib/facades/facade.interface.ts +0 -31
- package/templates/_lib_template/src/lib/interceptors/bridge-defaults.interceptor.ts +0 -53
- package/templates/_lib_template/src/lib/interceptors/content-type.interceptor.ts +0 -49
- package/templates/_lib_template/src/lib/interceptors/singleflight.interceptor.ts +0 -55
- package/templates/_lib_template/src/lib/provide-bridge.ts +0 -186
- package/tools/build.js +0 -1
- package/tools/dev.js +0 -1
- package/tools/domain/paths.js +0 -10
- package/tools/generate.js +0 -1
- package/tools/generator/models/oas-contract.js +0 -159
- package/tools/generator/models/openapi-to-models.js +0 -240
- package/tools/generator/models/type-resolver.js +0 -107
- package/tools/generator/models/utils.js +0 -62
- package/tools/infra/dev-support.js +0 -189
- package/tools/infra/generate/bridge.js +0 -212
- package/tools/infra/spec-loader.js +0 -59
- /package/{tools → src}/domain/lib-name.js +0 -0
- /package/{tools/generator → src/generators}/models/handlebars.js +0 -0
- /package/{tools/generator → src/generators}/models/templates/index.hbs +0 -0
- /package/{tools/generator → src/generators}/models/templates/model.hbs +0 -0
- /package/{tools/infra → src/infrastructure}/json.js +0 -0
- /package/{tools/infra → src/infrastructure}/logger.js +0 -0
- /package/templates/{_lib_template → angular-bridge}/ng-package.json +0 -0
- /package/templates/{_lib_template → angular-bridge}/package.json +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/eventsource-wrapper.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/mercure-topic.mapper.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/bridge.types.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/interceptors/bridge-debug.interceptor.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/tokens.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/utils/url.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/models/index.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/tsconfig.lib.json +0 -0
- /package/templates/{_lib_template → angular-bridge}/tsconfig.lib.prod.json +0 -0
package/README.md
CHANGED
|
@@ -59,10 +59,27 @@ Sorties :
|
|
|
59
59
|
## Notes importantes
|
|
60
60
|
|
|
61
61
|
- fallback automatique `.../api/docs.json` -> `.../api/docs.jsonopenapi` si nécessaire
|
|
62
|
-
- mode contract-driven : uniquement les schémas atteignables via `paths` pour les formats sélectionnés
|
|
62
|
+
- mode contract-driven : uniquement les schémas atteignables via `paths` et `webhooks` pour les formats sélectionnés
|
|
63
63
|
- modèles `*jsonMergePatch*` non générés (`PATCH` typé `Partial<T>`)
|
|
64
64
|
- Meridiane ne publie pas : publication via `npm publish` côté CI
|
|
65
65
|
|
|
66
|
+
## Couverture des schémas
|
|
67
|
+
|
|
68
|
+
Le compilateur suit les types d’[OpenAPI 3.0](https://spec.openapis.org/oas/v3.0.4.html) et le dialecte
|
|
69
|
+
[JSON Schema 2020-12 d’OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0.html#schema-object) :
|
|
70
|
+
|
|
71
|
+
- primitives, unions de `type`, objets, propriétés requises, dictionnaires et schémas booléens ;
|
|
72
|
+
- tableaux homogènes, tuples `prefixItems`, tuples historiques et bornes de longueur des tuples ;
|
|
73
|
+
- `enum` et `const`, y compris les valeurs JSON structurées ;
|
|
74
|
+
- `allOf`, `oneOf`, `anyOf`, `nullable` et références locales vers `components.schemas` ou `$defs` ;
|
|
75
|
+
- schémas racine objets sous forme d’interfaces et autres schémas sous forme d’aliases TypeScript ;
|
|
76
|
+
- contrats atteignables depuis les bodies, paramètres, en-têtes, callbacks et webhooks.
|
|
77
|
+
|
|
78
|
+
Les règles de validation sans équivalent structurel TypeScript (`pattern`, bornes numériques,
|
|
79
|
+
`uniqueItems`, `contains`, `not`, conditions, fermeture exacte des propriétés, etc.) restent à la
|
|
80
|
+
charge de la validation runtime. Les références externes doivent être préalablement regroupées dans
|
|
81
|
+
le document OpenAPI fourni à Meridiane.
|
|
82
|
+
|
|
66
83
|
## Spécifique à ce repo
|
|
67
84
|
|
|
68
|
-
Dans ce monorepo, `meridiane dev` peut être exécuté sans `packageName` et cible alors `@obsidiane/bridge-sandbox` (app `
|
|
85
|
+
Dans ce monorepo, `meridiane dev` peut être exécuté sans `packageName` et cible alors `@obsidiane/bridge-sandbox` (app `playground/angular-app`).
|
package/cli.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import process from 'node:process';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { runGenerate } from './
|
|
8
|
-
import { createLogger } from './
|
|
5
|
+
import { runBuild } from './src/application/run-build.js';
|
|
6
|
+
import { runDev } from './src/application/run-dev.js';
|
|
7
|
+
import { runGenerate } from './src/application/run-generate.js';
|
|
8
|
+
import { createLogger } from './src/infrastructure/logger.js';
|
|
9
9
|
|
|
10
10
|
const program = new Command();
|
|
11
11
|
|
|
@@ -30,6 +30,20 @@ function commonOptions(cmd) {
|
|
|
30
30
|
.option('--debug', 'Enable debug logs');
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function action(run) {
|
|
34
|
+
return async (packageName, opts) => {
|
|
35
|
+
const log = createLogger({debug: !!opts?.debug});
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const exitCode = await run(packageName, opts);
|
|
39
|
+
if (typeof exitCode === 'number' && exitCode !== 0) process.exit(exitCode);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
log.error(error);
|
|
42
|
+
process.exit(typeof error?.exitCode === 'number' ? error.exitCode : 1);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
commonOptions(
|
|
34
48
|
program
|
|
35
49
|
.command('dev')
|
|
@@ -37,17 +51,7 @@ commonOptions(
|
|
|
37
51
|
'[packageName]',
|
|
38
52
|
'NPM package name of the generated bridge (e.g. @acme/backend-bridge). Optional in this repo (defaults to @obsidiane/bridge-sandbox).'
|
|
39
53
|
)
|
|
40
|
-
.action(
|
|
41
|
-
const log = createLogger({ debug: !!opts?.debug });
|
|
42
|
-
try {
|
|
43
|
-
const exitCode = await runDev(packageName, opts);
|
|
44
|
-
if (typeof exitCode === 'number' && exitCode !== 0) process.exit(exitCode);
|
|
45
|
-
} catch (err) {
|
|
46
|
-
log.error(err);
|
|
47
|
-
const exitCode = typeof err?.exitCode === 'number' ? err.exitCode : 1;
|
|
48
|
-
process.exit(exitCode);
|
|
49
|
-
}
|
|
50
|
-
})
|
|
54
|
+
.action(action(runDev))
|
|
51
55
|
);
|
|
52
56
|
|
|
53
57
|
commonOptions(
|
|
@@ -55,17 +59,7 @@ commonOptions(
|
|
|
55
59
|
.command('build')
|
|
56
60
|
.argument('<packageName>', 'NPM package name of the generated bridge (e.g. @acme/backend-bridge)')
|
|
57
61
|
.option('--version <semver>', 'Version to write in the generated bridge package.json (CI/CD)')
|
|
58
|
-
.action(
|
|
59
|
-
const log = createLogger({ debug: !!opts?.debug });
|
|
60
|
-
try {
|
|
61
|
-
const exitCode = await runBuild(packageName, opts);
|
|
62
|
-
if (typeof exitCode === 'number' && exitCode !== 0) process.exit(exitCode);
|
|
63
|
-
} catch (err) {
|
|
64
|
-
log.error(err);
|
|
65
|
-
const exitCode = typeof err?.exitCode === 'number' ? err.exitCode : 1;
|
|
66
|
-
process.exit(exitCode);
|
|
67
|
-
}
|
|
68
|
-
})
|
|
62
|
+
.action(action(runBuild))
|
|
69
63
|
);
|
|
70
64
|
|
|
71
65
|
commonOptions(
|
|
@@ -74,17 +68,7 @@ commonOptions(
|
|
|
74
68
|
.argument('<packageName>', 'NPM package name of the generated bridge (e.g. @acme/backend-bridge)')
|
|
75
69
|
.option('--version <semver>', 'Version to write in the generated bridge package.json')
|
|
76
70
|
.option('--out <dir>', 'Output directory (default: projects/<libName>)')
|
|
77
|
-
.action(
|
|
78
|
-
const log = createLogger({ debug: !!opts?.debug });
|
|
79
|
-
try {
|
|
80
|
-
const exitCode = await runGenerate(packageName, opts);
|
|
81
|
-
if (typeof exitCode === 'number' && exitCode !== 0) process.exit(exitCode);
|
|
82
|
-
} catch (err) {
|
|
83
|
-
log.error(err);
|
|
84
|
-
const exitCode = typeof err?.exitCode === 'number' ? err.exitCode : 1;
|
|
85
|
-
process.exit(exitCode);
|
|
86
|
-
}
|
|
87
|
-
})
|
|
71
|
+
.action(action(runGenerate))
|
|
88
72
|
);
|
|
89
73
|
|
|
90
74
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@obsidiane/meridiane",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.11",
|
|
4
4
|
"description": "Générateur de librairie Angular et modèles TypeScript (OpenAPI) pour un bridge Symfony/API Platform + Mercure.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
|
-
"url": "https://github.com/obsidiane-lab/meridiane"
|
|
7
|
+
"url": "git+https://github.com/obsidiane-lab/meridiane.git"
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"publishConfig": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
20
|
"cli.js",
|
|
21
|
-
"
|
|
21
|
+
"src/**",
|
|
22
22
|
"templates/**",
|
|
23
23
|
"README.md"
|
|
24
24
|
],
|
package/src/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Sources internes du générateur
|
|
2
|
+
|
|
3
|
+
- `application/` orchestre les workflows du CLI ;
|
|
4
|
+
- `domain/` contient les règles et la normalisation des options ;
|
|
5
|
+
- `infrastructure/` gère fichiers, processus, specs, workspaces et installation locale ;
|
|
6
|
+
- `generators/bridge/` écrit les fichiers du runtime Angular ;
|
|
7
|
+
- `generators/models/` transforme OpenAPI et écrit les modèles TypeScript.
|
|
8
|
+
|
|
9
|
+
Le runtime Angular généré se trouve dans `../templates/angular-bridge/`.
|
|
@@ -2,10 +2,10 @@ import process from 'node:process';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { ensureStandaloneWorkspace, ensureToolchainInstalled } from '../
|
|
8
|
-
import { ExitCodeError, runCommand } from '../
|
|
5
|
+
import { generateBridgeWorkspace } from './generate-bridge.js';
|
|
6
|
+
import { readOpenApiSpec } from '../infrastructure/spec-loader.js';
|
|
7
|
+
import { ensureStandaloneWorkspace, ensureToolchainInstalled } from '../infrastructure/standalone-workspace.js';
|
|
8
|
+
import { ExitCodeError, runCommand } from '../infrastructure/exec.js';
|
|
9
9
|
|
|
10
10
|
export async function runBridgeWorkflow({
|
|
11
11
|
repoRoot,
|
|
@@ -28,15 +28,7 @@ export async function runBridgeWorkflow({
|
|
|
28
28
|
const { workspaceRoot, distRoot, npmCacheDir, requireFromWs } = await ensureStandaloneWorkspace({ repoRoot, log });
|
|
29
29
|
log?.info?.(`workspace: ${workspaceRoot}`);
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
if (!noModels) {
|
|
33
|
-
if (!resolvedSpec) {
|
|
34
|
-
if (!specSource) throw new Error('Missing --spec (url or JSON file path)');
|
|
35
|
-
log?.step?.('lecture de la spec OpenAPI');
|
|
36
|
-
resolvedSpec = await readOpenApiSpec(specSource);
|
|
37
|
-
log?.success?.('spec chargée');
|
|
38
|
-
}
|
|
39
|
-
}
|
|
31
|
+
const resolvedSpec = await loadSpecIfNeeded({ noModels, spec, specSource, log });
|
|
40
32
|
|
|
41
33
|
await ensureToolchainInstalled({ workspaceRoot, requireFromWs, npmCacheDir, log });
|
|
42
34
|
|
|
@@ -86,6 +78,16 @@ export async function runBridgeWorkflow({
|
|
|
86
78
|
return { workspaceRoot, distRoot, distDir, npmCacheDir };
|
|
87
79
|
}
|
|
88
80
|
|
|
81
|
+
async function loadSpecIfNeeded({ noModels, spec, specSource, log }) {
|
|
82
|
+
if (noModels || spec) return spec;
|
|
83
|
+
if (!specSource) throw new Error('Missing --spec (url or JSON file path)');
|
|
84
|
+
|
|
85
|
+
log?.step?.('lecture de la spec OpenAPI');
|
|
86
|
+
const loadedSpec = await readOpenApiSpec(specSource);
|
|
87
|
+
log?.success?.('spec chargée');
|
|
88
|
+
return loadedSpec;
|
|
89
|
+
}
|
|
90
|
+
|
|
89
91
|
async function cleanupPackedTarballs(distDir) {
|
|
90
92
|
const entries = await fs.readdir(distDir, {withFileTypes: true});
|
|
91
93
|
const tarballs = entries.filter((entry) => entry.isFile() && entry.name.endsWith('.tgz'));
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { configureBridgeOutput, writeBridgeFiles } from '../generators/bridge/write-bridge-files.js';
|
|
4
|
+
import { writeModelFiles } from '../generators/models/write-model-files.js';
|
|
5
|
+
|
|
6
|
+
/** Generates the Angular bridge sources in a workspace project. */
|
|
7
|
+
export async function generateBridgeWorkspace({
|
|
8
|
+
cwd,
|
|
9
|
+
libName,
|
|
10
|
+
packageName,
|
|
11
|
+
version,
|
|
12
|
+
noModels,
|
|
13
|
+
spec,
|
|
14
|
+
requiredMode,
|
|
15
|
+
formats,
|
|
16
|
+
include,
|
|
17
|
+
exclude,
|
|
18
|
+
debug,
|
|
19
|
+
log,
|
|
20
|
+
distRoot,
|
|
21
|
+
projectDir: configuredProjectDir,
|
|
22
|
+
projectReadmePath,
|
|
23
|
+
}) {
|
|
24
|
+
const projectDir = configuredProjectDir ?? path.resolve(cwd, 'projects', libName);
|
|
25
|
+
|
|
26
|
+
log?.step?.(`génération de la librairie (${path.relative(cwd, projectDir)})`);
|
|
27
|
+
await writeBridgeFiles({
|
|
28
|
+
projectDir,
|
|
29
|
+
libName,
|
|
30
|
+
packageName,
|
|
31
|
+
version,
|
|
32
|
+
projectReadmePath,
|
|
33
|
+
debug,
|
|
34
|
+
log,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (distRoot) await configureBridgeOutput({ projectDir, distRoot, libName, log });
|
|
38
|
+
|
|
39
|
+
if (noModels) {
|
|
40
|
+
log?.info?.('models ignorés (--no-models)');
|
|
41
|
+
if (debug) log?.debug?.('[meridiane] --no-models enabled, skipping models generation');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!spec) throw new Error('Internal error: spec is required when noModels=false');
|
|
46
|
+
|
|
47
|
+
log?.step?.('génération des models (OpenAPI)');
|
|
48
|
+
await writeModelFiles({ projectDir, spec, requiredMode, formats, include, exclude, debug, log });
|
|
49
|
+
log?.success?.('génération terminée');
|
|
50
|
+
}
|
|
@@ -2,7 +2,7 @@ import process from 'node:process';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { normalizeBuildOptions } from '../domain/bridge-options.js';
|
|
5
|
-
import { createLogger } from '../
|
|
5
|
+
import { createLogger } from '../infrastructure/logger.js';
|
|
6
6
|
import { runBridgeWorkflow } from './bridge-workflow.js';
|
|
7
7
|
|
|
8
8
|
export async function runBuild(packageName, opts) {
|
|
@@ -2,17 +2,37 @@ import process from 'node:process';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { normalizeDevOptions } from '../domain/bridge-options.js';
|
|
5
|
-
import { createLogger } from '../
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
resolveDevAppRoot,
|
|
10
|
-
resolveSandboxAppRoot,
|
|
11
|
-
resolveSpecSource,
|
|
12
|
-
} from '../infra/dev-support.js';
|
|
13
|
-
import { readOpenApiSpec } from '../infra/spec-loader.js';
|
|
5
|
+
import { createLogger } from '../infrastructure/logger.js';
|
|
6
|
+
import {resolveDevAppRoot, resolveSandboxAppRoot, resolveSpecSource} from '../infrastructure/dev-project.js';
|
|
7
|
+
import {installPackageFromDist} from '../infrastructure/local-package-install.js';
|
|
8
|
+
import { readOpenApiSpec } from '../infrastructure/spec-loader.js';
|
|
14
9
|
import { runBridgeWorkflow } from './bridge-workflow.js';
|
|
15
10
|
|
|
11
|
+
async function loadDevSpec({config, opts, appRoot, log}) {
|
|
12
|
+
if (config.noModels) return {noModels: true};
|
|
13
|
+
|
|
14
|
+
const specSource = await resolveSpecSource({
|
|
15
|
+
useSandboxDefaults: config.useSandboxDefaults,
|
|
16
|
+
appRoot,
|
|
17
|
+
specArg: opts.spec,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (!specSource) throw new Error('Missing --spec (url or JSON file path)');
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
return {noModels: false, specSource, spec: await readOpenApiSpec(specSource)};
|
|
24
|
+
} catch (error) {
|
|
25
|
+
const canSkipModels = config.useSandboxDefaults && opts.spec == null;
|
|
26
|
+
if (!canSkipModels) throw error;
|
|
27
|
+
|
|
28
|
+
log.warn(
|
|
29
|
+
'spec OpenAPI indisponible (backend non démarré ?) → génération des models ignorée. ' +
|
|
30
|
+
'Démarrez le backend ou passez `--spec ./openapi.json` (ou `--no-models`).'
|
|
31
|
+
);
|
|
32
|
+
return {noModels: true, specSource};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
16
36
|
export async function runDev(packageName, opts) {
|
|
17
37
|
const startCwd = process.cwd();
|
|
18
38
|
const config = normalizeDevOptions(packageName, opts);
|
|
@@ -20,32 +40,7 @@ export async function runDev(packageName, opts) {
|
|
|
20
40
|
|
|
21
41
|
const appRoot = config.useSandboxDefaults ? await resolveSandboxAppRoot(startCwd) : await resolveDevAppRoot(startCwd);
|
|
22
42
|
const projectReadmePath = path.join(appRoot, 'README.md');
|
|
23
|
-
|
|
24
|
-
const specSource = config.noModels
|
|
25
|
-
? undefined
|
|
26
|
-
: await resolveSpecSource({
|
|
27
|
-
useSandboxDefaults: config.useSandboxDefaults,
|
|
28
|
-
appRoot,
|
|
29
|
-
specArg: opts.spec,
|
|
30
|
-
});
|
|
31
|
-
let noModels = config.noModels;
|
|
32
|
-
let spec;
|
|
33
|
-
if (!noModels) {
|
|
34
|
-
if (!specSource) throw new Error('Missing --spec (url or JSON file path)');
|
|
35
|
-
try {
|
|
36
|
-
spec = await readOpenApiSpec(specSource);
|
|
37
|
-
} catch (err) {
|
|
38
|
-
if (config.useSandboxDefaults && opts.spec == null) {
|
|
39
|
-
noModels = true;
|
|
40
|
-
log.warn(
|
|
41
|
-
"spec OpenAPI indisponible (backend non démarré ?) → génération des models ignorée. " +
|
|
42
|
-
'Démarrez le backend ou passez `--spec ./openapi.json` (ou `--no-models`).'
|
|
43
|
-
);
|
|
44
|
-
} else {
|
|
45
|
-
throw err;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
43
|
+
const {noModels, specSource, spec} = await loadDevSpec({config, opts, appRoot, log});
|
|
49
44
|
|
|
50
45
|
log.title('dev — build standalone + install local');
|
|
51
46
|
log.info(`app: ${appRoot}`);
|
|
@@ -74,7 +69,6 @@ export async function runDev(packageName, opts) {
|
|
|
74
69
|
projectReadmePath,
|
|
75
70
|
});
|
|
76
71
|
|
|
77
|
-
await findLatestTgz(distDir);
|
|
78
72
|
await installPackageFromDist({ packageName: config.packageName, distDir, appRoot, log });
|
|
79
73
|
|
|
80
74
|
log.success('bridge installé (node_modules) ✅');
|
|
@@ -2,9 +2,9 @@ import process from 'node:process';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { normalizeGenerateOptions } from '../domain/bridge-options.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { generateBridgeWorkspace } from './generate-bridge.js';
|
|
6
|
+
import { createLogger } from '../infrastructure/logger.js';
|
|
7
|
+
import { readOpenApiSpec } from '../infrastructure/spec-loader.js';
|
|
8
8
|
|
|
9
9
|
function resolveProjectDir({ cwd, libName, outDir }) {
|
|
10
10
|
if (outDir && outDir.length > 0) return path.resolve(cwd, outDir);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { deriveLibName } from './lib-name.js';
|
|
2
|
-
import {
|
|
2
|
+
import {splitListArgs} from './list-values.js';
|
|
3
3
|
|
|
4
4
|
const DEFAULT_FORMATS = ['application/ld+json'];
|
|
5
5
|
const SANDBOX_PACKAGE_NAME = '@obsidiane/bridge-sandbox';
|
|
@@ -23,13 +23,12 @@ function normalizeVersion(version) {
|
|
|
23
23
|
return raw || '0.0.0';
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
function normalizeNamedOptions(packageName, opts) {
|
|
27
27
|
const common = normalizeCommonOptions(opts);
|
|
28
28
|
const normalizedPackageName = String(packageName || '').trim();
|
|
29
29
|
if (!normalizedPackageName) throw new Error('Missing packageName');
|
|
30
30
|
|
|
31
31
|
return {
|
|
32
|
-
mode: 'build',
|
|
33
32
|
packageName: normalizedPackageName,
|
|
34
33
|
libName: deriveLibName(normalizedPackageName),
|
|
35
34
|
version: normalizeVersion(opts?.version),
|
|
@@ -42,25 +41,14 @@ export function normalizeBuildOptions(packageName, opts) {
|
|
|
42
41
|
};
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
export function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (!normalizedPackageName) throw new Error('Missing packageName');
|
|
49
|
-
|
|
50
|
-
const outDir = typeof opts?.out === 'string' ? opts.out.trim() : '';
|
|
44
|
+
export function normalizeBuildOptions(packageName, opts) {
|
|
45
|
+
return normalizeNamedOptions(packageName, opts);
|
|
46
|
+
}
|
|
51
47
|
|
|
48
|
+
export function normalizeGenerateOptions(packageName, opts) {
|
|
52
49
|
return {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
libName: deriveLibName(normalizedPackageName),
|
|
56
|
-
version: normalizeVersion(opts?.version),
|
|
57
|
-
requiredMode: 'spec',
|
|
58
|
-
formats: normalizeFormats(common.formats),
|
|
59
|
-
include: common.include,
|
|
60
|
-
exclude: common.exclude,
|
|
61
|
-
noModels: common.noModels,
|
|
62
|
-
debug: common.debug,
|
|
63
|
-
outDir,
|
|
50
|
+
...normalizeNamedOptions(packageName, opts),
|
|
51
|
+
outDir: typeof opts?.out === 'string' ? opts.out.trim() : '',
|
|
64
52
|
};
|
|
65
53
|
}
|
|
66
54
|
|
|
@@ -70,7 +58,6 @@ export function normalizeDevOptions(packageName, opts) {
|
|
|
70
58
|
const effectivePackageName = String(packageName ?? SANDBOX_PACKAGE_NAME).trim();
|
|
71
59
|
|
|
72
60
|
return {
|
|
73
|
-
mode: 'dev',
|
|
74
61
|
useSandboxDefaults,
|
|
75
62
|
packageName: effectivePackageName,
|
|
76
63
|
libName: deriveLibName(effectivePackageName),
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import { readJson, writeJsonIfChanged } from '../../infrastructure/json.js';
|
|
6
|
+
|
|
7
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const packageRoot = path.resolve(currentDir, '../../..');
|
|
9
|
+
const bridgeTemplateDir = path.join(packageRoot, 'templates', 'angular-bridge');
|
|
10
|
+
|
|
11
|
+
export async function writeBridgeFiles({
|
|
12
|
+
projectDir,
|
|
13
|
+
libName,
|
|
14
|
+
packageName,
|
|
15
|
+
version,
|
|
16
|
+
projectReadmePath,
|
|
17
|
+
debug,
|
|
18
|
+
log,
|
|
19
|
+
}) {
|
|
20
|
+
if (debug) {
|
|
21
|
+
log?.debug?.('[meridiane] generate lib', {
|
|
22
|
+
libName,
|
|
23
|
+
packageName,
|
|
24
|
+
version,
|
|
25
|
+
targetDir: projectDir,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await fs.rm(projectDir, { recursive: true, force: true });
|
|
30
|
+
await fs.mkdir(projectDir, { recursive: true });
|
|
31
|
+
await fs.cp(bridgeTemplateDir, projectDir, { recursive: true });
|
|
32
|
+
|
|
33
|
+
await replacePlaceholders(projectDir, {
|
|
34
|
+
'__LIB_NAME__': libName,
|
|
35
|
+
'__PACKAGE_NAME__': packageName,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
39
|
+
const packageJson = await readJson(packageJsonPath);
|
|
40
|
+
packageJson.name = packageName;
|
|
41
|
+
packageJson.version = version;
|
|
42
|
+
await writeJsonIfChanged(packageJsonPath, packageJson);
|
|
43
|
+
|
|
44
|
+
await appendProjectReadme(path.join(projectDir, 'README.md'), projectReadmePath, debug, log);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function configureBridgeOutput({ projectDir, distRoot, libName, log }) {
|
|
48
|
+
const ngPackagePath = path.join(projectDir, 'ng-package.json');
|
|
49
|
+
const ngPackage = await readJson(ngPackagePath);
|
|
50
|
+
const outputDir = path.join(distRoot, libName);
|
|
51
|
+
|
|
52
|
+
ngPackage.dest = path.relative(projectDir, outputDir).split(path.sep).join('/');
|
|
53
|
+
await writeJsonIfChanged(ngPackagePath, ngPackage);
|
|
54
|
+
log?.debug?.('ng-package.json dest overridden', { dest: ngPackage.dest });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function replacePlaceholders(directory, placeholders) {
|
|
58
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
59
|
+
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const filePath = path.join(directory, entry.name);
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
await replacePlaceholders(filePath, placeholders);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!entry.isFile()) continue;
|
|
67
|
+
|
|
68
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
69
|
+
const rendered = Object.entries(placeholders)
|
|
70
|
+
.reduce((value, [placeholder, replacement]) => value.replaceAll(placeholder, replacement), content);
|
|
71
|
+
if (rendered !== content) await fs.writeFile(filePath, rendered);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function appendProjectReadme(targetPath, sourcePath, debug, log) {
|
|
76
|
+
if (!sourcePath) return;
|
|
77
|
+
|
|
78
|
+
const projectReadme = await readText(sourcePath);
|
|
79
|
+
if (!projectReadme?.trim()) return;
|
|
80
|
+
|
|
81
|
+
const bridgeReadme = await readText(targetPath);
|
|
82
|
+
if (!bridgeReadme?.trim()) {
|
|
83
|
+
await fs.writeFile(targetPath, projectReadme);
|
|
84
|
+
if (debug) log?.debug?.('[meridiane] project README copied', { source: sourcePath });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const combinedReadme =
|
|
89
|
+
`${bridgeReadme.trimEnd()}\n\n---\n\n## Documentation du projet\n\n${projectReadme.trimStart()}`;
|
|
90
|
+
await fs.writeFile(targetPath, combinedReadme);
|
|
91
|
+
if (debug) log?.debug?.('[meridiane] project README appended', { source: sourcePath });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function readText(filePath) {
|
|
95
|
+
try {
|
|
96
|
+
return await fs.readFile(filePath, 'utf8');
|
|
97
|
+
} catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {hasOwn, isObject, mergeAllOf, schemaNameFromRef} from './schema-utils.js';
|
|
2
|
+
|
|
3
|
+
function hasPropertyConflict(left, right) {
|
|
4
|
+
return Object.keys(right).some((propertyName) => hasOwn(left, propertyName));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Finds component schemas that can be emitted as TypeScript interfaces without
|
|
9
|
+
* losing schema semantics. Other schemas are emitted as type aliases.
|
|
10
|
+
*/
|
|
11
|
+
export function collectInterfaceTypeNames(schemas, schemaNames, typeNames) {
|
|
12
|
+
const results = new Map();
|
|
13
|
+
const visiting = new Set();
|
|
14
|
+
|
|
15
|
+
function isInterfaceSchema(schema) {
|
|
16
|
+
if (!isObject(schema)) return false;
|
|
17
|
+
if (
|
|
18
|
+
schema.$ref || schema.nullable || hasOwn(schema, 'const') || Array.isArray(schema.enum) ||
|
|
19
|
+
Array.isArray(schema.oneOf) || Array.isArray(schema.anyOf) || schema.patternProperties ||
|
|
20
|
+
(hasOwn(schema, 'additionalProperties') && schema.additionalProperties !== false)
|
|
21
|
+
) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const declaresObject = schema.type === 'object' || isObject(schema.properties);
|
|
26
|
+
if (schema.type && schema.type !== 'object') return false;
|
|
27
|
+
if (!Array.isArray(schema.allOf)) return declaresObject;
|
|
28
|
+
|
|
29
|
+
const hasOnlyObjectParts = schema.allOf.every((part) => {
|
|
30
|
+
if (!part?.$ref) return isInterfaceSchema(part);
|
|
31
|
+
const referencedName = schemaNameFromRef(part.$ref);
|
|
32
|
+
return referencedName ? isNamedInterface(referencedName) : false;
|
|
33
|
+
});
|
|
34
|
+
return hasOnlyObjectParts && (declaresObject || schema.allOf.length > 0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isNamedInterface(schemaName) {
|
|
38
|
+
if (results.has(schemaName)) return results.get(schemaName);
|
|
39
|
+
if (visiting.has(schemaName)) return true;
|
|
40
|
+
|
|
41
|
+
visiting.add(schemaName);
|
|
42
|
+
const result = isInterfaceSchema(schemas[schemaName]);
|
|
43
|
+
visiting.delete(schemaName);
|
|
44
|
+
results.set(schemaName, result);
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return new Set(
|
|
49
|
+
schemaNames
|
|
50
|
+
.filter(isNamedInterface)
|
|
51
|
+
.map((schemaName) => typeNames.get(schemaName))
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Flattens object-only allOf parts and keeps component references as interface
|
|
58
|
+
* inheritance. Unsafe or conflicting compositions fall back to a type alias.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveInterfaceSchema(raw, currentTypeName, typeNames, interfaceTypeNames) {
|
|
61
|
+
if (!raw?.allOf) return {schema: raw, extendsTypes: [], compatible: true};
|
|
62
|
+
|
|
63
|
+
const merged = mergeAllOf(raw.allOf);
|
|
64
|
+
if (merged) {
|
|
65
|
+
const ownProperties = raw.properties ?? {};
|
|
66
|
+
if (hasPropertyConflict(merged.properties, ownProperties)) {
|
|
67
|
+
return {schema: raw, extendsTypes: [], compatible: false};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
schema: {
|
|
72
|
+
...merged,
|
|
73
|
+
properties: {...ownProperties, ...merged.properties},
|
|
74
|
+
required: [...new Set([...(raw.required ?? []), ...merged.required])],
|
|
75
|
+
},
|
|
76
|
+
extendsTypes: [],
|
|
77
|
+
compatible: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const properties = {...(raw.properties ?? {})};
|
|
82
|
+
const required = new Set(raw.required ?? []);
|
|
83
|
+
const inheritedTypes = new Set();
|
|
84
|
+
|
|
85
|
+
for (const part of raw.allOf) {
|
|
86
|
+
if (part?.$ref) {
|
|
87
|
+
const referencedSchema = schemaNameFromRef(part.$ref);
|
|
88
|
+
if (!referencedSchema || /^Hydra/i.test(referencedSchema)) continue;
|
|
89
|
+
|
|
90
|
+
const referencedType = typeNames.get(referencedSchema) ?? referencedSchema;
|
|
91
|
+
if (referencedType === currentTypeName) continue;
|
|
92
|
+
if (!interfaceTypeNames.has(referencedType)) {
|
|
93
|
+
return {schema: raw, extendsTypes: [], compatible: false};
|
|
94
|
+
}
|
|
95
|
+
inheritedTypes.add(referencedType);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (part?.type !== 'object' && !isObject(part?.properties)) {
|
|
100
|
+
return {schema: raw, extendsTypes: [], compatible: false};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const partProperties = part.properties ?? {};
|
|
104
|
+
if (hasPropertyConflict(properties, partProperties)) {
|
|
105
|
+
return {schema: raw, extendsTypes: [], compatible: false};
|
|
106
|
+
}
|
|
107
|
+
Object.assign(properties, partProperties);
|
|
108
|
+
for (const property of part.required ?? []) required.add(property);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
schema: {type: 'object', properties, required: [...required]},
|
|
113
|
+
extendsTypes: [...inheritedTypes].sort((left, right) => left.localeCompare(right)),
|
|
114
|
+
compatible: true,
|
|
115
|
+
};
|
|
116
|
+
}
|