@cocoar/scenar-tooling 0.1.0-beta.21

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 (51) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +11 -0
  3. package/package.json +15 -0
  4. package/src/index.d.ts +5 -0
  5. package/src/index.js +9 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/check-scenario-metadata-imports.d.ts +8 -0
  8. package/src/lib/check-scenario-metadata-imports.js +11 -0
  9. package/src/lib/check-scenario-metadata-imports.js.map +1 -0
  10. package/src/lib/config/config.fn.d.ts +5 -0
  11. package/src/lib/config/config.fn.js +45 -0
  12. package/src/lib/config/config.fn.js.map +1 -0
  13. package/src/lib/config/config.types.d.ts +17 -0
  14. package/src/lib/config/config.types.js +3 -0
  15. package/src/lib/config/config.types.js.map +1 -0
  16. package/src/lib/config/index.d.ts +2 -0
  17. package/src/lib/config/index.js +6 -0
  18. package/src/lib/config/index.js.map +1 -0
  19. package/src/lib/fs-walk.d.ts +3 -0
  20. package/src/lib/fs-walk.js +23 -0
  21. package/src/lib/fs-walk.js.map +1 -0
  22. package/src/lib/generate-scenario-registry/generate-scenario-registry.fn.d.ts +10 -0
  23. package/src/lib/generate-scenario-registry/generate-scenario-registry.fn.js +699 -0
  24. package/src/lib/generate-scenario-registry/generate-scenario-registry.fn.js.map +1 -0
  25. package/src/lib/generate-scenario-registry/generate-scenario-registry.types.d.ts +15 -0
  26. package/src/lib/generate-scenario-registry/generate-scenario-registry.types.js +3 -0
  27. package/src/lib/generate-scenario-registry/generate-scenario-registry.types.js.map +1 -0
  28. package/src/lib/generate-scenario-registry/index.d.ts +2 -0
  29. package/src/lib/generate-scenario-registry/index.js +6 -0
  30. package/src/lib/generate-scenario-registry/index.js.map +1 -0
  31. package/src/lib/logging/index.d.ts +2 -0
  32. package/src/lib/logging/index.js +6 -0
  33. package/src/lib/logging/index.js.map +1 -0
  34. package/src/lib/logging/logging.fn.d.ts +7 -0
  35. package/src/lib/logging/logging.fn.js +51 -0
  36. package/src/lib/logging/logging.fn.js.map +1 -0
  37. package/src/lib/logging/logging.types.d.ts +8 -0
  38. package/src/lib/logging/logging.types.js +3 -0
  39. package/src/lib/logging/logging.types.js.map +1 -0
  40. package/src/lib/path-utils.d.ts +2 -0
  41. package/src/lib/path-utils.js +20 -0
  42. package/src/lib/path-utils.js.map +1 -0
  43. package/src/lib/scenar-api/index.d.ts +2 -0
  44. package/src/lib/scenar-api/index.js +6 -0
  45. package/src/lib/scenar-api/index.js.map +1 -0
  46. package/src/lib/scenar-api/scenar-api.fn.d.ts +8 -0
  47. package/src/lib/scenar-api/scenar-api.fn.js +52 -0
  48. package/src/lib/scenar-api/scenar-api.fn.js.map +1 -0
  49. package/src/lib/scenar-api/scenar-api.types.d.ts +14 -0
  50. package/src/lib/scenar-api/scenar-api.types.js +5 -0
  51. package/src/lib/scenar-api/scenar-api.types.js.map +1 -0
