@obsidiane/meridiane 0.0.7 → 0.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 +14 -66
- package/cli.js +59 -65
- package/package.json +3 -5
- package/templates/_lib_template/package.json +2 -1
- package/templates/_lib_template/src/models/index.ts +0 -1
- package/tools/build.js +104 -0
- package/tools/core/generate.js +152 -0
- package/tools/core/json.js +71 -0
- package/tools/core/lib-name.js +15 -0
- package/tools/core/logger.js +51 -0
- package/tools/core/paths.js +17 -0
- package/tools/core/spec.js +59 -0
- package/tools/core/standalone.js +117 -0
- package/tools/dev.js +218 -0
- package/tools/generator/models/handlebars.js +8 -2
- package/tools/generator/models/naming.js +21 -15
- package/tools/generator/models/openapi-to-models.js +65 -39
- package/tools/generator/models/schema-utils.js +42 -83
- package/tools/generator/models/templates/model.hbs +3 -3
- package/tools/generator/models/utils.js +1 -8
- package/models.config.example.js +0 -39
- package/templates/_lib_template/tsconfig.spec.json +0 -14
- package/tools/dev-bridge.js +0 -138
- package/tools/generate-lib.js +0 -151
- package/tools/generate-models.js +0 -175
- package/tools/init.js +0 -148
- package/tools/sandbox-bridge.js +0 -77
- package/tools/utils/cli-args.js +0 -41
- package/tools/utils/dotenv.js +0 -55
|
@@ -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
|
+
|
package/tools/dev.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
import { deriveLibName } from './core/lib-name.js';
|
|
8
|
+
import { normalizePreset, 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 preset = normalizePreset(useSandboxDefaults ? (opts.preset ?? 'native') : opts.preset);
|
|
143
|
+
const include = splitListArgs(opts.include);
|
|
144
|
+
const exclude = splitListArgs(opts.exclude);
|
|
145
|
+
|
|
146
|
+
const version = '0.0.0-dev';
|
|
147
|
+
const requiredMode = 'all-optional';
|
|
148
|
+
const specSource = useSandboxDefaults ? (opts.spec ?? 'http://localhost:8000/api/docs.json') : opts.spec;
|
|
149
|
+
|
|
150
|
+
log.title('dev — build standalone + install local');
|
|
151
|
+
log.info(`app: ${appRoot}`);
|
|
152
|
+
log.info(`package: ${effectivePackageName}@${version}`);
|
|
153
|
+
log.info(`lib: ${libName}`);
|
|
154
|
+
if (useSandboxDefaults) log.info('defaults sandbox appliqués');
|
|
155
|
+
if (noModels) log.info('models: désactivés (--no-models)');
|
|
156
|
+
else log.info(`spec: ${specSource} | preset: ${preset}`);
|
|
157
|
+
|
|
158
|
+
const { workspaceRoot, distRoot, npmCacheDir, requireFromWs } = await ensureStandaloneWorkspace({ repoRoot: appRoot, log });
|
|
159
|
+
log.info(`workspace: ${workspaceRoot}`);
|
|
160
|
+
|
|
161
|
+
let spec;
|
|
162
|
+
if (!noModels) {
|
|
163
|
+
log.step('lecture de la spec OpenAPI');
|
|
164
|
+
spec = await readOpenApiSpec(specSource);
|
|
165
|
+
log.success('spec chargée');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await ensureToolchainInstalled({ workspaceRoot, requireFromWs, npmCacheDir, log });
|
|
169
|
+
|
|
170
|
+
await generateBridgeWorkspace({
|
|
171
|
+
cwd: workspaceRoot,
|
|
172
|
+
libName,
|
|
173
|
+
packageName: effectivePackageName,
|
|
174
|
+
version,
|
|
175
|
+
noModels,
|
|
176
|
+
spec,
|
|
177
|
+
requiredMode,
|
|
178
|
+
preset,
|
|
179
|
+
include,
|
|
180
|
+
exclude,
|
|
181
|
+
debug,
|
|
182
|
+
log,
|
|
183
|
+
distRoot,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const ngPackageJsonPath = path.join(workspaceRoot, 'projects', libName, 'ng-package.json');
|
|
187
|
+
const tsconfigDevPath = path.join(workspaceRoot, 'projects', libName, 'tsconfig.lib.json');
|
|
188
|
+
|
|
189
|
+
log.step('ng-packagr');
|
|
190
|
+
const ngPackagrPath = requireFromWs.resolve('ng-packagr/src/cli/main.js');
|
|
191
|
+
const packagrCode = await run(process.execPath, [ngPackagrPath, '-p', ngPackageJsonPath, '-c', tsconfigDevPath], {
|
|
192
|
+
cwd: workspaceRoot,
|
|
193
|
+
});
|
|
194
|
+
if (packagrCode !== 0) process.exit(packagrCode);
|
|
195
|
+
|
|
196
|
+
const distDir = path.join(distRoot, libName);
|
|
197
|
+
log.step(`npm pack (${path.relative(appRoot, distDir)})`);
|
|
198
|
+
const packCode = await run('npm', ['pack'], {
|
|
199
|
+
cwd: distDir,
|
|
200
|
+
env: {
|
|
201
|
+
...process.env,
|
|
202
|
+
npm_config_cache: npmCacheDir,
|
|
203
|
+
npm_config_update_notifier: 'false',
|
|
204
|
+
npm_config_fund: 'false',
|
|
205
|
+
npm_config_audit: 'false',
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
if (packCode !== 0) process.exit(packCode);
|
|
209
|
+
|
|
210
|
+
await findLatestTgz(distDir); // keep producing the tgz in dist for inspection/use
|
|
211
|
+
await installPackageFromDist({ packageName: effectivePackageName, distDir, appRoot, log });
|
|
212
|
+
|
|
213
|
+
log.success('bridge installé (node_modules) ✅');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
217
|
+
throw new Error('This module is not meant to be executed directly.');
|
|
218
|
+
}
|
|
@@ -2,13 +2,19 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import Handlebars from 'handlebars';
|
|
4
4
|
|
|
5
|
+
const templateCache = new Map();
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Rend un template Handlebars avec un contexte et écrit le fichier.
|
|
7
9
|
* @param {{ templatePath: string, outPath: string, ctx: any }} params
|
|
8
10
|
*/
|
|
9
11
|
export async function renderTemplateToFile({templatePath, outPath, ctx}) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
let tpl = templateCache.get(templatePath);
|
|
13
|
+
if (!tpl) {
|
|
14
|
+
const tplSrc = await fs.readFile(templatePath, 'utf8');
|
|
15
|
+
tpl = Handlebars.compile(tplSrc, {noEscape: true});
|
|
16
|
+
templateCache.set(templatePath, tpl);
|
|
17
|
+
}
|
|
12
18
|
const out = tpl(ctx);
|
|
13
19
|
await fs.mkdir(path.dirname(outPath), {recursive: true});
|
|
14
20
|
await fs.writeFile(outPath, out, 'utf8');
|
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Construit un label "tokenisé" (sans `.` ni `-`) à partir d'un nom de schéma OpenAPI.
|
|
3
|
+
* Le label est ensuite passé à `sanitizeTypeName()` pour produire un identifiant TS stable.
|
|
4
|
+
*
|
|
4
5
|
* @param {string} original
|
|
6
|
+
* @param {{ preset: 'all'|'native' }} cfg
|
|
5
7
|
*/
|
|
6
|
-
export function
|
|
7
|
-
const
|
|
8
|
-
if (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (groupTokens.length && groupTokens[0].toLowerCase() === String(baseSimple).toLowerCase()) {
|
|
13
|
-
groupTokens.shift();
|
|
14
|
-
}
|
|
15
|
-
const group = groupTokens.join(' ');
|
|
16
|
-
return group ? `${base} ${group}` : base;
|
|
8
|
+
export function schemaLabel(original, cfg) {
|
|
9
|
+
const preset = cfg?.preset ?? 'all';
|
|
10
|
+
if (preset === 'native') {
|
|
11
|
+
// En native, on vise un nom "entity-like" : on ignore le groupe (après `-`) si présent.
|
|
12
|
+
// Les variantes .jsonld/.jsonapi/jsonMergePatch sont censées être filtrées par le preset.
|
|
13
|
+
return String(original).split('-')[0];
|
|
17
14
|
}
|
|
18
|
-
return original.replace(/\.(jsonld|jsonapi)$/ig, '');
|
|
19
|
-
}
|
|
20
15
|
|
|
16
|
+
// En all, on encode tout : `.` et `-` deviennent des séparateurs de tokens.
|
|
17
|
+
const rawTokens = String(original).split(/[.-]/g).filter(Boolean);
|
|
18
|
+
const tokens = rawTokens.map((t) => {
|
|
19
|
+
const lower = t.toLowerCase();
|
|
20
|
+
if (lower === 'jsonld') return 'Jsonld';
|
|
21
|
+
if (lower === 'jsonapi') return 'Jsonapi';
|
|
22
|
+
if (lower === 'jsonmergepatch' || t === 'jsonMergePatch') return 'JsonMergePatch';
|
|
23
|
+
return t;
|
|
24
|
+
});
|
|
25
|
+
return tokens.join(' ');
|
|
26
|
+
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { sanitizeTypeName, quoteKeyIfNeeded } from './utils.js';
|
|
2
|
-
import { filterSchemaNames, applySchemaNameFilters, mergeAllOf, isObject } from './schema-utils.js';
|
|
3
|
-
import {
|
|
2
|
+
import { filterSchemaNames, applySchemaNameFilters, mergeAllOf, isObject, schemaNameFromRef } from './schema-utils.js';
|
|
3
|
+
import { schemaLabel } from './naming.js';
|
|
4
4
|
import { tsTypeOf, withNullable } from './type-resolver.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @typedef {{
|
|
8
8
|
* requiredMode?: 'all-optional'|'spec',
|
|
9
|
-
* preferFlavor?: 'jsonld'|'jsonapi'|'none',
|
|
10
|
-
* hydraBaseRegex?: RegExp|string,
|
|
11
9
|
* preset?: 'all'|'native',
|
|
12
10
|
* includeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
|
|
13
11
|
* excludeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
|
|
@@ -18,40 +16,37 @@ import { tsTypeOf, withNullable } from './type-resolver.js';
|
|
|
18
16
|
* Construit une map originalName -> sanitizedName, en assurant l’unicité
|
|
19
17
|
* @param {string[]} schemaNames
|
|
20
18
|
*/
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const roots = parsed.filter((p) => !p.group);
|
|
29
|
-
const grouped = parsed.filter((p) => !!p.group);
|
|
30
|
-
|
|
31
|
-
// Root sanitized names (to avoid collisions when dropping group)
|
|
32
|
-
const rootSanitized = new Set(roots.map((p) => sanitizeTypeName(p.base, new Set())));
|
|
33
|
-
|
|
34
|
-
// Count base candidates among grouped to see if unique
|
|
35
|
-
const baseCount = new Map();
|
|
36
|
-
for (const p of grouped) {
|
|
37
|
-
const cand = sanitizeTypeName(p.base, new Set());
|
|
38
|
-
baseCount.set(cand, (baseCount.get(cand) || 0) + 1);
|
|
39
|
-
}
|
|
19
|
+
function normalizeNativeBase(original) {
|
|
20
|
+
return String(original)
|
|
21
|
+
.replace(/\.jsonld\b/ig, '')
|
|
22
|
+
.replace(/\.jsonapi\b/ig, '')
|
|
23
|
+
.replace(/\.jsonmergepatch\b/ig, '')
|
|
24
|
+
.split('-')[0];
|
|
25
|
+
}
|
|
40
26
|
|
|
41
|
-
|
|
27
|
+
function buildNameMap(schemaNames, schemas, preset) {
|
|
28
|
+
// Build final map with uniqueness (deterministic order)
|
|
42
29
|
const used = new Set();
|
|
43
30
|
const nameMap = new Map();
|
|
44
|
-
for (const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
31
|
+
for (const original of schemaNames) {
|
|
32
|
+
const label = schemaLabel(original, {preset});
|
|
33
|
+
nameMap.set(original, sanitizeTypeName(label, used));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// In native mode, alias excluded variants to the selected base type
|
|
37
|
+
// so `$ref` doesn't leak "User-user.write" etc into generated types.
|
|
38
|
+
if (preset === 'native') {
|
|
39
|
+
const baseToSanitized = new Map();
|
|
40
|
+
for (const original of schemaNames) {
|
|
41
|
+
baseToSanitized.set(normalizeNativeBase(original), nameMap.get(original));
|
|
42
|
+
}
|
|
43
|
+
for (const original of Object.keys(schemas)) {
|
|
44
|
+
const base = normalizeNativeBase(original);
|
|
45
|
+
const sanitized = baseToSanitized.get(base);
|
|
46
|
+
if (sanitized) nameMap.set(original, sanitized);
|
|
52
47
|
}
|
|
53
|
-
nameMap.set(p.name, sanitizeTypeName(label, used));
|
|
54
48
|
}
|
|
49
|
+
|
|
55
50
|
return nameMap;
|
|
56
51
|
}
|
|
57
52
|
|
|
@@ -65,7 +60,8 @@ export function buildModelsFromOpenAPI(spec, options) {
|
|
|
65
60
|
const opts = { requiredMode: 'all-optional', ...(options || {}) };
|
|
66
61
|
const schemas = spec?.components?.schemas || {};
|
|
67
62
|
const schemaNames = applySchemaNameFilters(filterSchemaNames(schemas, opts), opts);
|
|
68
|
-
const
|
|
63
|
+
const preset = opts.preset ?? 'all';
|
|
64
|
+
const nameMap = buildNameMap(schemaNames, schemas, preset);
|
|
69
65
|
const allSanitizedNames = new Set([...nameMap.values()]);
|
|
70
66
|
|
|
71
67
|
/** @type {ModelDefinition[]} */
|
|
@@ -73,19 +69,49 @@ export function buildModelsFromOpenAPI(spec, options) {
|
|
|
73
69
|
|
|
74
70
|
for (const originalName of schemaNames) {
|
|
75
71
|
const raw = schemas[originalName];
|
|
72
|
+
const sanitized = nameMap.get(originalName);
|
|
73
|
+
if (!sanitized) continue;
|
|
74
|
+
|
|
75
|
+
/** @type {string[]} */
|
|
76
|
+
const extendsTypes = [];
|
|
77
|
+
|
|
76
78
|
let effective = raw;
|
|
77
79
|
if (raw?.allOf) {
|
|
78
|
-
const merged = mergeAllOf(raw.allOf
|
|
79
|
-
if (merged)
|
|
80
|
+
const merged = mergeAllOf(raw.allOf);
|
|
81
|
+
if (merged) {
|
|
82
|
+
effective = merged;
|
|
83
|
+
} else {
|
|
84
|
+
// Non-flattenable allOf (refs, mixed forms): keep inline object props and model refs as extends.
|
|
85
|
+
const props = {};
|
|
86
|
+
const required = new Set();
|
|
87
|
+
const ext = new Set();
|
|
88
|
+
|
|
89
|
+
for (const part of raw.allOf) {
|
|
90
|
+
if (part?.$ref) {
|
|
91
|
+
const orig = schemaNameFromRef(part.$ref);
|
|
92
|
+
const refName = orig ? nameMap.get(orig) : undefined;
|
|
93
|
+
if (refName && refName !== sanitized && allSanitizedNames.has(refName)) ext.add(refName);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const isObj = part?.type === 'object' || isObject(part?.properties);
|
|
97
|
+
if (!isObj) continue;
|
|
98
|
+
Object.assign(props, part.properties || {});
|
|
99
|
+
for (const r of part.required || []) required.add(r);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
extendsTypes.push(...[...ext].sort((a, b) => a.localeCompare(b)));
|
|
103
|
+
effective = { type: 'object', properties: props, required: [...required] };
|
|
104
|
+
}
|
|
80
105
|
}
|
|
81
106
|
|
|
82
|
-
const isObjectLike = effective?.type === 'object' || isObject(effective?.properties);
|
|
107
|
+
const isObjectLike = effective?.type === 'object' || isObject(effective?.properties) || extendsTypes.length > 0;
|
|
83
108
|
if (!isObjectLike) continue;
|
|
84
109
|
|
|
85
|
-
const sanitized = nameMap.get(originalName);
|
|
86
110
|
const required = new Set(effective.required || []);
|
|
87
111
|
const deps = new Set();
|
|
88
112
|
|
|
113
|
+
for (const e of extendsTypes) deps.add(e);
|
|
114
|
+
|
|
89
115
|
const props = Object.entries(effective.properties || {}).map(([propName, prop]) => {
|
|
90
116
|
const baseType = tsTypeOf(prop, deps, nameMap);
|
|
91
117
|
const type = withNullable(baseType, prop);
|
|
@@ -102,7 +128,7 @@ export function buildModelsFromOpenAPI(spec, options) {
|
|
|
102
128
|
.filter((n) => allSanitizedNames.has(n))
|
|
103
129
|
.sort((a, b) => a.localeCompare(b));
|
|
104
130
|
|
|
105
|
-
models.push({name: sanitized, props, imports});
|
|
131
|
+
models.push({name: sanitized, props, imports, extendsTypes});
|
|
106
132
|
}
|
|
107
133
|
|
|
108
134
|
return {models};
|