@open-mercato/cli 0.6.8-develop.7100.1.fbf66fca35 → 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.
- package/.turbo/turbo-build.log +1 -7
- package/dist/agentic/guides/module-facts.json +598 -5508
- package/dist/agentic/guides/module-facts.v2.json +598 -5594
- package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
- package/dist/agentic/guides/modules/api_docs/index.md +1 -1
- package/dist/agentic/guides/modules/api_keys/exact-override-targets.md +0 -1
- package/dist/agentic/guides/modules/api_keys/index.md +2 -4
- package/dist/agentic/guides/modules/attachments/di-registrations-rich.md +5 -6
- package/dist/agentic/guides/modules/attachments/di-service-tokens.md +4 -5
- package/dist/agentic/guides/modules/attachments/exact-override-targets.md +5 -6
- package/dist/agentic/guides/modules/attachments/index.md +2 -2
- package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
- package/dist/agentic/guides/modules/auth/di-registrations-rich.md +2 -3
- package/dist/agentic/guides/modules/auth/di-service-tokens.md +2 -3
- package/dist/agentic/guides/modules/auth/exact-override-targets.md +2 -3
- package/dist/agentic/guides/modules/auth/index.md +1 -1
- package/dist/agentic/guides/modules/business_rules/index.md +1 -1
- package/dist/agentic/guides/modules/catalog/incoming-installed-contributions.md +0 -3
- package/dist/agentic/guides/modules/catalog/index.md +2 -2
- package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
- package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
- package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
- package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
- package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
- package/dist/agentic/guides/modules/checkout/index.md +1 -1
- package/dist/agentic/guides/modules/communication_channels/di-registrations-rich.md +8 -9
- package/dist/agentic/guides/modules/communication_channels/exact-override-targets.md +8 -9
- package/dist/agentic/guides/modules/communication_channels/incoming-installed-contributions.md +0 -2
- package/dist/agentic/guides/modules/communication_channels/index.md +3 -3
- package/dist/agentic/guides/modules/configs/index.md +1 -1
- package/dist/agentic/guides/modules/content/index.md +1 -1
- package/dist/agentic/guides/modules/currencies/index.md +1 -1
- package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
- package/dist/agentic/guides/modules/customers/active-extension-bindings.md +3 -6
- package/dist/agentic/guides/modules/customers/contribution-resolutions.md +0 -1
- package/dist/agentic/guides/modules/customers/events.md +1 -2
- package/dist/agentic/guides/modules/customers/incoming-installed-contributions.md +0 -7
- package/dist/agentic/guides/modules/customers/index.md +4 -4
- package/dist/agentic/guides/modules/customers/umes-contributions.md +0 -1
- package/dist/agentic/guides/modules/customers/umes-hosts.md +0 -1
- package/dist/agentic/guides/modules/dashboards/index.md +1 -1
- package/dist/agentic/guides/modules/data_sync/exact-override-targets.md +2 -2
- package/dist/agentic/guides/modules/data_sync/index.md +1 -1
- package/dist/agentic/guides/modules/data_sync/workers.md +2 -2
- package/dist/agentic/guides/modules/design_system/index.md +1 -1
- package/dist/agentic/guides/modules/devices/index.md +1 -1
- package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
- package/dist/agentic/guides/modules/directory/exact-override-targets.md +0 -2
- package/dist/agentic/guides/modules/directory/index.md +3 -5
- package/dist/agentic/guides/modules/entities/index.md +1 -1
- package/dist/agentic/guides/modules/eudr/index.md +1 -1
- package/dist/agentic/guides/modules/events/index.md +1 -1
- package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe/exact-override-targets.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe/workers.md +1 -1
- package/dist/agentic/guides/modules/generators/index.md +1 -1
- package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
- package/dist/agentic/guides/modules/integrations/index.md +1 -1
- package/dist/agentic/guides/modules/messages/index.md +1 -1
- package/dist/agentic/guides/modules/notifications/index.md +1 -1
- package/dist/agentic/guides/modules/onboarding/index.md +1 -1
- package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
- package/dist/agentic/guides/modules/perspectives/index.md +1 -1
- package/dist/agentic/guides/modules/planner/index.md +1 -1
- package/dist/agentic/guides/modules/portal/index.md +1 -1
- package/dist/agentic/guides/modules/progress/index.md +1 -1
- package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
- package/dist/agentic/guides/modules/query_index/index.md +1 -1
- package/dist/agentic/guides/modules/record_locks/index.md +1 -1
- package/dist/agentic/guides/modules/resources/index.md +1 -1
- package/dist/agentic/guides/modules/sales/active-extension-bindings.md +0 -1
- package/dist/agentic/guides/modules/sales/contribution-resolutions.md +0 -1
- package/dist/agentic/guides/modules/sales/exact-override-targets.md +4 -5
- package/dist/agentic/guides/modules/sales/host-extension-points.md +1 -1
- package/dist/agentic/guides/modules/sales/incoming-installed-contributions.md +0 -2
- package/dist/agentic/guides/modules/sales/index.md +3 -3
- package/dist/agentic/guides/modules/sales/setup.md +1 -1
- package/dist/agentic/guides/modules/sales/umes-contributions.md +0 -1
- package/dist/agentic/guides/modules/sales/umes-diagnostics.md +0 -1
- package/dist/agentic/guides/modules/sales/umes-hosts.md +0 -9
- package/dist/agentic/guides/modules/scheduler/cli-commands.md +0 -1
- package/dist/agentic/guides/modules/scheduler/exact-override-targets.md +1 -3
- package/dist/agentic/guides/modules/scheduler/index.md +2 -2
- package/dist/agentic/guides/modules/scheduler/workers.md +1 -2
- package/dist/agentic/guides/modules/search/index.md +1 -1
- package/dist/agentic/guides/modules/security/index.md +1 -1
- package/dist/agentic/guides/modules/security/owned-contract-module-metadata.md +1 -1
- package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
- package/dist/agentic/guides/modules/sso/index.md +1 -1
- package/dist/agentic/guides/modules/staff/index.md +1 -1
- package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
- package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
- package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
- package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
- package/dist/agentic/guides/modules/translations/index.md +1 -1
- package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
- package/dist/agentic/guides/modules/webhooks/index.md +1 -1
- package/dist/agentic/guides/modules/wms/index.md +1 -1
- package/dist/agentic/guides/modules/workflows/index.md +1 -1
- package/dist/agentic/guides/reference-module-facts.json +2 -2
- package/dist/agentic/guides/reference-modules/example/index.md +1 -1
- package/dist/agentic/guides/upstream/manifest.json +1 -1
- package/dist/agentic/shared/AGENTS.md.template +3 -3
- package/dist/agentic/shared/ai/harness/README.md +2 -2
- package/dist/agentic/shared/ai/harness/RELEASE.md +6 -8
- package/dist/agentic/shared/ai/harness/cases.json +0 -83
- package/dist/agentic/shared/ai/harness/cases.schema.json +4 -4
- package/dist/agentic/shared/ai/harness/validators.json +1 -1
- package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.7-to-0.7.0/SKILL.md +12 -3
- package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +11 -37
- package/dist/lib/generators/ast/index.js +1 -4
- package/dist/lib/generators/ast/index.js.map +2 -2
- package/dist/lib/generators/ast/source-file.js +1 -8
- package/dist/lib/generators/ast/source-file.js.map +2 -2
- package/dist/lib/generators/ast/writers.js +1 -9
- package/dist/lib/generators/ast/writers.js.map +2 -2
- package/dist/lib/generators/module-registry.js +189 -424
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/generators/route-manifest-shards.js +1 -4
- package/dist/lib/generators/route-manifest-shards.js.map +2 -2
- package/dist/lib/spawn.js +2 -3
- package/dist/lib/spawn.js.map +2 -2
- package/dist/lib/testing/integration.js +24 -162
- package/dist/lib/testing/integration.js.map +2 -2
- package/dist/mercato.js +0 -14
- package/dist/mercato.js.map +2 -2
- package/package.json +7 -8
- package/src/lib/__tests__/spawn.test.ts +0 -23
- package/src/lib/generators/__tests__/metadata-extraction.test.ts +0 -44
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +8 -32
- package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +1 -4
- package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +1 -1
- package/src/lib/generators/__tests__/module-subset.test.ts +72 -96
- package/src/lib/generators/__tests__/output-snapshots.test.ts +1 -1
- package/src/lib/generators/__tests__/route-manifest-shards.test.ts +0 -15
- package/src/lib/generators/__tests__/structural-contracts.test.ts +3 -7
- package/src/lib/generators/ast/index.ts +1 -2
- package/src/lib/generators/ast/source-file.ts +0 -13
- package/src/lib/generators/ast/writers.ts +1 -26
- package/src/lib/generators/module-registry.ts +227 -534
- package/src/lib/generators/route-manifest-shards.ts +1 -8
- package/src/lib/spawn.ts +4 -5
- package/src/lib/testing/__tests__/integration.test.ts +0 -435
- package/src/lib/testing/integration.ts +32 -254
- package/src/mercato.ts +0 -27
- package/dist/agentic/guides/modules/api_keys/di-registrations-rich.md +0 -11
- package/dist/agentic/guides/modules/api_keys/di-service-tokens.md +0 -11
- package/dist/agentic/guides/modules/channel_discord/acl-features.md +0 -13
- package/dist/agentic/guides/modules/channel_discord/ai-agents.md +0 -11
- package/dist/agentic/guides/modules/channel_discord/backend-pages.md +0 -11
- package/dist/agentic/guides/modules/channel_discord/cli-commands.md +0 -13
- package/dist/agentic/guides/modules/channel_discord/contribution-resolutions.md +0 -14
- package/dist/agentic/guides/modules/channel_discord/di-registrations-rich.md +0 -12
- package/dist/agentic/guides/modules/channel_discord/exact-override-targets.md +0 -26
- package/dist/agentic/guides/modules/channel_discord/index.md +0 -22
- package/dist/agentic/guides/modules/channel_discord/owned-contract-module-metadata.md +0 -11
- package/dist/agentic/guides/modules/channel_discord/setup.md +0 -11
- package/dist/agentic/guides/modules/channel_discord/umes-contributions.md +0 -14
- package/dist/agentic/guides/modules/channel_discord/workers.md +0 -12
- package/dist/agentic/guides/modules/directory/di-registrations-rich.md +0 -12
- package/dist/agentic/guides/modules/directory/di-service-tokens.md +0 -12
- package/dist/agentic/guides/modules/documents/acl-features.md +0 -17
- package/dist/agentic/guides/modules/documents/backend-pages.md +0 -13
- package/dist/agentic/guides/modules/documents/contribution-resolutions.md +0 -27
- package/dist/agentic/guides/modules/documents/di-registrations-rich.md +0 -21
- package/dist/agentic/guides/modules/documents/domain-commands.md +0 -37
- package/dist/agentic/guides/modules/documents/encryption.md +0 -11
- package/dist/agentic/guides/modules/documents/entities.md +0 -21
- package/dist/agentic/guides/modules/documents/events.md +0 -25
- package/dist/agentic/guides/modules/documents/exact-override-targets.md +0 -39
- package/dist/agentic/guides/modules/documents/index.md +0 -25
- package/dist/agentic/guides/modules/documents/notifications.md +0 -13
- package/dist/agentic/guides/modules/documents/owned-contract-module-metadata.md +0 -11
- package/dist/agentic/guides/modules/documents/setup.md +0 -11
- package/dist/agentic/guides/modules/documents/umes-contributions.md +0 -27
- package/dist/agentic/guides/modules/documents/umes-hosts.md +0 -36
- package/src/__tests__/resolve-queue-abandon-hook.test.ts +0 -39
- package/src/lib/generators/__tests__/__snapshots__/bootstrap-registrations.test.ts.snap +0 -14
- package/src/lib/generators/__tests__/__snapshots__/command-loaders.test.ts.snap +0 -23
- package/src/lib/generators/__tests__/ast-writers-escaping.test.ts +0 -64
- package/src/lib/generators/__tests__/bootstrap-registrations.test.ts +0 -176
- package/src/lib/generators/__tests__/command-loaders.test.ts +0 -239
- package/src/lib/generators/__tests__/generated-registry-imports.test.ts +0 -128
- 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
|
-
})
|