@@ -0,0 +1,699 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateScenarioRegistry = generateScenarioRegistry;
4
+ const tslib_1 = require("tslib");
5
+ const node_fs_1 = require("node:fs");
6
+ const promises_1 = require("node:fs/promises");
7
+ const node_path_1 = require("node:path");
8
+ const tinyglobby_1 = require("tinyglobby");
9
+ const ts_morph_1 = require("ts-morph");
10
+ const scenar_registry_1 = require("@cocoar/scenar-registry");
11
+ const node_1 = require("@cocoar/scenar-registry/node");
12
+ const path_utils_1 = require("../path-utils");
13
+ function generateScenarioRegistry(config, logger, options) {
14
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
15
+ const workspaceRoot = process.cwd();
16
+ const outputFilePath = (0, node_1.resolveGeneratedRegistryPath)({
17
+ workspaceRoot,
18
+ outputFilePath: options === null || options === void 0 ? void 0 : options.outputFilePath,
19
+ });
20
+ const ignoredDirNames = ['node_modules', 'dist', '.git'];
21
+ const scenarioFileAbsPaths = [];
22
+ const ignoredGlobs = ignoredDirNames.map((d) => `**/${d}/**`);
23
+ const matches = yield (0, tinyglobby_1.glob)(config.scenarios, {
24
+ cwd: workspaceRoot,
25
+ absolute: true,
26
+ onlyFiles: true,
27
+ dot: false,
28
+ ignore: ignoredGlobs,
29
+ });
30
+ for (const filePath of matches) {
31
+ if (!filePath.endsWith('.scenario.ts'))
32
+ continue;
33
+ scenarioFileAbsPaths.push((0, node_path_1.resolve)(filePath));
34
+ }
35
+ logger === null || logger === void 0 ? void 0 : logger.debug(`Found ${scenarioFileAbsPaths.length} *.scenario.ts files`);
36
+ const project = new ts_morph_1.Project({
37
+ tsConfigFilePath: (0, node_path_1.resolve)(workspaceRoot, '.scenar/tsconfig.scenar.json'),
38
+ skipAddingFilesFromTsConfig: true,
39
+ });
40
+ const issues = [];
41
+ const extracted = [];
42
+ const successfullyParsedScenarioFiles = new Set();
43
+ for (const absPath of scenarioFileAbsPaths) {
44
+ try {
45
+ const found = extractScenarioExports(project, absPath);
46
+ for (const s of found)
47
+ extracted.push(s);
48
+ successfullyParsedScenarioFiles.add(absPath);
49
+ }
50
+ catch (err) {
51
+ const message = err instanceof Error ? err.message : String(err);
52
+ issues.push({ fileAbsPath: absPath, message });
53
+ logger === null || logger === void 0 ? void 0 : logger.warn(`Skipping scenario file: ${absPath}`);
54
+ logger === null || logger === void 0 ? void 0 : logger.debug(message);
55
+ }
56
+ }
57
+ extracted.sort((a, b) => a.id.localeCompare(b.id));
58
+ const seenIds = new Set();
59
+ const scenarios = [];
60
+ for (const s of extracted) {
61
+ if (seenIds.has(s.id)) {
62
+ issues.push({
63
+ fileAbsPath: s.fileAbsPath,
64
+ message: `Duplicate scenario id '${s.id}'. This file will be skipped.`,
65
+ });
66
+ logger === null || logger === void 0 ? void 0 : logger.warn(`Skipping duplicate scenario id '${s.id}' in ${s.fileAbsPath}`);
67
+ continue;
68
+ }
69
+ seenIds.add(s.id);
70
+ scenarios.push(s);
71
+ }
72
+ const scenariosWithRuntimeIndex = scenarios.map((s) => (Object.assign(Object.assign({}, s), { scenarioImportPath: (0, path_utils_1.toRelativeImport)(outputFilePath, s.fileAbsPath) })));
73
+ const parserBlocks = scenariosWithRuntimeIndex
74
+ .map((s, index) => {
75
+ const defaultInputEntries = s.defaultInputs
76
+ .map((p) => ` ${JSON.stringify(p.inputName)}: ${p.valueExpression},`)
77
+ .join('\n');
78
+ const requiredInputEntries = s.requiredInputs
79
+ .map((p) => ` ${JSON.stringify(p.inputName)},`)
80
+ .join('\n');
81
+ const entries = s.inputParsers
82
+ .map((p) => ` ${JSON.stringify(p.inputName)}: ${p.parserExpression},`)
83
+ .join('\n');
84
+ const codecDescriptorEntries = s.inputCodecDescriptors
85
+ .map((d) => ` ${JSON.stringify(d.inputName)}: ${d.descriptorExpression},`)
86
+ .join('\n');
87
+ const canAutoImport = !!s.componentFileAbsPath && !!s.componentExportName;
88
+ const componentImportPath = canAutoImport
89
+ ? (0, path_utils_1.toRelativeImport)(outputFilePath, s.componentFileAbsPath)
90
+ : null;
91
+ const loadComponentExpression = canAutoImport
92
+ ? `(async () => {\n const mod = await import('${componentImportPath}');\n return mod.${s.componentExportName};\n })`
93
+ : `(async () => {\n throw new Error('Scenario is missing component information. Provide component/loadComponent() in the scenario module, or defineScenario<TComponent>() with an importable component type.');\n })`;
94
+ return `const autoInputs_${index}: Record<string, unknown> = {\n${defaultInputEntries}\n};\n\nconst requiredInputs_${index}: readonly string[] = [\n${requiredInputEntries}\n];\n\nconst autoInputParsers_${index}: Record<string, ScenarioInputParser> = {\n${entries}\n};\n\nconst autoInputCodecDescriptors_${index}: Record<string, ScenarioInputCodecDescriptor> = {\n${codecDescriptorEntries}\n};\n\nconst loadComponent_${index} = ${loadComponentExpression};`;
95
+ })
96
+ .join('\n\n');
97
+ const indexEntries = scenariosWithRuntimeIndex
98
+ .map((s, index) => {
99
+ const idLiteral = JSON.stringify(s.id);
100
+ const exportNameLiteral = JSON.stringify(s.exportName);
101
+ return ` ${idLiteral}: {\n id: ${idLiteral},\n exportName: ${exportNameLiteral},\n loadScenarioModule: () => import('${s.scenarioImportPath}'),\n autoInputs: autoInputs_${index},\n requiredInputs: requiredInputs_${index},\n inputParsers: autoInputParsers_${index},\n inputCodecDescriptors: autoInputCodecDescriptors_${index},\n loadComponent: loadComponent_${index},\n },`;
102
+ })
103
+ .join('\n');
104
+ const content = `/* eslint-disable */\n/*\n * Generated by Scenar Backstage tools.\n * Do not edit by hand.\n */\n\nexport const SCENAR_REGISTRY_SCHEMA_VERSION = ${scenar_registry_1.SCENAR_REGISTRY_SCHEMA_VERSION};\n\nimport {\n type ScenarioInputCodecDescriptor,\n type ScenarioInputParser,\n scenarioParseBoolean,\n scenarioParseIsoDate,\n scenarioParseJsonArray,\n scenarioParseJsonObject,\n scenarioParseNumber,\n scenarioParseString,\n} from '@cocoar/scenar-core';\n\n${parserBlocks}\n\nexport const SCENAR_REGISTRY_INDEX = {\n${indexEntries}\n} as const;\n\nexport const SCENAR_REGISTRY_IDS = Object.keys(SCENAR_REGISTRY_INDEX).sort();\n`;
105
+ yield (0, promises_1.mkdir)((0, node_path_1.dirname)(outputFilePath), { recursive: true });
106
+ yield (0, promises_1.writeFile)(outputFilePath, content, 'utf-8');
107
+ const watchedFileAbsPaths = Array.from(new Set([
108
+ ...successfullyParsedScenarioFiles,
109
+ ...scenarios.flatMap((s) => [s.fileAbsPath, s.componentFileAbsPath].filter(Boolean)),
110
+ ].map((p) => (0, node_path_1.resolve)(p)))).sort((a, b) => a.localeCompare(b));
111
+ return {
112
+ outputFilePath,
113
+ scenarioCount: scenarios.length,
114
+ skippedScenarioFileCount: scenarioFileAbsPaths.length - successfullyParsedScenarioFiles.size,
115
+ issues,
116
+ watchedFileAbsPaths,
117
+ };
118
+ });
119
+ }
120
+ function extractScenarioExports(project, fileAbsPath) {
121
+ var _a;
122
+ const sourceFile = project.addSourceFileAtPath(fileAbsPath);
123
+ const results = [];
124
+ for (const statement of sourceFile.getVariableStatements()) {
125
+ if (!statement.isExported())
126
+ continue;
127
+ for (const decl of statement.getDeclarations()) {
128
+ const exportName = decl.getName();
129
+ const initializer = decl.getInitializer();
130
+ if (!initializer)
131
+ continue;
132
+ // Primary supported authoring form: export const X = defineScenario({ ... })
133
+ const isDefineScenarioCall = isDefineScenarioCallExpression(sourceFile, initializer);
134
+ if (!isDefineScenarioCall)
135
+ continue;
136
+ const objectLiteral = (_a = initializer
137
+ .asKindOrThrow(ts_morph_1.SyntaxKind.CallExpression)
138
+ .getArguments()[0]) === null || _a === void 0 ? void 0 : _a.asKind(ts_morph_1.SyntaxKind.ObjectLiteralExpression);
139
+ if (!objectLiteral) {
140
+ throw new Error(`Scenario initializer in ${fileAbsPath} must be defineScenario({ ... }).`);
141
+ }
142
+ const idProp = objectLiteral.getProperty('id');
143
+ if (!idProp || !idProp.isKind(ts_morph_1.SyntaxKind.PropertyAssignment)) {
144
+ throw new Error(`Scenario '${exportName}' in ${fileAbsPath} must define an 'id' property (string literal).`);
145
+ }
146
+ const idInitializer = idProp.getInitializer();
147
+ const id = (idInitializer === null || idInitializer === void 0 ? void 0 : idInitializer.isKind(ts_morph_1.SyntaxKind.StringLiteral))
148
+ ? idInitializer.getLiteralValue()
149
+ : undefined;
150
+ if (!id) {
151
+ throw new Error(`Scenario id in ${fileAbsPath} must be a string literal (e.g. id: 'demo/hello').`);
152
+ }
153
+ const componentInfo = resolveScenarioComponentInfo(sourceFile, initializer, objectLiteral, fileAbsPath);
154
+ const defaultInputs = componentInfo.componentFileAbsPath
155
+ ? extractComponentDefaultInputs(project, componentInfo.componentFileAbsPath)
156
+ : [];
157
+ const requiredInputs = componentInfo.componentFileAbsPath
158
+ ? extractComponentRequiredInputs(project, componentInfo.componentFileAbsPath)
159
+ : [];
160
+ const inputParsers = componentInfo.componentFileAbsPath
161
+ ? extractComponentInputParsers(project, componentInfo.componentFileAbsPath)
162
+ : [];
163
+ const inputCodecDescriptors = componentInfo.componentFileAbsPath
164
+ ? extractComponentInputCodecDescriptors(project, componentInfo.componentFileAbsPath)
165
+ : [];
166
+ results.push({
167
+ fileAbsPath,
168
+ exportName,
169
+ id,
170
+ componentFileAbsPath: componentInfo.componentFileAbsPath,
171
+ componentExportName: componentInfo.componentExportName,
172
+ defaultInputs,
173
+ requiredInputs,
174
+ inputParsers,
175
+ inputCodecDescriptors,
176
+ });
177
+ }
178
+ }
179
+ if (results.length === 0) {
180
+ throw new Error(`Expected at least one exported defineScenario(...) in ${fileAbsPath} (e.g. export const myScenario = defineScenario({ id: '...' })).`);
181
+ }
182
+ // Stable output (important when multiple scenarios exist in one file).
183
+ results.sort((a, b) => a.id.localeCompare(b.id));
184
+ return results;
185
+ }
186
+ function isDefineScenarioCallExpression(sourceFile, expr) {
187
+ var _a, _b;
188
+ if (!expr.isKind(ts_morph_1.SyntaxKind.CallExpression))
189
+ return false;
190
+ const call = expr.asKindOrThrow(ts_morph_1.SyntaxKind.CallExpression);
191
+ const callee = call.getExpression();
192
+ // Supports:
193
+ // - import { defineScenario } from '@cocoar/scenar-core'; defineScenario(...)
194
+ // - import { defineScenario as def } from '@cocoar/scenar-core'; def(...)
195
+ if (callee.isKind(ts_morph_1.SyntaxKind.Identifier)) {
196
+ const localName = callee.getText();
197
+ for (const imp of sourceFile.getImportDeclarations()) {
198
+ if (imp.getModuleSpecifierValue() !== '@cocoar/scenar-core')
199
+ continue;
200
+ for (const spec of imp.getNamedImports()) {
201
+ const importedName = spec.getName();
202
+ const alias = (_b = (_a = spec.getAliasNode()) === null || _a === void 0 ? void 0 : _a.getText()) !== null && _b !== void 0 ? _b : null;
203
+ if (importedName === 'defineScenario' &&
204
+ (alias ? alias === localName : localName === 'defineScenario')) {
205
+ return true;
206
+ }
207
+ }
208
+ }
209
+ return false;
210
+ }
211
+ // Supports:
212
+ // - import * as scenar from '@cocoar/scenar-core'; scenar.defineScenario(...)
213
+ if (callee.isKind(ts_morph_1.SyntaxKind.PropertyAccessExpression)) {
214
+ const pa = callee.asKindOrThrow(ts_morph_1.SyntaxKind.PropertyAccessExpression);
215
+ const lhs = pa.getExpression();
216
+ const rhs = pa.getName();
217
+ if (!lhs.isKind(ts_morph_1.SyntaxKind.Identifier))
218
+ return false;
219
+ if (rhs !== 'defineScenario')
220
+ return false;
221
+ const nsLocalName = lhs.getText();
222
+ for (const imp of sourceFile.getImportDeclarations()) {
223
+ if (imp.getModuleSpecifierValue() !== '@cocoar/scenar-core')
224
+ continue;
225
+ const ns = imp.getNamespaceImport();
226
+ if ((ns === null || ns === void 0 ? void 0 : ns.getText()) === nsLocalName)
227
+ return true;
228
+ }
229
+ return false;
230
+ }
231
+ return false;
232
+ }
233
+ function resolveScenarioComponentInfo(scenarioSourceFile, scenarioInitializer, scenarioObjectLiteral, scenarioFileAbsPath) {
234
+ var _a, _b, _c, _d, _e;
235
+ const componentProp = scenarioObjectLiteral.getProperty('component');
236
+ if (componentProp === null || componentProp === void 0 ? void 0 : componentProp.isKind(ts_morph_1.SyntaxKind.PropertyAssignment)) {
237
+ const initializer = componentProp.getInitializer();
238
+ if (!initializer)
239
+ return { componentFileAbsPath: null, componentExportName: null };
240
+ // Prefer symbol resolution if possible.
241
+ try {
242
+ const symbol = initializer.getType().getSymbol();
243
+ const decls = (_a = symbol === null || symbol === void 0 ? void 0 : symbol.getDeclarations()) !== null && _a !== void 0 ? _a : [];
244
+ for (const d of decls) {
245
+ const sf = d.getSourceFile();
246
+ const abs = sf.getFilePath();
247
+ if (!abs)
248
+ continue;
249
+ return {
250
+ componentFileAbsPath: abs,
251
+ componentExportName: initializer.getText(),
252
+ };
253
+ }
254
+ }
255
+ catch (_f) {
256
+ // ignore
257
+ }
258
+ // Fallback: resolve from import declarations in the scenario file.
259
+ const name = initializer.getText();
260
+ for (const imp of scenarioSourceFile.getImportDeclarations()) {
261
+ const moduleSpec = imp.getModuleSpecifierValue();
262
+ if (!moduleSpec.startsWith('.'))
263
+ continue;
264
+ for (const spec of imp.getNamedImports()) {
265
+ const importedName = spec.getName();
266
+ const alias = (_c = (_b = spec.getAliasNode()) === null || _b === void 0 ? void 0 : _b.getText()) !== null && _c !== void 0 ? _c : null;
267
+ if (alias ? alias !== name : importedName !== name)
268
+ continue;
269
+ const resolved = resolveModuleToTsFile(scenarioFileAbsPath, moduleSpec);
270
+ return {
271
+ componentFileAbsPath: resolved,
272
+ componentExportName: importedName,
273
+ };
274
+ }
275
+ }
276
+ return { componentFileAbsPath: null, componentExportName: name };
277
+ }
278
+ const loadComponentProp = scenarioObjectLiteral.getProperty('loadComponent');
279
+ if (loadComponentProp === null || loadComponentProp === void 0 ? void 0 : loadComponentProp.isKind(ts_morph_1.SyntaxKind.PropertyAssignment)) {
280
+ const initializer = loadComponentProp.getInitializer();
281
+ if (!initializer)
282
+ return { componentFileAbsPath: null, componentExportName: null };
283
+ const importCall = initializer
284
+ .getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)
285
+ .find((ce) => ce.getExpression().getKind() === ts_morph_1.SyntaxKind.ImportKeyword);
286
+ const moduleArg = importCall === null || importCall === void 0 ? void 0 : importCall.getArguments()[0];
287
+ if (!moduleArg || !moduleArg.isKind(ts_morph_1.SyntaxKind.StringLiteral)) {
288
+ return { componentFileAbsPath: null, componentExportName: null };
289
+ }
290
+ const moduleSpec = moduleArg.getLiteralValue();
291
+ return {
292
+ componentFileAbsPath: resolveModuleToTsFile(scenarioFileAbsPath, moduleSpec),
293
+ componentExportName: null,
294
+ };
295
+ }
296
+ // Fallback: defineScenario<TComponent>({ id: '...' })
297
+ if (scenarioInitializer.isKind(ts_morph_1.SyntaxKind.CallExpression)) {
298
+ const call = scenarioInitializer.asKindOrThrow(ts_morph_1.SyntaxKind.CallExpression);
299
+ const typeArg = (_d = call.getTypeArguments()) === null || _d === void 0 ? void 0 : _d[0];
300
+ if (typeArg) {
301
+ const type = typeArg.getType();
302
+ const symbol = type.getSymbol();
303
+ const decls = (_e = symbol === null || symbol === void 0 ? void 0 : symbol.getDeclarations()) !== null && _e !== void 0 ? _e : [];
304
+ for (const d of decls) {
305
+ const sf = d.getSourceFile();
306
+ const abs = sf.getFilePath();
307
+ if (!abs)
308
+ continue;
309
+ return {
310
+ componentFileAbsPath: abs,
311
+ componentExportName: typeArg.getText(),
312
+ };
313
+ }
314
+ }
315
+ }
316
+ return { componentFileAbsPath: null, componentExportName: null };
317
+ }
318
+ function resolveModuleToTsFile(fromFileAbsPath, moduleSpecifier) {
319
+ if (!moduleSpecifier.startsWith('.')) {
320
+ return null;
321
+ }
322
+ const fromDir = (0, node_path_1.dirname)(fromFileAbsPath);
323
+ const base = (0, node_path_1.resolve)(fromDir, moduleSpecifier);
324
+ // Most Angular code imports without extension.
325
+ const candidates = [
326
+ base,
327
+ `${base}.ts`,
328
+ `${base}.tsx`,
329
+ (0, node_path_1.resolve)(base, 'index.ts'),
330
+ ];
331
+ for (const c of candidates) {
332
+ try {
333
+ const stat = (0, node_fs_1.statSync)(c);
334
+ if (stat.isFile())
335
+ return c;
336
+ }
337
+ catch (_a) {
338
+ // ignore
339
+ }
340
+ }
341
+ return null;
342
+ }
343
+ function extractComponentInputParsers(project, componentFileAbsPath) {
344
+ const sourceFile = project.addSourceFileAtPath(componentFileAbsPath);
345
+ const parsers = [];
346
+ for (const cls of sourceFile.getClasses()) {
347
+ for (const prop of cls.getProperties()) {
348
+ if (!isSignalInputProperty(prop))
349
+ continue;
350
+ const inputName = prop.getName();
351
+ if (!inputName)
352
+ continue;
353
+ const parserExpression = inferParserExpression(prop);
354
+ parsers.push({ inputName, parserExpression });
355
+ }
356
+ }
357
+ // Stable output.
358
+ parsers.sort((a, b) => a.inputName.localeCompare(b.inputName));
359
+ return parsers;
360
+ }
361
+ function extractComponentInputCodecDescriptors(project, componentFileAbsPath) {
362
+ const sourceFile = project.addSourceFileAtPath(componentFileAbsPath);
363
+ const descriptors = [];
364
+ for (const cls of sourceFile.getClasses()) {
365
+ for (const prop of cls.getProperties()) {
366
+ if (!isSignalInputProperty(prop))
367
+ continue;
368
+ const inputName = prop.getName();
369
+ if (!inputName)
370
+ continue;
371
+ const descriptorExpression = inferCodecDescriptorExpression(prop);
372
+ descriptors.push({ inputName, descriptorExpression });
373
+ }
374
+ }
375
+ // Stable output.
376
+ descriptors.sort((a, b) => a.inputName.localeCompare(b.inputName));
377
+ return descriptors;
378
+ }
379
+ function extractComponentDefaultInputs(project, componentFileAbsPath) {
380
+ const sourceFile = project.addSourceFileAtPath(componentFileAbsPath);
381
+ const defaults = [];
382
+ for (const cls of sourceFile.getClasses()) {
383
+ for (const prop of cls.getProperties()) {
384
+ if (!isSignalInputProperty(prop))
385
+ continue;
386
+ const inputName = prop.getName();
387
+ if (!inputName)
388
+ continue;
389
+ // Signal input: name = input('x')
390
+ const signalDefault = extractDefaultFromSignalInputInitializer(prop.getInitializer());
391
+ if (signalDefault !== null) {
392
+ defaults.push({ inputName, valueExpression: signalDefault });
393
+ continue;
394
+ }
395
+ }
396
+ }
397
+ defaults.sort((a, b) => a.inputName.localeCompare(b.inputName));
398
+ return defaults;
399
+ }
400
+ function extractComponentRequiredInputs(project, componentFileAbsPath) {
401
+ const sourceFile = project.addSourceFileAtPath(componentFileAbsPath);
402
+ const requiredInputs = [];
403
+ for (const cls of sourceFile.getClasses()) {
404
+ for (const prop of cls.getProperties()) {
405
+ if (!isRequiredSignalInputOrModelProperty(prop))
406
+ continue;
407
+ const inputName = prop.getName();
408
+ if (!inputName)
409
+ continue;
410
+ requiredInputs.push({ inputName });
411
+ }
412
+ }
413
+ requiredInputs.sort((a, b) => a.inputName.localeCompare(b.inputName));
414
+ return requiredInputs;
415
+ }
416
+ function extractDefaultFromSignalInputInitializer(initializer) {
417
+ if (!(initializer === null || initializer === void 0 ? void 0 : initializer.isKind(ts_morph_1.SyntaxKind.CallExpression)))
418
+ return null;
419
+ const call = initializer.asKindOrThrow(ts_morph_1.SyntaxKind.CallExpression);
420
+ // input.required() / model.required() have no default.
421
+ const exprText = call.getExpression().getText();
422
+ if (exprText !== 'input' && exprText !== 'model')
423
+ return null;
424
+ const firstArg = call.getArguments()[0];
425
+ if (!firstArg)
426
+ return null;
427
+ const expr = unwrapTrivialExpression(firstArg);
428
+ if (!isSafeLiteralExpression(expr))
429
+ return null;
430
+ return expr.getText();
431
+ }
432
+ function unwrapTrivialExpression(expr) {
433
+ let current = expr;
434
+ while (true) {
435
+ const paren = current.asKind(ts_morph_1.SyntaxKind.ParenthesizedExpression);
436
+ if (paren) {
437
+ current = paren.getExpression();
438
+ continue;
439
+ }
440
+ const asExpr = current.asKind(ts_morph_1.SyntaxKind.AsExpression);
441
+ if (asExpr) {
442
+ current = asExpr.getExpression();
443
+ continue;
444
+ }
445
+ const typeAssert = current.asKind(ts_morph_1.SyntaxKind.TypeAssertionExpression);
446
+ if (typeAssert) {
447
+ current = typeAssert.getExpression();
448
+ continue;
449
+ }
450
+ return current;
451
+ }
452
+ }
453
+ function isSafeLiteralExpression(expr) {
454
+ const unwrapped = unwrapTrivialExpression(expr);
455
+ if (unwrapped.isKind(ts_morph_1.SyntaxKind.StringLiteral) ||
456
+ unwrapped.isKind(ts_morph_1.SyntaxKind.NoSubstitutionTemplateLiteral) ||
457
+ unwrapped.isKind(ts_morph_1.SyntaxKind.NumericLiteral) ||
458
+ unwrapped.isKind(ts_morph_1.SyntaxKind.TrueKeyword) ||
459
+ unwrapped.isKind(ts_morph_1.SyntaxKind.FalseKeyword) ||
460
+ unwrapped.isKind(ts_morph_1.SyntaxKind.NullKeyword)) {
461
+ return true;
462
+ }
463
+ if (unwrapped.isKind(ts_morph_1.SyntaxKind.PrefixUnaryExpression)) {
464
+ const p = unwrapped.asKindOrThrow(ts_morph_1.SyntaxKind.PrefixUnaryExpression);
465
+ const op = p.getOperatorToken();
466
+ const operand = p.getOperand();
467
+ return (op === ts_morph_1.SyntaxKind.MinusToken && operand.isKind(ts_morph_1.SyntaxKind.NumericLiteral));
468
+ }
469
+ if (unwrapped.isKind(ts_morph_1.SyntaxKind.ArrayLiteralExpression)) {
470
+ const arr = unwrapped.asKindOrThrow(ts_morph_1.SyntaxKind.ArrayLiteralExpression);
471
+ for (const el of arr.getElements()) {
472
+ // Disallow spreads/holes since they are not safe literals.
473
+ if (el.isKind(ts_morph_1.SyntaxKind.SpreadElement) ||
474
+ el.isKind(ts_morph_1.SyntaxKind.OmittedExpression)) {
475
+ return false;
476
+ }
477
+ const elementExpression = el;
478
+ if (!isSafeLiteralExpression(elementExpression))
479
+ return false;
480
+ }
481
+ return true;
482
+ }
483
+ if (unwrapped.isKind(ts_morph_1.SyntaxKind.ObjectLiteralExpression)) {
484
+ const obj = unwrapped.asKindOrThrow(ts_morph_1.SyntaxKind.ObjectLiteralExpression);
485
+ for (const prop of obj.getProperties()) {
486
+ if (prop.isKind(ts_morph_1.SyntaxKind.PropertyAssignment)) {
487
+ const init = prop.getInitializer();
488
+ if (!init)
489
+ return false;
490
+ if (!isSafeLiteralExpression(init))
491
+ return false;
492
+ continue;
493
+ }
494
+ return false;
495
+ }
496
+ return true;
497
+ }
498
+ return false;
499
+ }
500
+ function inferParserExpression(prop) {
501
+ var _a;
502
+ const initializer = prop.getInitializer();
503
+ // If using Angular's input<T>() / input.required<T>() / model<T>() / model.required<T>(),
504
+ // prefer the generic argument type.
505
+ if ((initializer === null || initializer === void 0 ? void 0 : initializer.isKind(ts_morph_1.SyntaxKind.CallExpression)) &&
506
+ isSignalInputOrModelCallExpressionText(initializer.getExpression().getText())) {
507
+ const typeArg = (_a = initializer.getTypeArguments()) === null || _a === void 0 ? void 0 : _a[0];
508
+ if (typeArg) {
509
+ return inferParserExpressionFromTypeText(typeArg.getText());
510
+ }
511
+ }
512
+ let type = prop.getType();
513
+ type = unwrapNullableUnionType(type);
514
+ if (type.isBoolean())
515
+ return '(raw: string) => scenarioParseBoolean(raw)';
516
+ if (type.isNumber())
517
+ return '(raw: string) => scenarioParseNumber(raw)';
518
+ if (type.isString())
519
+ return '(raw: string) => scenarioParseString(raw)';
520
+ // Common class type: Date
521
+ // Canonical wire format: ISO 8601 via Date.toISOString()
522
+ if (type.getText() === 'Date')
523
+ return '(raw: string) => scenarioParseIsoDate(raw)';
524
+ // Structured values are transported as JSON in a single query param.
525
+ if (isArrayLikeType(type)) {
526
+ const element = getArrayLikeElementType(type);
527
+ if (element && isNonDateClassType(element)) {
528
+ return '(raw: string) => scenarioParseString(raw)';
529
+ }
530
+ return '(raw: string) => scenarioParseJsonArray(raw)';
531
+ }
532
+ if (type.isObject() && type.getText() !== 'Date') {
533
+ // Avoid pretending we can rebuild class instances from query params.
534
+ if (isNonDateClassType(type)) {
535
+ return '(raw: string) => scenarioParseString(raw)';
536
+ }
537
+ return '(raw: string) => scenarioParseJsonObject(raw)';
538
+ }
539
+ // Minimal fallback: keep it as a string.
540
+ return '(raw: string) => scenarioParseString(raw)';
541
+ }
542
+ function inferCodecDescriptorExpression(prop) {
543
+ var _a;
544
+ const initializer = prop.getInitializer();
545
+ // If using Angular's input<T>() / input.required<T>() / model<T>() / model.required<T>(),
546
+ // prefer the generic argument type.
547
+ if ((initializer === null || initializer === void 0 ? void 0 : initializer.isKind(ts_morph_1.SyntaxKind.CallExpression)) &&
548
+ isSignalInputOrModelCallExpressionText(initializer.getExpression().getText())) {
549
+ const typeArg = (_a = initializer.getTypeArguments()) === null || _a === void 0 ? void 0 : _a[0];
550
+ if (typeArg) {
551
+ return inferCodecDescriptorExpressionFromTypeText(typeArg.getText());
552
+ }
553
+ }
554
+ let type = prop.getType();
555
+ type = unwrapNullableUnionType(type);
556
+ if (type.isBoolean())
557
+ return "{ kind: 'boolean' }";
558
+ if (type.isNumber())
559
+ return "{ kind: 'number' }";
560
+ if (type.isString())
561
+ return "{ kind: 'string' }";
562
+ // Common class type: Date
563
+ if (type.getText() === 'Date')
564
+ return "{ kind: 'isoDate' }";
565
+ if (isArrayLikeType(type)) {
566
+ const element = getArrayLikeElementType(type);
567
+ if (element && isNonDateClassType(element))
568
+ return "{ kind: 'string' }";
569
+ return "{ kind: 'jsonArray' }";
570
+ }
571
+ if (type.isObject() && type.getText() !== 'Date') {
572
+ if (isNonDateClassType(type))
573
+ return "{ kind: 'string' }";
574
+ return "{ kind: 'jsonObject' }";
575
+ }
576
+ return "{ kind: 'string' }";
577
+ }
578
+ function inferParserExpressionFromTypeText(typeText) {
579
+ const normalized = stripNullableTypeText(typeText);
580
+ if (normalized === 'boolean')
581
+ return '(raw: string) => scenarioParseBoolean(raw)';
582
+ if (normalized === 'number')
583
+ return '(raw: string) => scenarioParseNumber(raw)';
584
+ if (normalized === 'string')
585
+ return '(raw: string) => scenarioParseString(raw)';
586
+ if (normalized === 'Date')
587
+ return '(raw: string) => scenarioParseIsoDate(raw)';
588
+ if (normalized.endsWith('[]') ||
589
+ normalized.startsWith('Array<') ||
590
+ normalized.startsWith('ReadonlyArray<') ||
591
+ normalized.startsWith('readonly ')) {
592
+ return '(raw: string) => scenarioParseJsonArray(raw)';
593
+ }
594
+ if (normalized === 'object' ||
595
+ normalized.startsWith('Record<') ||
596
+ normalized.startsWith('{')) {
597
+ return '(raw: string) => scenarioParseJsonObject(raw)';
598
+ }
599
+ return '(raw: string) => scenarioParseString(raw)';
600
+ }
601
+ function inferCodecDescriptorExpressionFromTypeText(typeText) {
602
+ const normalized = stripNullableTypeText(typeText);
603
+ if (normalized === 'boolean')
604
+ return "{ kind: 'boolean' }";
605
+ if (normalized === 'number')
606
+ return "{ kind: 'number' }";
607
+ if (normalized === 'string')
608
+ return "{ kind: 'string' }";
609
+ if (normalized === 'Date')
610
+ return "{ kind: 'isoDate' }";
611
+ if (normalized.endsWith('[]') ||
612
+ normalized.startsWith('Array<') ||
613
+ normalized.startsWith('ReadonlyArray<') ||
614
+ normalized.startsWith('readonly ')) {
615
+ return "{ kind: 'jsonArray' }";
616
+ }
617
+ if (normalized === 'object' ||
618
+ normalized.startsWith('Record<') ||
619
+ normalized.startsWith('{')) {
620
+ return "{ kind: 'jsonObject' }";
621
+ }
622
+ return "{ kind: 'string' }";
623
+ }
624
+ function unwrapNullableUnionType(type) {
625
+ if (!type.isUnion())
626
+ return type;
627
+ const parts = type.getUnionTypes();
628
+ const nonNullable = parts.filter((t) => !t.isNull() && !t.isUndefined());
629
+ if (nonNullable.length === 1)
630
+ return nonNullable[0];
631
+ // Old enum inference became a plain string under the fixed codec set.
632
+ const stringLiterals = nonNullable
633
+ .map((t) => t.getLiteralValue())
634
+ .filter((v) => typeof v === 'string');
635
+ if (stringLiterals.length === nonNullable.length &&
636
+ stringLiterals.length > 0) {
637
+ return nonNullable[0];
638
+ }
639
+ return type;
640
+ }
641
+ function stripNullableTypeText(typeText) {
642
+ // Lightweight normalization for input<T>() / model<T>() generic arguments.
643
+ return typeText
644
+ .replace(/\s+/g, ' ')
645
+ .replace(/\s*\|\s*null\b/g, '')
646
+ .replace(/\s*\|\s*undefined\b/g, '')
647
+ .trim();
648
+ }
649
+ function isArrayLikeType(type) {
650
+ var _a, _b;
651
+ const anyType = type;
652
+ if (((_a = anyType.isArray) === null || _a === void 0 ? void 0 : _a.call(anyType)) === true)
653
+ return true;
654
+ if (((_b = anyType.isTuple) === null || _b === void 0 ? void 0 : _b.call(anyType)) === true)
655
+ return true;
656
+ const text = type.getText();
657
+ return (text.endsWith('[]') ||
658
+ text.startsWith('Array<') ||
659
+ text.startsWith('ReadonlyArray<') ||
660
+ text.startsWith('readonly '));
661
+ }
662
+ function getArrayLikeElementType(type) {
663
+ var _a;
664
+ const anyType = type;
665
+ const el = (_a = anyType.getArrayElementType) === null || _a === void 0 ? void 0 : _a.call(anyType);
666
+ return el !== null && el !== void 0 ? el : null;
667
+ }
668
+ function isNonDateClassType(type) {
669
+ if (type.getText() === 'Date')
670
+ return false;
671
+ const symbol = type.getSymbol();
672
+ if (!symbol)
673
+ return false;
674
+ return symbol
675
+ .getDeclarations()
676
+ .some((d) => d.getKind() === ts_morph_1.SyntaxKind.ClassDeclaration);
677
+ }
678
+ function isSignalInputOrModelCallExpressionText(exprText) {
679
+ return (exprText === 'input' ||
680
+ exprText === 'input.required' ||
681
+ exprText === 'model' ||
682
+ exprText === 'model.required');
683
+ }
684
+ function isSignalInputProperty(prop) {
685
+ const initializer = prop.getInitializer();
686
+ if (!(initializer === null || initializer === void 0 ? void 0 : initializer.isKind(ts_morph_1.SyntaxKind.CallExpression)))
687
+ return false;
688
+ const call = initializer.asKindOrThrow(ts_morph_1.SyntaxKind.CallExpression);
689
+ return isSignalInputOrModelCallExpressionText(call.getExpression().getText());
690
+ }
691
+ function isRequiredSignalInputOrModelProperty(prop) {
692
+ const initializer = prop.getInitializer();
693
+ if (!(initializer === null || initializer === void 0 ? void 0 : initializer.isKind(ts_morph_1.SyntaxKind.CallExpression)))
694
+ return false;
695
+ const call = initializer.asKindOrThrow(ts_morph_1.SyntaxKind.CallExpression);
696
+ const exprText = call.getExpression().getText();
697
+ return exprText === 'input.required' || exprText === 'model.required';
698
+ }
699
+ //# sourceMappingURL=generate-scenario-registry.fn.js.map