@auto-engineer/narrative 1.5.1 → 1.5.2
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +5 -5
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +15 -0
- package/dist/src/transformers/model-to-narrative/ast/emit-helpers.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/ast/emit-helpers.js +1 -8
- package/dist/src/transformers/model-to-narrative/ast/emit-helpers.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/cross-module-imports.d.ts +12 -0
- package/dist/src/transformers/model-to-narrative/cross-module-imports.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/cross-module-imports.js +69 -21
- package/dist/src/transformers/model-to-narrative/cross-module-imports.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/module-code.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/module-code.js +10 -7
- package/dist/src/transformers/model-to-narrative/generators/module-code.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/types.d.ts +1 -1
- package/dist/src/transformers/model-to-narrative/generators/types.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/types.js +4 -3
- package/dist/src/transformers/model-to-narrative/generators/types.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/transformers/model-to-narrative/ast/emit-helpers.ts +1 -9
- package/src/transformers/model-to-narrative/cross-module-imports.specs.ts +191 -13
- package/src/transformers/model-to-narrative/cross-module-imports.ts +87 -24
- package/src/transformers/model-to-narrative/generators/module-code.ts +11 -4
- package/src/transformers/model-to-narrative/generators/types.ts +5 -7
- package/src/transformers/model-to-narrative/modules.specs.ts +3 -2
package/package.json
CHANGED
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"typescript": "^5.9.2",
|
|
24
24
|
"zod": "^3.22.4",
|
|
25
25
|
"zod-to-json-schema": "^3.22.3",
|
|
26
|
-
"@auto-engineer/file-store": "1.5.
|
|
27
|
-
"@auto-engineer/id": "1.5.
|
|
28
|
-
"@auto-engineer/message-bus": "1.5.
|
|
26
|
+
"@auto-engineer/file-store": "1.5.2",
|
|
27
|
+
"@auto-engineer/id": "1.5.2",
|
|
28
|
+
"@auto-engineer/message-bus": "1.5.2"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^20.0.0",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
|
-
"version": "1.5.
|
|
38
|
+
"version": "1.5.2",
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsx scripts/build.ts",
|
|
41
41
|
"test": "vitest run --reporter=dot",
|
|
@@ -7,14 +7,6 @@ export interface FieldTypeInfo {
|
|
|
7
7
|
[fieldName: string]: string; // field name -> type (e.g., 'Date', 'string', etc.)
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* Check if a string is a valid ISO date string
|
|
12
|
-
*/
|
|
13
|
-
function isISODateString(str: string): boolean {
|
|
14
|
-
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(?:Z|[+-]\d{2}:\d{2})?$/;
|
|
15
|
-
return isoDateRegex.test(str);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
10
|
/**
|
|
19
11
|
* Emit a TS expression from a plain JSON-like value with optional type information for Date handling.
|
|
20
12
|
*/
|
|
@@ -50,7 +42,7 @@ export function jsonToExpr(
|
|
|
50
42
|
const fieldType = typeInfo?.[k];
|
|
51
43
|
let valueExpr: tsNS.Expression;
|
|
52
44
|
|
|
53
|
-
if (fieldType === 'Date' && typeof x === 'string'
|
|
45
|
+
if (fieldType === 'Date' && typeof x === 'string') {
|
|
54
46
|
// Generate new Date('...') for Date fields
|
|
55
47
|
valueExpr = f.createNewExpression(f.createIdentifier('Date'), undefined, [f.createStringLiteral(x)]);
|
|
56
48
|
} else if (fieldType === 'Date' && x instanceof Date) {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import type { Model } from '../../index';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
computeAllCrossModuleDependencies,
|
|
5
|
+
computeCrossModuleImports,
|
|
6
|
+
resolveRelativeImport,
|
|
7
|
+
} from './cross-module-imports';
|
|
4
8
|
|
|
5
9
|
describe('resolveRelativeImport', () => {
|
|
6
10
|
it('resolves same directory imports', () => {
|
|
@@ -49,7 +53,6 @@ describe('computeCrossModuleImports', () => {
|
|
|
49
53
|
integrations: [],
|
|
50
54
|
modules: [
|
|
51
55
|
{
|
|
52
|
-
id: 'derived.ts',
|
|
53
56
|
sourceFile: 'derived.ts',
|
|
54
57
|
isDerived: true,
|
|
55
58
|
contains: { narrativeIds: [] },
|
|
@@ -103,7 +106,6 @@ describe('computeCrossModuleImports', () => {
|
|
|
103
106
|
integrations: [],
|
|
104
107
|
modules: [
|
|
105
108
|
{
|
|
106
|
-
id: 'self-contained',
|
|
107
109
|
sourceFile: 'self-contained.ts',
|
|
108
110
|
isDerived: false,
|
|
109
111
|
contains: { narrativeIds: ['test-1'] },
|
|
@@ -157,7 +159,6 @@ describe('computeCrossModuleImports', () => {
|
|
|
157
159
|
integrations: [],
|
|
158
160
|
modules: [
|
|
159
161
|
{
|
|
160
|
-
id: 'consumer',
|
|
161
162
|
sourceFile: 'consumer.ts',
|
|
162
163
|
isDerived: false,
|
|
163
164
|
contains: { narrativeIds: ['test-1'] },
|
|
@@ -212,14 +213,12 @@ describe('computeCrossModuleImports', () => {
|
|
|
212
213
|
integrations: [],
|
|
213
214
|
modules: [
|
|
214
215
|
{
|
|
215
|
-
id: 'shared',
|
|
216
216
|
sourceFile: 'shared/types.ts',
|
|
217
217
|
isDerived: false,
|
|
218
218
|
contains: { narrativeIds: ['shared-1'] },
|
|
219
219
|
declares: { messages: [{ kind: 'event', name: 'SharedEvent' }] },
|
|
220
220
|
},
|
|
221
221
|
{
|
|
222
|
-
id: 'consumer',
|
|
223
222
|
sourceFile: 'features/consumer.ts',
|
|
224
223
|
isDerived: false,
|
|
225
224
|
contains: { narrativeIds: ['consumer-1'] },
|
|
@@ -281,7 +280,6 @@ describe('computeCrossModuleImports', () => {
|
|
|
281
280
|
integrations: [],
|
|
282
281
|
modules: [
|
|
283
282
|
{
|
|
284
|
-
id: 'shared',
|
|
285
283
|
sourceFile: 'shared.ts',
|
|
286
284
|
isDerived: false,
|
|
287
285
|
contains: { narrativeIds: ['shared-1'] },
|
|
@@ -293,7 +291,6 @@ describe('computeCrossModuleImports', () => {
|
|
|
293
291
|
},
|
|
294
292
|
},
|
|
295
293
|
{
|
|
296
|
-
id: 'consumer',
|
|
297
294
|
sourceFile: 'consumer.ts',
|
|
298
295
|
isDerived: false,
|
|
299
296
|
contains: { narrativeIds: ['consumer-1'] },
|
|
@@ -349,14 +346,12 @@ describe('computeCrossModuleImports', () => {
|
|
|
349
346
|
integrations: [],
|
|
350
347
|
modules: [
|
|
351
348
|
{
|
|
352
|
-
id: 'derived.ts',
|
|
353
349
|
sourceFile: 'derived.ts',
|
|
354
350
|
isDerived: true,
|
|
355
351
|
contains: { narrativeIds: [] },
|
|
356
352
|
declares: { messages: [{ kind: 'event', name: 'DerivedEvent' }] },
|
|
357
353
|
},
|
|
358
354
|
{
|
|
359
|
-
id: 'consumer',
|
|
360
355
|
sourceFile: 'consumer.ts',
|
|
361
356
|
isDerived: false,
|
|
362
357
|
contains: { narrativeIds: ['consumer-1'] },
|
|
@@ -419,21 +414,18 @@ describe('computeCrossModuleImports', () => {
|
|
|
419
414
|
integrations: [],
|
|
420
415
|
modules: [
|
|
421
416
|
{
|
|
422
|
-
id: 'z-types',
|
|
423
417
|
sourceFile: 'z-types.ts',
|
|
424
418
|
isDerived: false,
|
|
425
419
|
contains: { narrativeIds: ['types-1'] },
|
|
426
420
|
declares: { messages: [{ kind: 'event', name: 'ZEvent' }] },
|
|
427
421
|
},
|
|
428
422
|
{
|
|
429
|
-
id: 'a-types',
|
|
430
423
|
sourceFile: 'a-types.ts',
|
|
431
424
|
isDerived: false,
|
|
432
425
|
contains: { narrativeIds: ['types-2'] },
|
|
433
426
|
declares: { messages: [{ kind: 'event', name: 'AEvent' }] },
|
|
434
427
|
},
|
|
435
428
|
{
|
|
436
|
-
id: 'consumer',
|
|
437
429
|
sourceFile: 'consumer.ts',
|
|
438
430
|
isDerived: false,
|
|
439
431
|
contains: { narrativeIds: ['consumer-1'] },
|
|
@@ -448,3 +440,189 @@ describe('computeCrossModuleImports', () => {
|
|
|
448
440
|
expect(imports.map((i) => i.fromPath)).toEqual(['./a-types', './z-types']);
|
|
449
441
|
});
|
|
450
442
|
});
|
|
443
|
+
|
|
444
|
+
describe('computeAllCrossModuleDependencies', () => {
|
|
445
|
+
it('returns empty maps when no cross-module imports exist', () => {
|
|
446
|
+
const model: Model = {
|
|
447
|
+
variant: 'specs',
|
|
448
|
+
narratives: [{ name: 'Test', id: 'test-1', slices: [] }],
|
|
449
|
+
messages: [{ type: 'event', source: 'internal', name: 'LocalEvent', fields: [] }],
|
|
450
|
+
integrations: [],
|
|
451
|
+
modules: [
|
|
452
|
+
{
|
|
453
|
+
sourceFile: 'self-contained.ts',
|
|
454
|
+
isDerived: false,
|
|
455
|
+
contains: { narrativeIds: ['test-1'] },
|
|
456
|
+
declares: { messages: [{ kind: 'event', name: 'LocalEvent' }] },
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const { importsPerModule, exportsPerModule } = computeAllCrossModuleDependencies(model.modules, model);
|
|
462
|
+
|
|
463
|
+
expect(importsPerModule.get('self-contained.ts')).toEqual([]);
|
|
464
|
+
expect(exportsPerModule.size).toBe(0);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('computes imports and exports in a single pass', () => {
|
|
468
|
+
const model: Model = {
|
|
469
|
+
variant: 'specs',
|
|
470
|
+
narratives: [
|
|
471
|
+
{ name: 'Shared', id: 'shared-1', slices: [] },
|
|
472
|
+
{
|
|
473
|
+
name: 'Consumer',
|
|
474
|
+
id: 'consumer-1',
|
|
475
|
+
slices: [
|
|
476
|
+
{
|
|
477
|
+
name: 'test',
|
|
478
|
+
type: 'command',
|
|
479
|
+
client: { specs: [] },
|
|
480
|
+
server: {
|
|
481
|
+
description: 'Test',
|
|
482
|
+
specs: [
|
|
483
|
+
{
|
|
484
|
+
type: 'gherkin',
|
|
485
|
+
feature: 'Test',
|
|
486
|
+
rules: [
|
|
487
|
+
{
|
|
488
|
+
name: 'rule',
|
|
489
|
+
examples: [
|
|
490
|
+
{
|
|
491
|
+
name: 'example',
|
|
492
|
+
steps: [{ keyword: 'Then', text: 'SharedEvent' }],
|
|
493
|
+
},
|
|
494
|
+
],
|
|
495
|
+
},
|
|
496
|
+
],
|
|
497
|
+
},
|
|
498
|
+
],
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
],
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
messages: [{ type: 'event', source: 'internal', name: 'SharedEvent', fields: [] }],
|
|
505
|
+
integrations: [],
|
|
506
|
+
modules: [
|
|
507
|
+
{
|
|
508
|
+
sourceFile: 'shared/types.ts',
|
|
509
|
+
isDerived: false,
|
|
510
|
+
contains: { narrativeIds: ['shared-1'] },
|
|
511
|
+
declares: { messages: [{ kind: 'event', name: 'SharedEvent' }] },
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
sourceFile: 'features/consumer.ts',
|
|
515
|
+
isDerived: false,
|
|
516
|
+
contains: { narrativeIds: ['consumer-1'] },
|
|
517
|
+
declares: { messages: [] },
|
|
518
|
+
},
|
|
519
|
+
],
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const { importsPerModule, exportsPerModule } = computeAllCrossModuleDependencies(model.modules, model);
|
|
523
|
+
|
|
524
|
+
// Consumer should import SharedEvent
|
|
525
|
+
expect(importsPerModule.get('features/consumer.ts')).toEqual([
|
|
526
|
+
{ fromPath: '../shared/types', typeNames: ['SharedEvent'] },
|
|
527
|
+
]);
|
|
528
|
+
|
|
529
|
+
// Shared module should export SharedEvent
|
|
530
|
+
expect(exportsPerModule.get('shared/types.ts')).toEqual(new Set(['SharedEvent']));
|
|
531
|
+
expect(exportsPerModule.has('features/consumer.ts')).toBe(false);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('aggregates exports from multiple importing modules', () => {
|
|
535
|
+
const model: Model = {
|
|
536
|
+
variant: 'specs',
|
|
537
|
+
narratives: [
|
|
538
|
+
{ name: 'Shared', id: 'shared-1', slices: [] },
|
|
539
|
+
{
|
|
540
|
+
name: 'Consumer1',
|
|
541
|
+
id: 'consumer-1',
|
|
542
|
+
slices: [
|
|
543
|
+
{
|
|
544
|
+
name: 'test',
|
|
545
|
+
type: 'command',
|
|
546
|
+
client: { specs: [] },
|
|
547
|
+
server: {
|
|
548
|
+
description: 'Test',
|
|
549
|
+
specs: [
|
|
550
|
+
{
|
|
551
|
+
type: 'gherkin',
|
|
552
|
+
feature: 'Test',
|
|
553
|
+
rules: [
|
|
554
|
+
{
|
|
555
|
+
name: 'rule',
|
|
556
|
+
examples: [{ name: 'ex', steps: [{ keyword: 'Then', text: 'EventA' }] }],
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
},
|
|
560
|
+
],
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
],
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
name: 'Consumer2',
|
|
567
|
+
id: 'consumer-2',
|
|
568
|
+
slices: [
|
|
569
|
+
{
|
|
570
|
+
name: 'test',
|
|
571
|
+
type: 'command',
|
|
572
|
+
client: { specs: [] },
|
|
573
|
+
server: {
|
|
574
|
+
description: 'Test',
|
|
575
|
+
specs: [
|
|
576
|
+
{
|
|
577
|
+
type: 'gherkin',
|
|
578
|
+
feature: 'Test',
|
|
579
|
+
rules: [
|
|
580
|
+
{
|
|
581
|
+
name: 'rule',
|
|
582
|
+
examples: [{ name: 'ex', steps: [{ keyword: 'Then', text: 'EventB' }] }],
|
|
583
|
+
},
|
|
584
|
+
],
|
|
585
|
+
},
|
|
586
|
+
],
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
],
|
|
590
|
+
},
|
|
591
|
+
],
|
|
592
|
+
messages: [
|
|
593
|
+
{ type: 'event', source: 'internal', name: 'EventA', fields: [] },
|
|
594
|
+
{ type: 'event', source: 'internal', name: 'EventB', fields: [] },
|
|
595
|
+
],
|
|
596
|
+
integrations: [],
|
|
597
|
+
modules: [
|
|
598
|
+
{
|
|
599
|
+
sourceFile: 'shared.ts',
|
|
600
|
+
isDerived: false,
|
|
601
|
+
contains: { narrativeIds: ['shared-1'] },
|
|
602
|
+
declares: {
|
|
603
|
+
messages: [
|
|
604
|
+
{ kind: 'event', name: 'EventA' },
|
|
605
|
+
{ kind: 'event', name: 'EventB' },
|
|
606
|
+
],
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
sourceFile: 'consumer1.ts',
|
|
611
|
+
isDerived: false,
|
|
612
|
+
contains: { narrativeIds: ['consumer-1'] },
|
|
613
|
+
declares: { messages: [] },
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
sourceFile: 'consumer2.ts',
|
|
617
|
+
isDerived: false,
|
|
618
|
+
contains: { narrativeIds: ['consumer-2'] },
|
|
619
|
+
declares: { messages: [] },
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
const { exportsPerModule } = computeAllCrossModuleDependencies(model.modules, model);
|
|
625
|
+
|
|
626
|
+
expect(exportsPerModule.get('shared.ts')).toEqual(new Set(['EventA', 'EventB']));
|
|
627
|
+
});
|
|
628
|
+
});
|
|
@@ -6,20 +6,91 @@ import type { CrossModuleImport } from './types';
|
|
|
6
6
|
|
|
7
7
|
export type { CrossModuleImport };
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Computes cross-module imports for a single module.
|
|
11
|
+
* Returns the list of imports needed (with relative paths).
|
|
12
|
+
*/
|
|
9
13
|
export function computeCrossModuleImports(module: Module, allModules: Module[], model: Model): CrossModuleImport[] {
|
|
10
|
-
|
|
14
|
+
const dependencyMap = computeModuleDependencies(module, allModules, model);
|
|
15
|
+
if (!dependencyMap) {
|
|
11
16
|
return [];
|
|
12
17
|
}
|
|
18
|
+
return convertToImports(module.sourceFile, dependencyMap);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Computes cross-module imports for all modules and derives which types need to be exported.
|
|
23
|
+
* Returns both the imports per module and the exports per module in a single pass.
|
|
24
|
+
*/
|
|
25
|
+
export function computeAllCrossModuleDependencies(
|
|
26
|
+
modules: Module[],
|
|
27
|
+
model: Model,
|
|
28
|
+
): {
|
|
29
|
+
importsPerModule: Map<string, CrossModuleImport[]>;
|
|
30
|
+
exportsPerModule: Map<string, Set<string>>;
|
|
31
|
+
} {
|
|
32
|
+
const importsPerModule = new Map<string, CrossModuleImport[]>();
|
|
33
|
+
const exportsPerModule = new Map<string, Set<string>>();
|
|
34
|
+
|
|
35
|
+
for (const module of modules) {
|
|
36
|
+
const dependencyMap = computeModuleDependencies(module, modules, model);
|
|
37
|
+
|
|
38
|
+
if (!dependencyMap) {
|
|
39
|
+
importsPerModule.set(module.sourceFile, []);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Convert to imports with relative paths
|
|
44
|
+
importsPerModule.set(module.sourceFile, convertToImports(module.sourceFile, dependencyMap));
|
|
45
|
+
|
|
46
|
+
// Track which types need to be exported from each declaring module
|
|
47
|
+
for (const [declaringSourceFile, typeNames] of dependencyMap) {
|
|
48
|
+
if (!exportsPerModule.has(declaringSourceFile)) {
|
|
49
|
+
exportsPerModule.set(declaringSourceFile, new Set());
|
|
50
|
+
}
|
|
51
|
+
for (const typeName of typeNames) {
|
|
52
|
+
exportsPerModule.get(declaringSourceFile)!.add(typeName);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { importsPerModule, exportsPerModule };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resolveRelativeImport(fromPath: string, toPath: string): string {
|
|
61
|
+
const fromDir = dirname(fromPath);
|
|
62
|
+
const toDir = dirname(toPath);
|
|
63
|
+
const toFile = basename(toPath, extname(toPath));
|
|
64
|
+
|
|
65
|
+
const relativePath = relative(fromDir, toDir);
|
|
66
|
+
if (relativePath === '') {
|
|
67
|
+
return `./${toFile}`;
|
|
68
|
+
}
|
|
69
|
+
if (!relativePath.startsWith('.')) {
|
|
70
|
+
return `./${relativePath}/${toFile}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return join(relativePath, toFile);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Core logic: computes which types a module needs from other modules.
|
|
78
|
+
* Returns a map of declaringModuleSourceFile -> typeNames[], or null if no dependencies.
|
|
79
|
+
*/
|
|
80
|
+
function computeModuleDependencies(module: Module, allModules: Module[], model: Model): Map<string, string[]> | null {
|
|
81
|
+
if (module.isDerived) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
13
84
|
|
|
14
85
|
const declaredKeys = new Set(module.declares.messages.map((m) => toMessageKey(m.kind, m.name)));
|
|
15
86
|
const usedKeys = collectUsedMessageKeysForModule(module, model);
|
|
16
87
|
const neededKeys = new Set([...usedKeys].filter((k) => !declaredKeys.has(k)));
|
|
17
88
|
|
|
18
89
|
if (neededKeys.size === 0) {
|
|
19
|
-
return
|
|
90
|
+
return null;
|
|
20
91
|
}
|
|
21
92
|
|
|
22
|
-
const
|
|
93
|
+
const dependencyMap = new Map<string, string[]>();
|
|
23
94
|
|
|
24
95
|
for (const msgKey of neededKeys) {
|
|
25
96
|
const { name } = parseMessageKey(msgKey);
|
|
@@ -27,36 +98,28 @@ export function computeCrossModuleImports(module: Module, allModules: Module[],
|
|
|
27
98
|
|
|
28
99
|
if (declaringModule) {
|
|
29
100
|
const modulePath = declaringModule.sourceFile;
|
|
30
|
-
if (!
|
|
31
|
-
|
|
101
|
+
if (!dependencyMap.has(modulePath)) {
|
|
102
|
+
dependencyMap.set(modulePath, []);
|
|
32
103
|
}
|
|
33
|
-
|
|
104
|
+
dependencyMap.get(modulePath)!.push(name);
|
|
34
105
|
}
|
|
35
106
|
}
|
|
36
107
|
|
|
37
|
-
|
|
38
|
-
for (const [modulePath, typeNames] of importsByModule) {
|
|
39
|
-
const relativePath = resolveRelativeImport(module.sourceFile, modulePath);
|
|
40
|
-
imports.push({ fromPath: relativePath, typeNames });
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return sortImportsBySource(imports);
|
|
108
|
+
return dependencyMap.size > 0 ? dependencyMap : null;
|
|
44
109
|
}
|
|
45
110
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Converts a dependency map (absolute paths) to CrossModuleImport[] (relative paths).
|
|
113
|
+
*/
|
|
114
|
+
function convertToImports(fromSourceFile: string, dependencyMap: Map<string, string[]>): CrossModuleImport[] {
|
|
115
|
+
const imports: CrossModuleImport[] = [];
|
|
50
116
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
if (!relativePath.startsWith('.')) {
|
|
56
|
-
return `./${relativePath}/${toFile}`;
|
|
117
|
+
for (const [modulePath, typeNames] of dependencyMap) {
|
|
118
|
+
const relativePath = resolveRelativeImport(fromSourceFile, modulePath);
|
|
119
|
+
imports.push({ fromPath: relativePath, typeNames });
|
|
57
120
|
}
|
|
58
121
|
|
|
59
|
-
return
|
|
122
|
+
return sortImportsBySource(imports);
|
|
60
123
|
}
|
|
61
124
|
|
|
62
125
|
function collectUsedMessageKeysForModule(module: Module, model: Model): Set<string> {
|
|
@@ -2,7 +2,7 @@ import ts from 'typescript';
|
|
|
2
2
|
import type { Model, Module } from '../../../index';
|
|
3
3
|
import { deriveModules } from '../../narrative-to-model/derive-modules';
|
|
4
4
|
import { analyzeCodeUsage } from '../analysis/usage';
|
|
5
|
-
import {
|
|
5
|
+
import { computeAllCrossModuleDependencies } from '../cross-module-imports';
|
|
6
6
|
import { sortFilesByPath } from '../ordering';
|
|
7
7
|
import type { CrossModuleImport, GeneratedFile } from '../types';
|
|
8
8
|
import { extractTypeIntegrationNames } from '../utils/integration-extractor';
|
|
@@ -22,11 +22,15 @@ export function generateAllModulesCode(model: Model, opts: GenerateModuleCodeOpt
|
|
|
22
22
|
const modules =
|
|
23
23
|
model.modules && model.modules.length > 0 ? model.modules : deriveModules(model.narratives, model.messages);
|
|
24
24
|
|
|
25
|
+
// Compute all cross-module dependencies in a single pass
|
|
26
|
+
const { importsPerModule, exportsPerModule } = computeAllCrossModuleDependencies(modules, model);
|
|
27
|
+
|
|
25
28
|
const files: GeneratedFile[] = [];
|
|
26
29
|
|
|
27
30
|
for (const module of modules) {
|
|
28
|
-
const crossModuleImports =
|
|
29
|
-
const
|
|
31
|
+
const crossModuleImports = importsPerModule.get(module.sourceFile) ?? [];
|
|
32
|
+
const exportedTypes = exportsPerModule.get(module.sourceFile);
|
|
33
|
+
const code = generateModuleCode(module, model, opts, crossModuleImports, exportedTypes);
|
|
30
34
|
files.push({
|
|
31
35
|
path: module.sourceFile,
|
|
32
36
|
code,
|
|
@@ -53,6 +57,7 @@ function generateModuleCode(
|
|
|
53
57
|
model: Model,
|
|
54
58
|
opts: GenerateModuleCodeOptions,
|
|
55
59
|
crossModuleImports: CrossModuleImport[],
|
|
60
|
+
exportedTypes?: Set<string>,
|
|
56
61
|
): string {
|
|
57
62
|
const f = ts.factory;
|
|
58
63
|
|
|
@@ -130,6 +135,7 @@ function generateModuleCode(
|
|
|
130
135
|
usedFlowFunctionNames,
|
|
131
136
|
narratives,
|
|
132
137
|
crossModuleImports,
|
|
138
|
+
exportedTypes,
|
|
133
139
|
);
|
|
134
140
|
|
|
135
141
|
const file = f.createSourceFile(statements, f.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
|
|
@@ -146,6 +152,7 @@ function buildStatements(
|
|
|
146
152
|
usedFlowFunctionNames: string[],
|
|
147
153
|
flows: Model['narratives'],
|
|
148
154
|
crossModuleImports: CrossModuleImport[],
|
|
155
|
+
exportedTypes?: Set<string>,
|
|
149
156
|
): ts.Statement[] {
|
|
150
157
|
const f = tsModule.factory;
|
|
151
158
|
const statements: ts.Statement[] = [];
|
|
@@ -185,7 +192,7 @@ function buildStatements(
|
|
|
185
192
|
required: field.required,
|
|
186
193
|
})),
|
|
187
194
|
}));
|
|
188
|
-
statements.push(...buildTypeAliases(tsModule, adaptedMessages));
|
|
195
|
+
statements.push(...buildTypeAliases(tsModule, adaptedMessages, exportedTypes));
|
|
189
196
|
|
|
190
197
|
for (const flow of flows) {
|
|
191
198
|
statements.push(...buildFlowStatements(tsModule, flow, messages));
|
|
@@ -7,7 +7,7 @@ type Message = {
|
|
|
7
7
|
fields: { name: string; type: string; required: boolean }[];
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
export function buildTypeAliases(ts: typeof tsNS, messages: Message[]): tsNS.Statement[] {
|
|
10
|
+
export function buildTypeAliases(ts: typeof tsNS, messages: Message[], exportedTypes?: Set<string>): tsNS.Statement[] {
|
|
11
11
|
const f = ts.factory;
|
|
12
12
|
|
|
13
13
|
const mkK = (s: string) => f.createLiteralTypeNode(f.createStringLiteral(s, true));
|
|
@@ -37,11 +37,9 @@ export function buildTypeAliases(ts: typeof tsNS, messages: Message[]): tsNS.Sta
|
|
|
37
37
|
m.type === 'event' ? 'Event' : m.type === 'command' ? 'Command' : m.type === 'query' ? 'Query' : 'State';
|
|
38
38
|
const rhs = f.createTypeReferenceNode(baseTypeName, typeArgs);
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
rhs,
|
|
45
|
-
);
|
|
40
|
+
// Add export modifier if this type is imported by other modules
|
|
41
|
+
const modifiers = exportedTypes?.has(m.name) ? [f.createModifier(ts.SyntaxKind.ExportKeyword)] : undefined;
|
|
42
|
+
|
|
43
|
+
return f.createTypeAliasDeclaration(modifiers, name, [], rhs);
|
|
46
44
|
});
|
|
47
45
|
}
|
|
@@ -152,12 +152,13 @@ describe('module functionality', () => {
|
|
|
152
152
|
|
|
153
153
|
expect(result.files).toHaveLength(2);
|
|
154
154
|
|
|
155
|
-
// Find the orders file
|
|
156
155
|
const ordersFile = result.files.find((f) => f.path.includes('orders'));
|
|
157
156
|
expect(ordersFile).toBeDefined();
|
|
158
157
|
|
|
159
|
-
// Orders file should import OrderCreated from shared
|
|
160
158
|
expect(ordersFile!.code).toContain("import type { OrderCreated } from '../shared/types.narrative';");
|
|
159
|
+
const sharedFile = result.files.find((f) => f.path.includes('types'));
|
|
160
|
+
expect(sharedFile).toBeDefined();
|
|
161
|
+
expect(sharedFile!.code).toContain('export type OrderCreated = Event<');
|
|
161
162
|
});
|
|
162
163
|
});
|
|
163
164
|
|