@open-mercato/cli 0.6.8-develop.7099.1.7ba2771d06 → 0.7.0

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 (185) hide show
  1. package/.turbo/turbo-build.log +1 -7
  2. package/dist/agentic/guides/module-facts.json +598 -5508
  3. package/dist/agentic/guides/module-facts.v2.json +598 -5594
  4. package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
  5. package/dist/agentic/guides/modules/api_docs/index.md +1 -1
  6. package/dist/agentic/guides/modules/api_keys/exact-override-targets.md +0 -1
  7. package/dist/agentic/guides/modules/api_keys/index.md +2 -4
  8. package/dist/agentic/guides/modules/attachments/di-registrations-rich.md +5 -6
  9. package/dist/agentic/guides/modules/attachments/di-service-tokens.md +4 -5
  10. package/dist/agentic/guides/modules/attachments/exact-override-targets.md +5 -6
  11. package/dist/agentic/guides/modules/attachments/index.md +2 -2
  12. package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
  13. package/dist/agentic/guides/modules/auth/di-registrations-rich.md +2 -3
  14. package/dist/agentic/guides/modules/auth/di-service-tokens.md +2 -3
  15. package/dist/agentic/guides/modules/auth/exact-override-targets.md +2 -3
  16. package/dist/agentic/guides/modules/auth/index.md +1 -1
  17. package/dist/agentic/guides/modules/business_rules/index.md +1 -1
  18. package/dist/agentic/guides/modules/catalog/incoming-installed-contributions.md +0 -3
  19. package/dist/agentic/guides/modules/catalog/index.md +2 -2
  20. package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
  21. package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
  22. package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
  23. package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
  24. package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
  25. package/dist/agentic/guides/modules/checkout/index.md +1 -1
  26. package/dist/agentic/guides/modules/communication_channels/di-registrations-rich.md +8 -9
  27. package/dist/agentic/guides/modules/communication_channels/exact-override-targets.md +8 -9
  28. package/dist/agentic/guides/modules/communication_channels/incoming-installed-contributions.md +0 -2
  29. package/dist/agentic/guides/modules/communication_channels/index.md +3 -3
  30. package/dist/agentic/guides/modules/configs/index.md +1 -1
  31. package/dist/agentic/guides/modules/content/index.md +1 -1
  32. package/dist/agentic/guides/modules/currencies/index.md +1 -1
  33. package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
  34. package/dist/agentic/guides/modules/customers/active-extension-bindings.md +3 -6
  35. package/dist/agentic/guides/modules/customers/contribution-resolutions.md +0 -1
  36. package/dist/agentic/guides/modules/customers/events.md +1 -2
  37. package/dist/agentic/guides/modules/customers/incoming-installed-contributions.md +0 -7
  38. package/dist/agentic/guides/modules/customers/index.md +4 -4
  39. package/dist/agentic/guides/modules/customers/umes-contributions.md +0 -1
  40. package/dist/agentic/guides/modules/customers/umes-hosts.md +0 -1
  41. package/dist/agentic/guides/modules/dashboards/index.md +1 -1
  42. package/dist/agentic/guides/modules/data_sync/exact-override-targets.md +2 -2
  43. package/dist/agentic/guides/modules/data_sync/index.md +1 -1
  44. package/dist/agentic/guides/modules/data_sync/workers.md +2 -2
  45. package/dist/agentic/guides/modules/design_system/index.md +1 -1
  46. package/dist/agentic/guides/modules/devices/index.md +1 -1
  47. package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
  48. package/dist/agentic/guides/modules/directory/exact-override-targets.md +0 -2
  49. package/dist/agentic/guides/modules/directory/index.md +3 -5
  50. package/dist/agentic/guides/modules/entities/index.md +1 -1
  51. package/dist/agentic/guides/modules/eudr/index.md +1 -1
  52. package/dist/agentic/guides/modules/events/index.md +1 -1
  53. package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
  54. package/dist/agentic/guides/modules/gateway_stripe/exact-override-targets.md +1 -1
  55. package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
  56. package/dist/agentic/guides/modules/gateway_stripe/workers.md +1 -1
  57. package/dist/agentic/guides/modules/generators/index.md +1 -1
  58. package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
  59. package/dist/agentic/guides/modules/integrations/index.md +1 -1
  60. package/dist/agentic/guides/modules/messages/index.md +1 -1
  61. package/dist/agentic/guides/modules/notifications/index.md +1 -1
  62. package/dist/agentic/guides/modules/onboarding/index.md +1 -1
  63. package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
  64. package/dist/agentic/guides/modules/perspectives/index.md +1 -1
  65. package/dist/agentic/guides/modules/planner/index.md +1 -1
  66. package/dist/agentic/guides/modules/portal/index.md +1 -1
  67. package/dist/agentic/guides/modules/progress/index.md +1 -1
  68. package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
  69. package/dist/agentic/guides/modules/query_index/index.md +1 -1
  70. package/dist/agentic/guides/modules/record_locks/index.md +1 -1
  71. package/dist/agentic/guides/modules/resources/index.md +1 -1
  72. package/dist/agentic/guides/modules/sales/active-extension-bindings.md +0 -1
  73. package/dist/agentic/guides/modules/sales/contribution-resolutions.md +0 -1
  74. package/dist/agentic/guides/modules/sales/exact-override-targets.md +4 -5
  75. package/dist/agentic/guides/modules/sales/host-extension-points.md +1 -1
  76. package/dist/agentic/guides/modules/sales/incoming-installed-contributions.md +0 -2
  77. package/dist/agentic/guides/modules/sales/index.md +3 -3
  78. package/dist/agentic/guides/modules/sales/setup.md +1 -1
  79. package/dist/agentic/guides/modules/sales/umes-contributions.md +0 -1
  80. package/dist/agentic/guides/modules/sales/umes-diagnostics.md +0 -1
  81. package/dist/agentic/guides/modules/sales/umes-hosts.md +0 -9
  82. package/dist/agentic/guides/modules/scheduler/cli-commands.md +0 -1
  83. package/dist/agentic/guides/modules/scheduler/exact-override-targets.md +1 -3
  84. package/dist/agentic/guides/modules/scheduler/index.md +2 -2
  85. package/dist/agentic/guides/modules/scheduler/workers.md +1 -2
  86. package/dist/agentic/guides/modules/search/index.md +1 -1
  87. package/dist/agentic/guides/modules/security/index.md +1 -1
  88. package/dist/agentic/guides/modules/security/owned-contract-module-metadata.md +1 -1
  89. package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
  90. package/dist/agentic/guides/modules/sso/index.md +1 -1
  91. package/dist/agentic/guides/modules/staff/index.md +1 -1
  92. package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
  93. package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
  94. package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
  95. package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
  96. package/dist/agentic/guides/modules/translations/index.md +1 -1
  97. package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
  98. package/dist/agentic/guides/modules/webhooks/index.md +1 -1
  99. package/dist/agentic/guides/modules/wms/index.md +1 -1
  100. package/dist/agentic/guides/modules/workflows/index.md +1 -1
  101. package/dist/agentic/guides/reference-module-facts.json +2 -2
  102. package/dist/agentic/guides/reference-modules/example/index.md +1 -1
  103. package/dist/agentic/guides/upstream/manifest.json +1 -1
  104. package/dist/agentic/shared/AGENTS.md.template +3 -3
  105. package/dist/agentic/shared/ai/harness/README.md +2 -2
  106. package/dist/agentic/shared/ai/harness/RELEASE.md +6 -8
  107. package/dist/agentic/shared/ai/harness/cases.json +0 -83
  108. package/dist/agentic/shared/ai/harness/cases.schema.json +4 -4
  109. package/dist/agentic/shared/ai/harness/validators.json +1 -1
  110. package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.7-to-0.7.0/SKILL.md +12 -3
  111. package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +11 -37
  112. package/dist/lib/generators/ast/index.js +1 -4
  113. package/dist/lib/generators/ast/index.js.map +2 -2
  114. package/dist/lib/generators/ast/source-file.js +1 -8
  115. package/dist/lib/generators/ast/source-file.js.map +2 -2
  116. package/dist/lib/generators/ast/writers.js +1 -9
  117. package/dist/lib/generators/ast/writers.js.map +2 -2
  118. package/dist/lib/generators/module-registry.js +189 -424
  119. package/dist/lib/generators/module-registry.js.map +2 -2
  120. package/dist/lib/generators/route-manifest-shards.js +1 -4
  121. package/dist/lib/generators/route-manifest-shards.js.map +2 -2
  122. package/dist/lib/spawn.js +2 -3
  123. package/dist/lib/spawn.js.map +2 -2
  124. package/dist/lib/testing/integration.js +24 -162
  125. package/dist/lib/testing/integration.js.map +2 -2
  126. package/dist/mercato.js +0 -14
  127. package/dist/mercato.js.map +2 -2
  128. package/package.json +7 -8
  129. package/src/lib/__tests__/spawn.test.ts +0 -23
  130. package/src/lib/generators/__tests__/metadata-extraction.test.ts +0 -44
  131. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +8 -32
  132. package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +1 -4
  133. package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +1 -1
  134. package/src/lib/generators/__tests__/module-subset.test.ts +72 -96
  135. package/src/lib/generators/__tests__/output-snapshots.test.ts +1 -1
  136. package/src/lib/generators/__tests__/route-manifest-shards.test.ts +0 -15
  137. package/src/lib/generators/__tests__/structural-contracts.test.ts +3 -7
  138. package/src/lib/generators/ast/index.ts +1 -2
  139. package/src/lib/generators/ast/source-file.ts +0 -13
  140. package/src/lib/generators/ast/writers.ts +1 -26
  141. package/src/lib/generators/module-registry.ts +227 -534
  142. package/src/lib/generators/route-manifest-shards.ts +1 -8
  143. package/src/lib/spawn.ts +4 -5
  144. package/src/lib/testing/__tests__/integration.test.ts +0 -435
  145. package/src/lib/testing/integration.ts +32 -254
  146. package/src/mercato.ts +0 -27
  147. package/dist/agentic/guides/modules/api_keys/di-registrations-rich.md +0 -11
  148. package/dist/agentic/guides/modules/api_keys/di-service-tokens.md +0 -11
  149. package/dist/agentic/guides/modules/channel_discord/acl-features.md +0 -13
  150. package/dist/agentic/guides/modules/channel_discord/ai-agents.md +0 -11
  151. package/dist/agentic/guides/modules/channel_discord/backend-pages.md +0 -11
  152. package/dist/agentic/guides/modules/channel_discord/cli-commands.md +0 -13
  153. package/dist/agentic/guides/modules/channel_discord/contribution-resolutions.md +0 -14
  154. package/dist/agentic/guides/modules/channel_discord/di-registrations-rich.md +0 -12
  155. package/dist/agentic/guides/modules/channel_discord/exact-override-targets.md +0 -26
  156. package/dist/agentic/guides/modules/channel_discord/index.md +0 -22
  157. package/dist/agentic/guides/modules/channel_discord/owned-contract-module-metadata.md +0 -11
  158. package/dist/agentic/guides/modules/channel_discord/setup.md +0 -11
  159. package/dist/agentic/guides/modules/channel_discord/umes-contributions.md +0 -14
  160. package/dist/agentic/guides/modules/channel_discord/workers.md +0 -12
  161. package/dist/agentic/guides/modules/directory/di-registrations-rich.md +0 -12
  162. package/dist/agentic/guides/modules/directory/di-service-tokens.md +0 -12
  163. package/dist/agentic/guides/modules/documents/acl-features.md +0 -17
  164. package/dist/agentic/guides/modules/documents/backend-pages.md +0 -13
  165. package/dist/agentic/guides/modules/documents/contribution-resolutions.md +0 -27
  166. package/dist/agentic/guides/modules/documents/di-registrations-rich.md +0 -21
  167. package/dist/agentic/guides/modules/documents/domain-commands.md +0 -37
  168. package/dist/agentic/guides/modules/documents/encryption.md +0 -11
  169. package/dist/agentic/guides/modules/documents/entities.md +0 -21
  170. package/dist/agentic/guides/modules/documents/events.md +0 -25
  171. package/dist/agentic/guides/modules/documents/exact-override-targets.md +0 -39
  172. package/dist/agentic/guides/modules/documents/index.md +0 -25
  173. package/dist/agentic/guides/modules/documents/notifications.md +0 -13
  174. package/dist/agentic/guides/modules/documents/owned-contract-module-metadata.md +0 -11
  175. package/dist/agentic/guides/modules/documents/setup.md +0 -11
  176. package/dist/agentic/guides/modules/documents/umes-contributions.md +0 -27
  177. package/dist/agentic/guides/modules/documents/umes-hosts.md +0 -36
  178. package/src/__tests__/resolve-queue-abandon-hook.test.ts +0 -39
  179. package/src/lib/generators/__tests__/__snapshots__/bootstrap-registrations.test.ts.snap +0 -14
  180. package/src/lib/generators/__tests__/__snapshots__/command-loaders.test.ts.snap +0 -23
  181. package/src/lib/generators/__tests__/ast-writers-escaping.test.ts +0 -64
  182. package/src/lib/generators/__tests__/bootstrap-registrations.test.ts +0 -176
  183. package/src/lib/generators/__tests__/command-loaders.test.ts +0 -239
  184. package/src/lib/generators/__tests__/generated-registry-imports.test.ts +0 -128
  185. package/src/lib/generators/__tests__/registry-variant-parity.test.ts +0 -258
