@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.
Files changed (91) hide show
  1. package/README.md +19 -2
  2. package/cli.js +21 -37
  3. package/package.json +3 -3
  4. package/src/README.md +9 -0
  5. package/{tools/app → src/application}/bridge-workflow.js +15 -13
  6. package/src/application/generate-bridge.js +50 -0
  7. package/{tools/app → src/application}/run-build.js +1 -1
  8. package/{tools/app → src/application}/run-dev.js +30 -36
  9. package/{tools/app → src/application}/run-generate.js +3 -3
  10. package/{tools → src}/domain/bridge-options.js +8 -21
  11. package/src/domain/list-values.js +6 -0
  12. package/src/generators/bridge/write-bridge-files.js +100 -0
  13. package/src/generators/models/interface-schema.js +116 -0
  14. package/src/generators/models/model-selection.js +148 -0
  15. package/src/generators/models/oas-contract.js +170 -0
  16. package/{tools/generator → src/generators}/models/oas-schema-name.js +6 -15
  17. package/src/generators/models/openapi-to-models.js +136 -0
  18. package/{tools/generator → src/generators}/models/schema-utils.js +27 -3
  19. package/src/generators/models/templates/type-alias.hbs +2 -0
  20. package/src/generators/models/type-expression.js +65 -0
  21. package/src/generators/models/type-resolver.js +240 -0
  22. package/src/generators/models/utils.js +89 -0
  23. package/src/generators/models/write-model-files.js +55 -0
  24. package/src/infrastructure/dev-project.js +48 -0
  25. package/{tools/infra → src/infrastructure}/exec.js +3 -2
  26. package/src/infrastructure/local-package-install.js +104 -0
  27. package/src/infrastructure/spec-loader.js +65 -0
  28. package/{tools/infra → src/infrastructure}/standalone-workspace.js +31 -29
  29. package/templates/{_lib_template → angular-bridge}/README.md +9 -0
  30. package/templates/angular-bridge/src/lib/bridge/rest/api-platform.adapter.ts +82 -0
  31. package/templates/angular-bridge/src/lib/bridge/rest/http-request.options.ts +30 -0
  32. package/templates/{_lib_template → angular-bridge}/src/lib/bridge/rest/query-builder.ts +10 -10
  33. package/templates/angular-bridge/src/lib/bridge/sse/mercure-connections.ts +255 -0
  34. package/templates/angular-bridge/src/lib/bridge/sse/mercure-payload.ts +82 -0
  35. package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.builder.ts +39 -0
  36. package/templates/angular-bridge/src/lib/bridge/sse/mercure.adapter.ts +358 -0
  37. package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/ref-count-topic.registry.ts +3 -12
  38. package/templates/angular-bridge/src/lib/bridge/sse/subscription-claims.ts +76 -0
  39. package/templates/angular-bridge/src/lib/bridge-options.ts +59 -0
  40. package/templates/{_lib_template → angular-bridge}/src/lib/facades/bridge.facade.ts +50 -51
  41. package/templates/{_lib_template → angular-bridge}/src/lib/facades/facade.factory.ts +0 -1
  42. package/templates/angular-bridge/src/lib/facades/facade.interface.ts +13 -0
  43. package/templates/{_lib_template → angular-bridge}/src/lib/facades/resource.facade.ts +24 -31
  44. package/templates/angular-bridge/src/lib/interceptors/bearer-auth.interceptor.ts +33 -0
  45. package/templates/angular-bridge/src/lib/interceptors/bridge-defaults.interceptor.ts +65 -0
  46. package/templates/angular-bridge/src/lib/interceptors/content-type.interceptor.ts +37 -0
  47. package/templates/angular-bridge/src/lib/interceptors/scoped.interceptor.ts +9 -0
  48. package/templates/angular-bridge/src/lib/interceptors/singleflight.interceptor.ts +46 -0
  49. package/templates/{_lib_template → angular-bridge}/src/lib/ports/realtime.port.ts +6 -2
  50. package/templates/{_lib_template → angular-bridge}/src/lib/ports/resource-repository.port.ts +7 -7
  51. package/templates/angular-bridge/src/lib/provide-bridge.ts +98 -0
  52. package/templates/angular-bridge/src/lib/utils/iri.ts +6 -0
  53. package/templates/angular-bridge/src/lib/utils/request-url.ts +31 -0
  54. package/templates/{_lib_template → angular-bridge}/src/public-api.ts +2 -0
  55. package/templates/_lib_template/src/lib/bridge/rest/api-platform.adapter.ts +0 -84
  56. package/templates/_lib_template/src/lib/bridge/rest/http-request.options.ts +0 -21
  57. package/templates/_lib_template/src/lib/bridge/sse/mercure-url.builder.ts +0 -17
  58. package/templates/_lib_template/src/lib/bridge/sse/mercure.adapter.ts +0 -806
  59. package/templates/_lib_template/src/lib/facades/facade.interface.ts +0 -31
  60. package/templates/_lib_template/src/lib/interceptors/bridge-defaults.interceptor.ts +0 -53
  61. package/templates/_lib_template/src/lib/interceptors/content-type.interceptor.ts +0 -49
  62. package/templates/_lib_template/src/lib/interceptors/singleflight.interceptor.ts +0 -55
  63. package/templates/_lib_template/src/lib/provide-bridge.ts +0 -186
  64. package/tools/build.js +0 -1
  65. package/tools/dev.js +0 -1
  66. package/tools/domain/paths.js +0 -10
  67. package/tools/generate.js +0 -1
  68. package/tools/generator/models/oas-contract.js +0 -159
  69. package/tools/generator/models/openapi-to-models.js +0 -240
  70. package/tools/generator/models/type-resolver.js +0 -107
  71. package/tools/generator/models/utils.js +0 -62
  72. package/tools/infra/dev-support.js +0 -189
  73. package/tools/infra/generate/bridge.js +0 -212
  74. package/tools/infra/spec-loader.js +0 -59
  75. /package/{tools → src}/domain/lib-name.js +0 -0
  76. /package/{tools/generator → src/generators}/models/handlebars.js +0 -0
  77. /package/{tools/generator → src/generators}/models/templates/index.hbs +0 -0
  78. /package/{tools/generator → src/generators}/models/templates/model.hbs +0 -0
  79. /package/{tools/infra → src/infrastructure}/json.js +0 -0
  80. /package/{tools/infra → src/infrastructure}/logger.js +0 -0
  81. /package/templates/{_lib_template → angular-bridge}/ng-package.json +0 -0
  82. /package/templates/{_lib_template → angular-bridge}/package.json +0 -0
  83. /package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/eventsource-wrapper.ts +0 -0
  84. /package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/mercure-topic.mapper.ts +0 -0
  85. /package/templates/{_lib_template → angular-bridge}/src/lib/bridge.types.ts +0 -0
  86. /package/templates/{_lib_template → angular-bridge}/src/lib/interceptors/bridge-debug.interceptor.ts +0 -0
  87. /package/templates/{_lib_template → angular-bridge}/src/lib/tokens.ts +0 -0
  88. /package/templates/{_lib_template → angular-bridge}/src/lib/utils/url.ts +0 -0
  89. /package/templates/{_lib_template → angular-bridge}/src/models/index.ts +0 -0
  90. /package/templates/{_lib_template → angular-bridge}/tsconfig.lib.json +0 -0
  91. /package/templates/{_lib_template → angular-bridge}/tsconfig.lib.prod.json +0 -0