@@ -1,176 +0,0 @@
1
- /**
2
- * Contract tests for `renderBootstrapRegistrationsFile`, the emitter behind
3
- * `bootstrap-registrations.generated.ts`.
4
- *
5
- * The file is always written — even when no generator plugin contributes a bootstrap
6
- * registration — so `bootstrap.ts` can import it unconditionally. These tests cover
7
- * that core-only case, one plugin, two plugins whose registration imports overlap
8
- * (de-duplication), and a malformed import statement, which must fail at generation
9
- * time with the offending statement named rather than emit a broken file.
10
- */
11
- import ts from 'typescript-js'
12
- import { renderBootstrapRegistrationsFile } from '../module-registry'
13
-
14
- const CORE_ENTRY_IMPORTS = [
15
- 'import { backendRoutes } from "./backend-routes.generated"',
16
- 'import { frontendRoutes } from "./frontend-routes.generated"',
17
- ]
18
- const CORE_REGISTRATION_IMPORTS = [
19
- "import { registerBackendRouteManifests, registerFrontendRouteManifests } from '@open-mercato/shared/modules/registry'",
20
- ]
21
- const CORE_CALLS = [
22
- 'registerBackendRouteManifests(backendRoutes)',
23
- 'registerFrontendRouteManifests(frontendRoutes)',
24
- ]
25
-
26
- function parseFile(content: string): ts.SourceFile {
27
- return ts.createSourceFile('bootstrap-registrations.generated.ts', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
28
- }
29
-
30
- function expectNoSyntaxErrors(content: string): void {
31
- const sourceFile = parseFile(content)
32
- const diagnostics = (sourceFile as unknown as { parseDiagnostics?: ts.Diagnostic[] }).parseDiagnostics ?? []
33
- expect(diagnostics.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, ' '))).toEqual([])
34
- }
35
-
36
- function parseImports(content: string): Array<{ moduleSpecifier: string; names: string[] }> {
37
- return parseFile(content)
38
- .statements.filter(ts.isImportDeclaration)
39
- .map((declaration) => {
40
- const bindings = declaration.importClause?.namedBindings
41
- return {
42
- moduleSpecifier: (declaration.moduleSpecifier as ts.StringLiteral).text,
43
- names: bindings && ts.isNamedImports(bindings) ? bindings.elements.map((element) => element.name.text) : [],
44
- }
45
- })
46
- }
47
-
48
- function parseRegistrationCalls(content: string): string[] {
49
- const sourceFile = parseFile(content)
50
- const declaration = sourceFile.statements.find(
51
- (statement): statement is ts.FunctionDeclaration =>
52
- ts.isFunctionDeclaration(statement) && statement.name?.text === 'runBootstrapRegistrations',
53
- )
54
- if (!declaration) throw new Error('runBootstrapRegistrations declaration not found')
55
- return (declaration.body?.statements ?? []).map((statement) => statement.getText(sourceFile).replace(/;$/, ''))
56
- }
57
-
58
- function isExportedVoidFunction(content: string): boolean {
59
- const declaration = parseFile(content).statements.find(
60
- (statement): statement is ts.FunctionDeclaration =>
61
- ts.isFunctionDeclaration(statement) && statement.name?.text === 'runBootstrapRegistrations',
62
- )
63
- if (!declaration) return false
64
- const isExported = declaration.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false
65
- return isExported && declaration.type?.kind === ts.SyntaxKind.VoidKeyword
66
- }
67
-
68
- describe('renderBootstrapRegistrationsFile', () => {
69
- it('emits the core route registrations when no plugin contributes one', () => {
70
- const content = renderBootstrapRegistrationsFile({
71
- entryImports: CORE_ENTRY_IMPORTS,
72
- registrationImports: CORE_REGISTRATION_IMPORTS,
73
- calls: CORE_CALLS,
74
- })
75
-
76
- expectNoSyntaxErrors(content)
77
- expect(content.startsWith('// AUTO-GENERATED by mercato generate registry\n')).toBe(true)
78
- expect(isExportedVoidFunction(content)).toBe(true)
79
- expect(parseImports(content)).toEqual([
80
- { moduleSpecifier: './backend-routes.generated', names: ['backendRoutes'] },
81
- { moduleSpecifier: './frontend-routes.generated', names: ['frontendRoutes'] },
82
- {
83
- moduleSpecifier: '@open-mercato/shared/modules/registry',
84
- names: ['registerBackendRouteManifests', 'registerFrontendRouteManifests'],
85
- },
86
- ])
87
- expect(parseRegistrationCalls(content)).toEqual(CORE_CALLS)
88
- })
89
-
90
- it('appends a single plugin registration after the core ones', () => {
91
- const content = renderBootstrapRegistrationsFile({
92
- entryImports: [
93
- ...CORE_ENTRY_IMPORTS,
94
- 'import { securityMfaProviderEntries } from "./security-mfa-providers.generated"',
95
- ],
96
- registrationImports: [
97
- ...CORE_REGISTRATION_IMPORTS,
98
- "import { registerMfaProviders } from '@open-mercato/core/modules/security/mfa'",
99
- ],
100
- calls: [...CORE_CALLS, 'registerMfaProviders(securityMfaProviderEntries)'],
101
- })
102
-
103
- expectNoSyntaxErrors(content)
104
- expect(parseImports(content).map((parsed) => parsed.moduleSpecifier)).toEqual([
105
- './backend-routes.generated',
106
- './frontend-routes.generated',
107
- './security-mfa-providers.generated',
108
- '@open-mercato/shared/modules/registry',
109
- '@open-mercato/core/modules/security/mfa',
110
- ])
111
- expect(parseRegistrationCalls(content)).toEqual([
112
- ...CORE_CALLS,
113
- 'registerMfaProviders(securityMfaProviderEntries)',
114
- ])
115
- })
116
-
117
- it('de-duplicates an identical registration import contributed by two plugins', () => {
118
- const sharedRegistrationImport = "import { registerEntries } from '@open-mercato/core/modules/shared/registrations'"
119
- const content = renderBootstrapRegistrationsFile({
120
- entryImports: [
121
- ...CORE_ENTRY_IMPORTS,
122
- 'import { firstPluginEntries } from "./first-plugin.generated"',
123
- 'import { secondPluginEntries } from "./second-plugin.generated"',
124
- ],
125
- registrationImports: [...CORE_REGISTRATION_IMPORTS, sharedRegistrationImport, sharedRegistrationImport],
126
- calls: [...CORE_CALLS, 'registerEntries(firstPluginEntries)', 'registerEntries(secondPluginEntries)'],
127
- })
128
-
129
- expectNoSyntaxErrors(content)
130
- expect(parseImports(content).filter((parsed) => parsed.names.includes('registerEntries'))).toHaveLength(1)
131
- expect(parseRegistrationCalls(content)).toEqual([
132
- ...CORE_CALLS,
133
- 'registerEntries(firstPluginEntries)',
134
- 'registerEntries(secondPluginEntries)',
135
- ])
136
- })
137
-
138
- it('emits a valid module with an empty body when nothing registers at all', () => {
139
- const content = renderBootstrapRegistrationsFile({ entryImports: [], registrationImports: [], calls: [] })
140
-
141
- expectNoSyntaxErrors(content)
142
- expect(isExportedVoidFunction(content)).toBe(true)
143
- expect(parseImports(content)).toEqual([])
144
- expect(parseRegistrationCalls(content)).toEqual([])
145
- })
146
-
147
- it('fails at generation time and names the statement when a plugin import is malformed', () => {
148
- expect(() =>
149
- renderBootstrapRegistrationsFile({
150
- entryImports: CORE_ENTRY_IMPORTS,
151
- registrationImports: [...CORE_REGISTRATION_IMPORTS, 'registerSomething from "./not-an-import"'],
152
- calls: CORE_CALLS,
153
- }),
154
- ).toThrow('Expected import declaration, received: registerSomething from "./not-an-import"')
155
- })
156
-
157
- it('fails when a plugin import uses a non-literal module specifier', () => {
158
- expect(() =>
159
- renderBootstrapRegistrationsFile({
160
- entryImports: CORE_ENTRY_IMPORTS,
161
- registrationImports: [...CORE_REGISTRATION_IMPORTS, 'import { registerSomething } from `./templated`'],
162
- calls: CORE_CALLS,
163
- }),
164
- ).toThrow('Unsupported import declaration: import { registerSomething } from `./templated`')
165
- })
166
-
167
- it('pins the emitted text so emitter changes surface as a reviewable diff', () => {
168
- expect(
169
- renderBootstrapRegistrationsFile({
170
- entryImports: CORE_ENTRY_IMPORTS,
171
- registrationImports: CORE_REGISTRATION_IMPORTS,
172
- calls: CORE_CALLS,
173
- }),
174
- ).toMatchSnapshot()
175
- })
176
- })
@@ -1,239 +0,0 @@
1
- /**
2
- * Contract tests for `renderCommandLoadersFile`, the emitter behind
3
- * `command-loaders.generated.ts`.
4
- *
5
- * The emitter is exercised directly rather than through a full generate run so the
6
- * assertions stay readable and fast. Expectations are structural — the file is parsed
7
- * and its exported array is inspected — so they survive the formatting differences a
8
- * ts-morph emitter introduces. One inline snapshot pins the emitted text as well, so a
9
- * reviewer can see the formatting delta of any future emitter change in the diff.
10
- */
11
- import ts from 'typescript-js'
12
- import { renderCommandLoadersFile, type CommandLoaderGenerationEntry } from '../module-registry'
13
-
14
- type ParsedLoaderEntry = {
15
- moduleId?: string
16
- id?: string
17
- key?: string
18
- loadImportPath?: string
19
- propertyOrder: string[]
20
- }
21
-
22
- function parseFile(content: string): ts.SourceFile {
23
- return ts.createSourceFile('command-loaders.generated.ts', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
24
- }
25
-
26
- function expectNoSyntaxErrors(content: string): void {
27
- const sourceFile = parseFile(content)
28
- const diagnostics = (sourceFile as unknown as { parseDiagnostics?: ts.Diagnostic[] }).parseDiagnostics ?? []
29
- expect(diagnostics.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, ' '))).toEqual([])
30
- }
31
-
32
- function findExportedArray(sourceFile: ts.SourceFile, exportName: string): ts.ArrayLiteralExpression {
33
- let found: ts.ArrayLiteralExpression | null = null
34
- const visit = (node: ts.Node): void => {
35
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === exportName) {
36
- if (node.initializer && ts.isArrayLiteralExpression(node.initializer)) {
37
- found = node.initializer
38
- }
39
- }
40
- ts.forEachChild(node, visit)
41
- }
42
- visit(sourceFile)
43
- if (!found) throw new Error(`Exported array "${exportName}" not found`)
44
- return found
45
- }
46
-
47
- function readStringLiteral(node: ts.Expression): string | undefined {
48
- return ts.isStringLiteralLike(node) ? node.text : undefined
49
- }
50
-
51
- function parseTypeOnlyImports(sourceFile: ts.SourceFile): Array<{ moduleSpecifier: string; names: string[] }> {
52
- return sourceFile.statements
53
- .filter(ts.isImportDeclaration)
54
- .filter((declaration) => declaration.importClause?.isTypeOnly === true)
55
- .map((declaration) => {
56
- const bindings = declaration.importClause?.namedBindings
57
- return {
58
- moduleSpecifier: (declaration.moduleSpecifier as ts.StringLiteral).text,
59
- names: bindings && ts.isNamedImports(bindings) ? bindings.elements.map((element) => element.name.text) : [],
60
- }
61
- })
62
- }
63
-
64
- function readExportedArrayTypeAnnotation(sourceFile: ts.SourceFile, exportName: string): string | undefined {
65
- let annotation: string | undefined
66
- const visit = (node: ts.Node): void => {
67
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === exportName) {
68
- annotation = node.type?.getText(sourceFile)
69
- }
70
- ts.forEachChild(node, visit)
71
- }
72
- visit(sourceFile)
73
- return annotation
74
- }
75
-
76
- function readDefaultExportedIdentifier(sourceFile: ts.SourceFile): string | undefined {
77
- const assignment = sourceFile.statements.find(ts.isExportAssignment)
78
- if (!assignment || assignment.isExportEquals) return undefined
79
- return ts.isIdentifier(assignment.expression) ? assignment.expression.text : undefined
80
- }
81
-
82
- function readLoadImportPath(node: ts.Expression): string | undefined {
83
- if (!ts.isArrowFunction(node)) return undefined
84
- const body = node.body
85
- if (ts.isBlock(body)) return undefined
86
- if (!ts.isCallExpression(body) || body.expression.kind !== ts.SyntaxKind.ImportKeyword) return undefined
87
- const [specifier] = body.arguments
88
- return specifier ? readStringLiteral(specifier) : undefined
89
- }
90
-
91
- function parseLoaderEntries(content: string): ParsedLoaderEntry[] {
92
- const sourceFile = parseFile(content)
93
- const array = findExportedArray(sourceFile, 'commandLoaderEntries')
94
- return array.elements.map((element) => {
95
- if (!ts.isObjectLiteralExpression(element)) throw new Error('Expected an object literal entry')
96
- const parsed: ParsedLoaderEntry = { propertyOrder: [] }
97
- for (const property of element.properties) {
98
- if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) continue
99
- const name = property.name.text
100
- parsed.propertyOrder.push(name)
101
- if (name === 'moduleId') parsed.moduleId = readStringLiteral(property.initializer)
102
- if (name === 'id') parsed.id = readStringLiteral(property.initializer)
103
- if (name === 'key') parsed.key = readStringLiteral(property.initializer)
104
- if (name === 'load') parsed.loadImportPath = readLoadImportPath(property.initializer)
105
- }
106
- return parsed
107
- })
108
- }
109
-
110
- function entry(overrides: Partial<CommandLoaderGenerationEntry> = {}): CommandLoaderGenerationEntry {
111
- return {
112
- moduleId: 'catalog',
113
- key: 'catalog:commands:products',
114
- importPath: '@open-mercato/core/modules/catalog/commands/products',
115
- ids: ['catalog.products.create'],
116
- ...overrides,
117
- }
118
- }
119
-
120
- describe('renderCommandLoadersFile', () => {
121
- it('emits a parseable module with the documented exports', () => {
122
- const content = renderCommandLoadersFile([entry()])
123
-
124
- expectNoSyntaxErrors(content)
125
- expect(content.startsWith('// AUTO-GENERATED by mercato generate command-loaders\n')).toBe(true)
126
- expect(content.endsWith('\n')).toBe(true)
127
-
128
- const sourceFile = parseFile(content)
129
- expect(parseTypeOnlyImports(sourceFile)).toEqual([
130
- { moduleSpecifier: '@open-mercato/shared/lib/commands', names: ['CommandLoader'] },
131
- ])
132
- expect(readExportedArrayTypeAnnotation(sourceFile, 'commandLoaderEntries')).toBe('CommandLoader[]')
133
- expect(readDefaultExportedIdentifier(sourceFile)).toBe('commandLoaderEntries')
134
- })
135
-
136
- it('emits one id-scoped entry per command id followed by the module-scoped fallback entry', () => {
137
- const content = renderCommandLoadersFile([
138
- entry({ ids: ['catalog.products.create', 'catalog.products.delete'] }),
139
- ])
140
-
141
- expect(parseLoaderEntries(content)).toEqual([
142
- {
143
- moduleId: 'catalog',
144
- id: 'catalog.products.create',
145
- key: 'catalog:commands:products',
146
- loadImportPath: '@open-mercato/core/modules/catalog/commands/products',
147
- propertyOrder: ['moduleId', 'id', 'key', 'load'],
148
- },
149
- {
150
- moduleId: 'catalog',
151
- id: 'catalog.products.delete',
152
- key: 'catalog:commands:products',
153
- loadImportPath: '@open-mercato/core/modules/catalog/commands/products',
154
- propertyOrder: ['moduleId', 'id', 'key', 'load'],
155
- },
156
- {
157
- moduleId: 'catalog',
158
- id: undefined,
159
- key: 'catalog:commands:products',
160
- loadImportPath: '@open-mercato/core/modules/catalog/commands/products',
161
- propertyOrder: ['moduleId', 'key', 'load'],
162
- },
163
- ])
164
- })
165
-
166
- it('keeps entries from several modules in input order', () => {
167
- const content = renderCommandLoadersFile([
168
- entry(),
169
- entry({
170
- moduleId: 'sales',
171
- key: 'sales:commands:orders',
172
- importPath: '@open-mercato/core/modules/sales/commands/orders',
173
- ids: ['sales.orders.create'],
174
- }),
175
- ])
176
-
177
- expect(parseLoaderEntries(content).map((parsed) => [parsed.key, parsed.id])).toEqual([
178
- ['catalog:commands:products', 'catalog.products.create'],
179
- ['catalog:commands:products', undefined],
180
- ['sales:commands:orders', 'sales.orders.create'],
181
- ['sales:commands:orders', undefined],
182
- ])
183
- })
184
-
185
- it('emits the module-scoped entry only when a module declares no command ids', () => {
186
- const content = renderCommandLoadersFile([entry({ ids: [] })])
187
-
188
- expectNoSyntaxErrors(content)
189
- expect(parseLoaderEntries(content)).toEqual([
190
- {
191
- moduleId: 'catalog',
192
- id: undefined,
193
- key: 'catalog:commands:products',
194
- loadImportPath: '@open-mercato/core/modules/catalog/commands/products',
195
- propertyOrder: ['moduleId', 'key', 'load'],
196
- },
197
- ])
198
- })
199
-
200
- it('emits a valid module with an empty array when no module contributes commands', () => {
201
- const content = renderCommandLoadersFile([])
202
-
203
- expectNoSyntaxErrors(content)
204
- expect(parseLoaderEntries(content)).toEqual([])
205
- })
206
-
207
- it('rejects the same command id contributed by two different modules', () => {
208
- expect(() =>
209
- renderCommandLoadersFile([
210
- entry(),
211
- entry({
212
- moduleId: 'sales',
213
- key: 'sales:commands:products',
214
- importPath: '@open-mercato/core/modules/sales/commands/products',
215
- }),
216
- ]),
217
- ).toThrow(
218
- '[generate] Duplicate command id "catalog.products.create" discovered in "catalog:commands:products" and "sales:commands:products"',
219
- )
220
- })
221
-
222
- it('tolerates the same command id repeated within one module key', () => {
223
- const content = renderCommandLoadersFile([
224
- entry({ ids: ['catalog.products.create', 'catalog.products.create'] }),
225
- ])
226
-
227
- expect(parseLoaderEntries(content)).toHaveLength(3)
228
- })
229
-
230
- it('rejects an unsafe import path instead of emitting it', () => {
231
- expect(() => renderCommandLoadersFile([entry({ importPath: '' })])).toThrow(
232
- 'Unsafe generated module specifier',
233
- )
234
- })
235
-
236
- it('pins the emitted text so emitter changes surface as a reviewable diff', () => {
237
- expect(renderCommandLoadersFile([entry()])).toMatchSnapshot()
238
- })
239
- })
@@ -1,128 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
- import os from 'node:os'
4
- import type { PackageResolver, ModuleEntry } from '../../resolver'
5
- import { generateModuleRegistry, generateModuleRegistryApp, generateModuleRegistryCli } from '../module-registry'
6
-
7
- /**
8
- * The generator's output, not its helpers.
9
- *
10
- * A worker declaring `metadata.onJobAbandoned` makes the registry emit
11
- * `createLazyModuleWorkerAbandonHook(...)`, and the import for it used to be opted into per render
12
- * site. Forgetting it at one of the six sites produced a file referencing an undeclared name, which
13
- * type-checks nowhere and surfaces only at `build:app` — every unit test stayed green because no
14
- * fixture declared such a worker.
15
- *
16
- * These assertions are driven off the files the generators actually wrote, so a renderer added later
17
- * is covered without anyone remembering to extend this test.
18
- */
19
-
20
- let tmpDir: string
21
-
22
- function touchFile(filePath: string, content = ''): void {
23
- fs.mkdirSync(path.dirname(filePath), { recursive: true })
24
- fs.writeFileSync(filePath, content)
25
- }
26
-
27
- function createMockResolver(dir: string, enabled: ModuleEntry[]): PackageResolver {
28
- const outputDir = path.join(dir, 'output', 'generated')
29
- fs.mkdirSync(outputDir, { recursive: true })
30
- return {
31
- isMonorepo: () => true,
32
- getRootDir: () => dir,
33
- getAppDir: () => path.join(dir, 'app'),
34
- getOutputDir: () => outputDir,
35
- getModulesConfigPath: () => path.join(dir, 'app', 'src', 'modules.ts'),
36
- discoverPackages: () => [],
37
- loadEnabledModules: () => enabled,
38
- getModulePaths: (entry: ModuleEntry) => ({
39
- appBase: path.join(dir, 'app', 'src', 'modules', entry.id),
40
- pkgBase: path.join(dir, 'packages', 'core', 'src', 'modules', entry.id),
41
- }),
42
- getModuleImportBase: (entry: ModuleEntry) => ({
43
- appBase: `@/modules/${entry.id}`,
44
- pkgBase: `@open-mercato/core/modules/${entry.id}`,
45
- }),
46
- getPackageOutputDir: () => outputDir,
47
- getPackageRoot: () => path.join(dir, 'packages', 'core'),
48
- }
49
- }
50
-
51
- function scaffoldWorkerModule(dir: string, modId: string, workerSource: string): void {
52
- touchFile(path.join(dir, 'packages', 'core', 'src', 'modules', modId, 'workers', 'sync.ts'), workerSource)
53
- }
54
-
55
- function generatedFiles(dir: string): Array<{ name: string; content: string }> {
56
- const outputDir = path.join(dir, 'output', 'generated')
57
- if (!fs.existsSync(outputDir)) return []
58
- return fs.readdirSync(outputDir)
59
- .filter((name) => name.endsWith('.ts'))
60
- .map((name) => ({ name, content: fs.readFileSync(path.join(outputDir, name), 'utf8') }))
61
- }
62
-
63
- const ABANDON_HOOK_FACTORY = 'createLazyModuleWorkerAbandonHook'
64
-
65
- const WORKER_WITH_HOOK = `
66
- import { failAbandonedRun } from '../lib/abandoned-run'
67
- export const metadata = {
68
- queue: 'demo-queue',
69
- id: 'demo:worker',
70
- concurrency: 2,
71
- onJobAbandoned: failAbandonedRun,
72
- }
73
- export default async function handle() {}
74
- `
75
-
76
- const WORKER_WITHOUT_HOOK = `
77
- export const metadata = {
78
- queue: 'demo-queue',
79
- id: 'demo:worker',
80
- concurrency: 2,
81
- }
82
- export default async function handle() {}
83
- `
84
-
85
- async function generateAll(resolver: PackageResolver): Promise<void> {
86
- await generateModuleRegistry({ resolver, quiet: true })
87
- await generateModuleRegistryApp({ resolver, quiet: true })
88
- await generateModuleRegistryCli({ resolver, quiet: true })
89
- }
90
-
91
- beforeEach(() => {
92
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'generated-registry-imports-'))
93
- })
94
-
95
- afterEach(() => {
96
- fs.rmSync(tmpDir, { recursive: true, force: true })
97
- })
98
-
99
- describe('generated registries — emitted identifiers are imported', () => {
100
- it('imports the abandon-hook helper in every file that calls it', async () => {
101
- scaffoldWorkerModule(tmpDir, 'demo', WORKER_WITH_HOOK)
102
- const resolver = createMockResolver(tmpDir, [{ id: 'demo', from: 'pkg' } as ModuleEntry])
103
-
104
- await generateAll(resolver)
105
-
106
- const files = generatedFiles(tmpDir)
107
- const callers = files.filter(({ content }) => content.includes(`${ABANDON_HOOK_FACTORY}(`))
108
- // The fixture must actually exercise the path, or this test passes vacuously — which is exactly
109
- // how the missing import survived every existing suite.
110
- expect(callers.length).toBeGreaterThan(0)
111
-
112
- for (const { name, content } of callers) {
113
- expect(`${name}: ${content.includes(`import { `) && new RegExp(`import \\{[^}]*\\b${ABANDON_HOOK_FACTORY}\\b[^}]*\\} from`, 's').test(content)}`)
114
- .toBe(`${name}: true`)
115
- }
116
- })
117
-
118
- it('leaves output untouched for a module set whose workers declare no hook', async () => {
119
- scaffoldWorkerModule(tmpDir, 'demo', WORKER_WITHOUT_HOOK)
120
- const resolver = createMockResolver(tmpDir, [{ id: 'demo', from: 'pkg' } as ModuleEntry])
121
-
122
- await generateAll(resolver)
123
-
124
- for (const { name, content } of generatedFiles(tmpDir)) {
125
- expect(`${name}: ${content.includes(ABANDON_HOOK_FACTORY)}`).toBe(`${name}: false`)
126
- }
127
- })
128
- })