@@ -1,240 +0,0 @@
1
- import { sanitizeTypeName, quoteKeyIfNeeded } from './utils.js';
2
- import { mergeAllOf, isObject, schemaNameFromRef } from './schema-utils.js';
3
- import { tsTypeOf, withNullable } from './type-resolver.js';
4
- import { canonicalizeMediaType, collectUsedSchemasForMediaType, expandWithSchemaDependencies } from './oas-contract.js';
5
- import { baseTsNameFromBasePath, formatSuffixToken, groupTsNameFromGroupPath, parseOpenApiSchemaName } from './oas-schema-name.js';
6
-
7
- /**
8
- * @typedef {{
9
- * requiredMode?: 'all'|'spec',
10
- * formats?: string[],
11
- * includeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
12
- * excludeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
13
- * }} BuildOptions
14
- */
15
-
16
- /**
17
- * @typedef {{
18
- * name: string,
19
- * tsKey: string,
20
- * type: string,
21
- * optional: boolean,
22
- * }} ModelProp
23
- */
24
-
25
- /**
26
- * @typedef {{
27
- * name: string,
28
- * props: ModelProp[],
29
- * imports: string[],
30
- * extendsTypes: string[],
31
- * extendsItem?: boolean,
32
- * }} ModelDefinition
33
- */
34
-
35
- function ensureNullable(ts) {
36
- return /\|\s*null\b/.test(ts) ? ts : `${ts} | null`;
37
- }
38
-
39
- function normalizeRequiredMode(value) {
40
- const v = String(value ?? '').trim().toLowerCase();
41
- if (!v) return 'all';
42
- if (v === 'spec') return 'spec';
43
- if (v === 'all') return 'all';
44
- return 'all';
45
- }
46
-
47
- /**
48
- * Construit les définitions de modèles à partir d’une spec OpenAPI (components.schemas)
49
- * @param {any} spec
50
- * @param {BuildOptions} [options]
51
- * @returns {{ models: ModelDefinition[] }}
52
- */
53
- export function buildModelsFromOpenAPI(spec, options) {
54
- const opts = { requiredMode: 'all', ...(options || {}) };
55
- const schemas = spec?.components?.schemas || {};
56
- const formatsOpt = Array.isArray(opts.formats) ? opts.formats : [];
57
- const formats = formatsOpt.length > 0 ? formatsOpt : ['application/ld+json'];
58
- const requiredMode = normalizeRequiredMode(opts.requiredMode);
59
-
60
- /** @type {ModelDefinition[]} */
61
- const models = [];
62
-
63
- // Contract-driven (formats): keep only schemas used by selected media types in real endpoints.
64
- const normalizedFormats = [];
65
- for (const f of formats) {
66
- const canon = canonicalizeMediaType(f);
67
- if (!canon) continue;
68
- if (!normalizedFormats.includes(canon)) normalizedFormats.push(canon);
69
- }
70
- if (normalizedFormats.length === 0) normalizedFormats.push('application/ld+json');
71
-
72
- /** @type {Map<string, Set<string>>} */
73
- const usedByFormat = new Map(); // formatLower -> Set<schemaName>
74
-
75
- for (const fmt of normalizedFormats) {
76
- usedByFormat.set(fmt, collectUsedSchemasForMediaType(spec, { mediaType: fmt }));
77
- }
78
-
79
- /** @type {Map<string, Set<string>>} */
80
- const schemaToFormats = new Map();
81
- for (const [fmt, names] of usedByFormat.entries()) {
82
- for (const name of names) {
83
- if (!name) continue;
84
- const set = schemaToFormats.get(name) || new Set();
85
- set.add(fmt);
86
- schemaToFormats.set(name, set);
87
- }
88
- }
89
-
90
- const include = Array.isArray(opts.includeSchemaNames) ? opts.includeSchemaNames : [];
91
- const exclude = Array.isArray(opts.excludeSchemaNames) ? opts.excludeSchemaNames : [];
92
- const matches = (val, rule) => {
93
- if (rule instanceof RegExp) return rule.test(val);
94
- if (typeof rule === 'function') return !!rule(val);
95
- if (typeof rule === 'string' && rule.length) return val.includes(rule);
96
- return false;
97
- };
98
-
99
- const allUsed = [...schemaToFormats.keys()]
100
- .filter(Boolean)
101
- .filter((n) => !/^Hydra/i.test(n))
102
- .sort((a, b) => a.localeCompare(b))
103
- .filter((originalName) => {
104
- const parsed = parseOpenApiSchemaName(originalName);
105
- const canonical = parsed.groupPath ? `${parsed.basePath}-${parsed.groupPath}` : parsed.basePath;
106
- const fulls = [originalName, canonical];
107
-
108
- const hasInclude = include.length > 0;
109
- const included = !hasInclude || include.some((r) => fulls.some((v) => matches(v, r)));
110
- const excluded = exclude.some((r) => fulls.some((v) => matches(v, r)));
111
- return included && !excluded;
112
- });
113
-
114
- // Ensure filters never break compilation: always keep transitive deps of selected schemas.
115
- const candidatesSet = include.length > 0 || exclude.length > 0 ? expandWithSchemaDependencies(allUsed, schemas) : new Set(allUsed);
116
-
117
- const candidates = [...candidatesSet]
118
- .filter((n) => !!n)
119
- .filter((n) => !/^Hydra/i.test(n))
120
- .sort((a, b) => a.localeCompare(b));
121
-
122
- // Assign a preferred format to each schema (used for naming + JSON-LD behavior).
123
- const formatIndex = new Map(normalizedFormats.map((f, i) => [f, i]));
124
- const schemaEntries = candidates.map((schemaName) => {
125
- const fmts = schemaToFormats.get(schemaName) || new Set();
126
- const best = [...fmts].sort((a, b) => (formatIndex.get(a) ?? 999) - (formatIndex.get(b) ?? 999))[0] || normalizedFormats[0];
127
- return { schemaName, preferredFormat: best, preferredFormatIndex: formatIndex.get(best) ?? 0 };
128
- });
129
- const preferredFormatBySchema = new Map(schemaEntries.map((e) => [e.schemaName, e.preferredFormat]));
130
-
131
- // Compute desired TS names (format-agnostic), then disambiguate collisions by preferred format.
132
- const byDesired = new Map(); // desired -> entries[]
133
- for (const e of schemaEntries) {
134
- const parsed = parseOpenApiSchemaName(e.schemaName);
135
- if (parsed.isMergePatch) continue; // never generate patch models
136
- const baseTs = baseTsNameFromBasePath(parsed.basePath);
137
- const groupTs = groupTsNameFromGroupPath(parsed.groupPath);
138
- const desired = `${baseTs}${groupTs}` || 'Model';
139
- const list = byDesired.get(desired) || [];
140
- list.push({ ...e, desired });
141
- byDesired.set(desired, list);
142
- }
143
-
144
- // Assign unique TS names and build schemaName -> tsName map.
145
- const usedTsNames = new Set();
146
- const nameMap = new Map();
147
- for (const [desired, list] of [...byDesired.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
148
- list.sort((a, b) => {
149
- const da = a.preferredFormatIndex - b.preferredFormatIndex;
150
- if (da !== 0) return da;
151
- return a.schemaName.localeCompare(b.schemaName);
152
- });
153
-
154
- for (let i = 0; i < list.length; i++) {
155
- const entry = list[i];
156
- const withFmt = i === 0 ? desired : `${desired}${formatSuffixToken(entry.preferredFormat)}`;
157
- const tsName = sanitizeTypeName(withFmt, usedTsNames);
158
- nameMap.set(entry.schemaName, tsName);
159
- }
160
- }
161
- const allSanitizedNames = new Set([...nameMap.values()]);
162
-
163
- for (const schemaName of candidates) {
164
- const sanitized = nameMap.get(schemaName);
165
- if (!sanitized) continue;
166
-
167
- const raw = schemas[schemaName];
168
- if (!raw) continue;
169
-
170
- const preferredFormat = preferredFormatBySchema.get(schemaName) ?? normalizedFormats[0];
171
- const isJsonLd = canonicalizeMediaType(preferredFormat) === 'application/ld+json';
172
- const forceNullable = requiredMode !== 'spec';
173
-
174
- /** @type {string[]} */
175
- const extendsTypes = [];
176
- const deps = new Set();
177
-
178
- let effective = raw;
179
- if (raw?.allOf) {
180
- const merged = mergeAllOf(raw.allOf);
181
- if (merged) {
182
- effective = merged;
183
- } else {
184
- // Non-flattenable allOf: keep inline object props + other model refs as extends.
185
- const props = {};
186
- const required = new Set();
187
- const ext = new Set();
188
-
189
- for (const part of raw.allOf) {
190
- if (part?.$ref) {
191
- const orig = schemaNameFromRef(part.$ref);
192
- if (!orig) continue;
193
- if (/^Hydra/i.test(orig)) continue;
194
- const refName = orig ? nameMap.get(orig) || orig : undefined;
195
- if (refName && refName !== sanitized && allSanitizedNames.has(refName)) ext.add(refName);
196
- continue;
197
- }
198
- const isObj = part?.type === 'object' || isObject(part?.properties);
199
- if (!isObj) continue;
200
- Object.assign(props, part.properties || {});
201
- for (const r of part.required || []) required.add(r);
202
- }
203
-
204
- extendsTypes.push(...[...ext].sort((a, b) => a.localeCompare(b)));
205
- effective = { type: 'object', properties: props, required: [...required] };
206
- }
207
- }
208
-
209
- const isObjectLike = effective?.type === 'object' || isObject(effective?.properties) || extendsTypes.length > 0;
210
- if (!isObjectLike) continue;
211
-
212
- for (const e of extendsTypes) deps.add(e);
213
-
214
- const required = new Set(effective.required || []);
215
- const props = Object.entries(effective.properties || {})
216
- .filter(([propName]) => {
217
- if (!isJsonLd) return true;
218
- return propName !== '@id' && propName !== '@type' && propName !== '@context';
219
- })
220
- .map(([propName, prop]) => {
221
- const baseType = tsTypeOf(prop, deps, nameMap, { requiredMode });
222
- const type = forceNullable ? ensureNullable(baseType) : withNullable(baseType, prop);
223
- return {
224
- name: propName,
225
- tsKey: quoteKeyIfNeeded(propName),
226
- type,
227
- optional: requiredMode === 'spec' ? !required.has(propName) : true,
228
- };
229
- });
230
-
231
- const imports = [...deps]
232
- .filter((n) => n !== sanitized)
233
- .filter((n) => allSanitizedNames.has(n))
234
- .sort((a, b) => a.localeCompare(b));
235
-
236
- models.push({ name: sanitized, props, imports, extendsTypes, extendsItem: isJsonLd });
237
- }
238
-
239
- return { models };
240
- }
@@ -1,107 +0,0 @@
1
- import { schemaNameFromRef } from './schema-utils.js';
2
- import { quoteKeyIfNeeded } from './utils.js';
3
-
4
- /**
5
- * Ajoute | null à un type TS si le schéma est nullable (compat OAS 3.0),
6
- * sauf si 'null' est déjà présent dans l'union.
7
- * @param {string} ts
8
- * @param {any} schema
9
- */
10
- export function withNullable(ts, schema) {
11
- return schema?.nullable && !/\|\s*null\b/.test(ts) ? `${ts} | null` : ts;
12
- }
13
-
14
- /**
15
- * Résout un schéma OpenAPI vers un type TypeScript, en collectant les dépendances ($ref)
16
- * @param {any} schema
17
- * @param {Set<string>} deps
18
- * @param {Map<string,string>} nameMap
19
- * @param {{ requiredMode?: 'all'|'spec' }} [cfg]
20
- * @returns {string}
21
- */
22
- export function tsTypeOf(schema, deps, nameMap, cfg) {
23
- if (!schema) return 'any';
24
- const requiredMode = cfg?.requiredMode ?? 'all';
25
- const forceNullable = requiredMode !== 'spec';
26
-
27
- if (schema.$ref) {
28
- const orig = schemaNameFromRef(schema.$ref);
29
- if (!orig) return 'any';
30
- if (/jsonMergePatch/i.test(orig)) return 'any';
31
- if (/^Hydra/i.test(orig)) return 'any';
32
- const name = nameMap.get(orig) || orig;
33
- deps.add(name);
34
- return name;
35
- }
36
-
37
- // Enum prioritaires: union littérale (supporte null)
38
- if (schema.enum) {
39
- const lit = schema.enum.map((v) => {
40
- if (v === null) return 'null';
41
- if (typeof v === 'string') return `'${v.replace(/'/g, "\\'")}'`;
42
- if (typeof v === 'number' || typeof v === 'bigint') return String(v);
43
- if (typeof v === 'boolean') return v ? 'true' : 'false';
44
- return 'any';
45
- });
46
- return [...new Set(lit)].join(' | ');
47
- }
48
-
49
- // OpenAPI 3.1: null explicite et tableau de types (union)
50
- if (schema.type === 'null') return 'null';
51
- if (Array.isArray(schema.type)) {
52
- const parts = new Set();
53
- for (const t of schema.type) {
54
- if (t === 'null') parts.add('null');
55
- else if (t === 'string') parts.add('string');
56
- else if (t === 'integer' || t === 'number') parts.add('number');
57
- else if (t === 'boolean') parts.add('boolean');
58
- else if (t === 'array') parts.add(tsTypeOf({ type: 'array', items: schema.items }, deps, nameMap, cfg));
59
- else if (t === 'object') {
60
- const objTs = tsTypeOf(
61
- { type: 'object', properties: schema.properties, required: schema.required, additionalProperties: schema.additionalProperties },
62
- deps,
63
- nameMap,
64
- cfg
65
- );
66
- parts.add(objTs);
67
- } else parts.add('any');
68
- }
69
- return [...parts].join(' | ');
70
- }
71
-
72
- if (schema.allOf) {
73
- // Aplatir au niveau buildModelsFromOpenAPI; ici on combine si nécessaire
74
- return schema.allOf.map((s) => tsTypeOf(s, deps, nameMap, cfg)).join(' & ');
75
- }
76
- if (schema.oneOf) return schema.oneOf.map((s) => tsTypeOf(s, deps, nameMap, cfg)).join(' | ');
77
- if (schema.anyOf) return schema.anyOf.map((s) => tsTypeOf(s, deps, nameMap, cfg)).join(' | ');
78
-
79
- if (schema.type === 'array') {
80
- const it = tsTypeOf(schema.items || {}, deps, nameMap, cfg);
81
- return `${it}[]`;
82
- }
83
-
84
- if (schema.type === 'object' || (schema.properties || schema.additionalProperties)) {
85
- const props = schema.properties || {};
86
- const entries = Object.entries(props);
87
- const inline = entries.map(([k, v]) => {
88
- const t0 = tsTypeOf(v, deps, nameMap, cfg);
89
- const opt = requiredMode === 'spec' ? ((schema.required || []).includes(k) ? '' : '?') : '?';
90
- const wantsNull = forceNullable || v?.nullable;
91
- const nullable = wantsNull && !/\|\s*null\b/.test(t0) ? ' | null' : '';
92
- const key = quoteKeyIfNeeded(k);
93
- return `${key}${opt}: ${t0}${nullable};`;
94
- });
95
- if (schema.additionalProperties) {
96
- const at = tsTypeOf(schema.additionalProperties, deps, nameMap, cfg);
97
- inline.push(`[key: string]: ${at};`);
98
- }
99
- return `{ ${inline.join(' ')} }`;
100
- }
101
-
102
- if (schema.type === 'string') return 'string';
103
- if (schema.type === 'integer' || schema.type === 'number') return 'number';
104
- if (schema.type === 'boolean') return 'boolean';
105
-
106
- return 'any';
107
- }
@@ -1,62 +0,0 @@
1
- import fs from 'node:fs/promises';
2
-
3
- /**
4
- * Supprime et recrée un dossier (idempotent)
5
- * @param {string} dir
6
- */
7
- export async function ensureCleanDir(dir) {
8
- await fs.rm(dir, { recursive: true, force: true });
9
- await fs.mkdir(dir, { recursive: true });
10
- }
11
-
12
- const RESERVED = new Set([
13
- 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield', 'let', 'implements', 'interface', 'package', 'private', 'protected', 'public', 'static', 'await', 'abstract', 'boolean', 'byte', 'char', 'double', 'final', 'float', 'goto', 'int', 'long', 'native', 'short', 'synchronized', 'throws', 'transient', 'volatile'
14
- ]);
15
-
16
- /**
17
- * Construit un PascalCase à partir d’une liste de tokens
18
- * @param {string[]} tokens
19
- */
20
- export function pascalCase(tokens) {
21
- return tokens
22
- .filter(Boolean)
23
- .map(t => t.replace(/^[^A-Za-z0-9]+|[^A-Za-z0-9]+$/g, ''))
24
- .filter(Boolean)
25
- .map(t => t.charAt(0).toUpperCase() + t.slice(1))
26
- .join('');
27
- }
28
-
29
- /**
30
- * Nettoie et désambiguïse un nom de type TypeScript, en évitant les mots réservés
31
- * et collisions via un suffixe numérique.
32
- * @param {string} original
33
- * @param {Set<string>} used
34
- */
35
- export function sanitizeTypeName(original, used) {
36
- const tokens = String(original).split(/[^A-Za-z0-9]+/);
37
- let name = pascalCase(tokens) || 'Model';
38
- if (/^[0-9]/.test(name)) name = '_' + name;
39
- if (RESERVED.has(name)) name = name + 'Model';
40
- let unique = name, i = 2;
41
- while (used.has(unique)) {
42
- unique = name + i++;
43
- }
44
- used.add(unique);
45
- return unique;
46
- }
47
-
48
- /**
49
- * Vérifie qu’une clé est un identifiant TS valide ET non réservé
50
- * @param {string} k
51
- */
52
- export function isValidTsIdentifier(k) {
53
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(k) && !RESERVED.has(k);
54
- }
55
-
56
- /**
57
- * Ajoute des quotes sur une clé si nécessaire (clé non valide en TS)
58
- * @param {string} k
59
- */
60
- export function quoteKeyIfNeeded(k) {
61
- return isValidTsIdentifier(k) ? k : `'${k.replace(/'/g, "\\'")}'`;
62
- }
@@ -1,189 +0,0 @@
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
- await invalidateAngularViteDependencyCache({ appRoot, packageName, log });
112
-
113
- return pkgDir;
114
- }
115
-
116
- async function invalidateAngularViteDependencyCache({ appRoot, packageName, log }) {
117
- const viteDepsRoots = await findAngularViteDepsRoots(appRoot);
118
- if (viteDepsRoots.length === 0) return;
119
-
120
- const cacheBase = packageNameToViteCacheBase(packageName);
121
- let removed = 0;
122
-
123
- for (const depsRoot of viteDepsRoots) {
124
- const files = [
125
- `${cacheBase}.js`,
126
- `${cacheBase}.js.map`,
127
- `${cacheBase}.mjs`,
128
- `${cacheBase}.mjs.map`,
129
- ];
130
-
131
- for (const fileName of files) {
132
- const p = path.join(depsRoot, fileName);
133
- try {
134
- await fs.rm(p, { force: true });
135
- removed += 1;
136
- } catch {
137
- // ignore
138
- }
139
- }
140
-
141
- // Metadata keeps dependency hashes/entries; remove it to force a clean re-optimization.
142
- try {
143
- await fs.rm(path.join(depsRoot, '_metadata.json'), { force: true });
144
- removed += 1;
145
- } catch {
146
- // ignore
147
- }
148
- }
149
-
150
- if (removed > 0) {
151
- log?.info?.(`cache Angular/Vite invalidé (${removed} fichier(s)) pour ${packageName}`);
152
- }
153
- }
154
-
155
- function packageNameToViteCacheBase(packageName) {
156
- const normalized = String(packageName || '').trim();
157
- // Example: "@obsidiane/bridge-sandbox" -> "@obsidiane_bridge-sandbox"
158
- return normalized.replace('/', '_');
159
- }
160
-
161
- async function findAngularViteDepsRoots(appRoot) {
162
- const angularCacheRoot = path.join(appRoot, '.angular', 'cache');
163
- const roots = [];
164
-
165
- async function walk(dir, depth) {
166
- if (depth > 8) return;
167
-
168
- let entries;
169
- try {
170
- entries = await fs.readdir(dir, { withFileTypes: true });
171
- } catch {
172
- return;
173
- }
174
-
175
- const hasDeps = entries.some((e) => e.isDirectory() && e.name === 'deps');
176
- if (hasDeps && path.basename(dir) === 'vite') {
177
- roots.push(path.join(dir, 'deps'));
178
- }
179
-
180
- for (const entry of entries) {
181
- if (!entry.isDirectory()) continue;
182
- if (entry.name === 'node_modules') continue;
183
- await walk(path.join(dir, entry.name), depth + 1);
184
- }
185
- }
186
-
187
- await walk(angularCacheRoot, 0);
188
- return roots;
189
- }