@open-mercato/cli 0.7.1-develop.7185.1.0f280ef1f1 → 0.7.1-develop.7191.1.3c9346566b
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/AGENTS.md +1 -1
- package/dist/agentic/guides/module-facts.json +137 -137
- package/dist/agentic/guides/module-facts.v2.json +137 -137
- 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/index.md +1 -1
- package/dist/agentic/guides/modules/attachments/index.md +1 -1
- package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
- 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/index.md +1 -1
- package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
- package/dist/agentic/guides/modules/channel_discord/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/channel_resend/index.md +1 -1
- package/dist/agentic/guides/modules/channel_ses/index.md +1 -1
- package/dist/agentic/guides/modules/checkout/index.md +1 -1
- package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
- 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 +1 -1
- package/dist/agentic/guides/modules/customers/index.md +1 -1
- package/dist/agentic/guides/modules/dashboards/index.md +1 -1
- package/dist/agentic/guides/modules/data_sync/index.md +1 -1
- 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/index.md +1 -1
- package/dist/agentic/guides/modules/documents/index.md +1 -1
- 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/index.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/phone_calls/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/index.md +1 -1
- package/dist/agentic/guides/modules/scheduler/index.md +1 -1
- 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/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/tillio/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 +1 -1
- package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +1 -0
- package/dist/agentic/guides/upstream/manifest.json +2 -2
- package/dist/lib/generate-watch-structure.js +1 -0
- package/dist/lib/generate-watch-structure.js.map +2 -2
- package/dist/lib/generators/module-registry.js +45 -0
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/module-runtimes.js +99 -0
- package/dist/lib/module-runtimes.js.map +7 -0
- package/dist/mercato.js +9 -0
- package/dist/mercato.js.map +2 -2
- package/package.json +6 -6
- package/src/__tests__/module-runtimes.test.ts +267 -0
- package/src/lib/__tests__/generate-watch-structure.test.ts +18 -0
- package/src/lib/generate-watch-structure.ts +1 -0
- package/src/lib/generators/__tests__/registry-variant-parity.test.ts +28 -1
- package/src/lib/generators/module-registry.ts +52 -0
- package/src/lib/module-runtimes.ts +195 -0
- package/src/mercato.ts +20 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cli",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7191.1.3c9346566b",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
"@mikro-orm/decorators": "^7.1.14",
|
|
61
61
|
"@mikro-orm/migrations": "^7.1.14",
|
|
62
62
|
"@mikro-orm/postgresql": "^7.1.14",
|
|
63
|
-
"@open-mercato/queue": "0.7.1-develop.
|
|
64
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
65
|
-
"@open-mercato/telemetry": "0.7.1-develop.
|
|
63
|
+
"@open-mercato/queue": "0.7.1-develop.7191.1.3c9346566b",
|
|
64
|
+
"@open-mercato/shared": "0.7.1-develop.7191.1.3c9346566b",
|
|
65
|
+
"@open-mercato/telemetry": "0.7.1-develop.7191.1.3c9346566b",
|
|
66
66
|
"cross-spawn": "^7.0.6",
|
|
67
67
|
"pg": "8.23.0",
|
|
68
68
|
"semver": "^7.8.5",
|
|
@@ -73,10 +73,10 @@
|
|
|
73
73
|
"typescript-js": "npm:typescript@6.0.3"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
76
|
+
"@open-mercato/shared": "0.7.1-develop.7191.1.3c9346566b"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
79
|
+
"@open-mercato/shared": "0.7.1-develop.7191.1.3c9346566b",
|
|
80
80
|
"@types/jest": "^30.0.0",
|
|
81
81
|
"jest": "^30.4.2",
|
|
82
82
|
"ts-jest": "^29.4.12"
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import type { AppContainer } from '@open-mercato/shared/lib/di/container'
|
|
2
|
+
import type { ModuleRuntime } from '@open-mercato/shared/modules/runtime'
|
|
3
|
+
import {
|
|
4
|
+
isProductionBuildPhase,
|
|
5
|
+
startModuleRuntimes,
|
|
6
|
+
type ModuleRuntimeCarrier,
|
|
7
|
+
} from '../lib/module-runtimes'
|
|
8
|
+
|
|
9
|
+
const container = {} as AppContainer
|
|
10
|
+
const silent = () => {}
|
|
11
|
+
|
|
12
|
+
function moduleWith(id: string, runtime: ModuleRuntime): ModuleRuntimeCarrier {
|
|
13
|
+
return { id, runtime }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('startModuleRuntimes', () => {
|
|
17
|
+
it('starts only the modules that declare a runtime', async () => {
|
|
18
|
+
const start = jest.fn().mockResolvedValue(undefined)
|
|
19
|
+
const started = await startModuleRuntimes({
|
|
20
|
+
modules: [{ id: 'no_runtime' }, moduleWith('has_runtime', { start })],
|
|
21
|
+
container,
|
|
22
|
+
role: 'worker',
|
|
23
|
+
log: silent,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
expect(start).toHaveBeenCalledTimes(1)
|
|
27
|
+
expect(started.started).toEqual(['has_runtime'])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('starts in module id order, so a failure is reproducible', async () => {
|
|
31
|
+
const order: string[] = []
|
|
32
|
+
const record = (id: string): ModuleRuntime => ({ start: async () => { order.push(id) } })
|
|
33
|
+
|
|
34
|
+
await startModuleRuntimes({
|
|
35
|
+
modules: [moduleWith('zulu', record('zulu')), moduleWith('alpha', record('alpha')), moduleWith('mike', record('mike'))],
|
|
36
|
+
container,
|
|
37
|
+
role: 'worker',
|
|
38
|
+
log: silent,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
expect(order).toEqual(['alpha', 'mike', 'zulu'])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('defaults to the worker role — the only one wired — and honours a declared role set', async () => {
|
|
45
|
+
const byDefault = jest.fn().mockResolvedValue(undefined)
|
|
46
|
+
const serverOnly = jest.fn().mockResolvedValue(undefined)
|
|
47
|
+
const modules = [
|
|
48
|
+
moduleWith('default_roles', { start: byDefault }),
|
|
49
|
+
moduleWith('server_only', { roles: ['server'], start: serverOnly }),
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
await startModuleRuntimes({ modules, container, role: 'worker', log: silent })
|
|
53
|
+
expect(byDefault).toHaveBeenCalledTimes(1)
|
|
54
|
+
expect(serverOnly).not.toHaveBeenCalled()
|
|
55
|
+
|
|
56
|
+
await startModuleRuntimes({ modules, container, role: 'scheduler', log: silent })
|
|
57
|
+
// Neither applies: 'scheduler' is in neither the default set nor the declared one.
|
|
58
|
+
expect(byDefault).toHaveBeenCalledTimes(1)
|
|
59
|
+
expect(serverOnly).not.toHaveBeenCalled()
|
|
60
|
+
|
|
61
|
+
// A role a module names explicitly still applies, so widening the default later is additive.
|
|
62
|
+
await startModuleRuntimes({ modules, container, role: 'server', log: silent })
|
|
63
|
+
expect(byDefault).toHaveBeenCalledTimes(1)
|
|
64
|
+
expect(serverOnly).toHaveBeenCalledTimes(1)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('fails startup when a runtime throws, and stops the ones already started', async () => {
|
|
68
|
+
const stop = jest.fn().mockResolvedValue(undefined)
|
|
69
|
+
const modules = [
|
|
70
|
+
moduleWith('a_ok', { start: async () => ({ stop }) }),
|
|
71
|
+
moduleWith('b_broken', { start: async () => { throw new Error('no broker') } }),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
await expect(startModuleRuntimes({ modules, container, role: 'worker', log: silent }))
|
|
75
|
+
.rejects.toThrow('no broker')
|
|
76
|
+
|
|
77
|
+
// The whole point: a half-started process must not be left running. A module that came up
|
|
78
|
+
// before the failure holds a lease or a subscription, and nothing else will release it.
|
|
79
|
+
expect(stop).toHaveBeenCalledTimes(1)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('fails startup when a runtime throws synchronously, and stops the ones already started', async () => {
|
|
83
|
+
const stop = jest.fn().mockResolvedValue(undefined)
|
|
84
|
+
const modules = [
|
|
85
|
+
moduleWith('a_ok', { start: async () => ({ stop }) }),
|
|
86
|
+
moduleWith('b_broken', { start: (() => { throw new Error('bad config') }) as ModuleRuntime['start'] }),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
await expect(startModuleRuntimes({ modules, container, role: 'worker', log: silent }))
|
|
90
|
+
.rejects.toThrow('bad config')
|
|
91
|
+
expect(stop).toHaveBeenCalledTimes(1)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('fails startup when a runtime does not start within the timeout', async () => {
|
|
95
|
+
const modules = [moduleWith('slow', { start: () => new Promise<never>(() => {}) })]
|
|
96
|
+
|
|
97
|
+
await expect(startModuleRuntimes({ modules, container, role: 'worker', log: silent, startTimeoutMs: 10 }))
|
|
98
|
+
.rejects.toThrow(/did not start within 10ms/)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// A start timeout is fatal, so an operator whose runtime legitimately needs longer than the
|
|
102
|
+
// default needs a supported way to say so rather than a worker that exits non-zero every boot.
|
|
103
|
+
it('takes both timeouts from the environment', async () => {
|
|
104
|
+
const modules = [moduleWith('slow', { start: () => new Promise<never>(() => {}) })]
|
|
105
|
+
|
|
106
|
+
await expect(startModuleRuntimes({
|
|
107
|
+
modules,
|
|
108
|
+
container,
|
|
109
|
+
role: 'worker',
|
|
110
|
+
log: silent,
|
|
111
|
+
env: { OM_MODULE_RUNTIME_START_TIMEOUT_MS: '7' },
|
|
112
|
+
})).rejects.toThrow(/did not start within 7ms/)
|
|
113
|
+
|
|
114
|
+
const stopped = jest.fn().mockResolvedValue(undefined)
|
|
115
|
+
const started = await startModuleRuntimes({
|
|
116
|
+
modules: [
|
|
117
|
+
moduleWith('a_first', { start: async () => ({ stop: stopped }) }),
|
|
118
|
+
moduleWith('b_hangs', { start: async () => ({ stop: () => new Promise<never>(() => {}) }) }),
|
|
119
|
+
],
|
|
120
|
+
container,
|
|
121
|
+
role: 'worker',
|
|
122
|
+
log: silent,
|
|
123
|
+
env: { OM_MODULE_RUNTIME_STOP_TIMEOUT_MS: '10' },
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
await expect(started.stop()).resolves.toBeUndefined()
|
|
127
|
+
expect(stopped).toHaveBeenCalledTimes(1)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('prefers an explicit timeout over the environment', async () => {
|
|
131
|
+
const modules = [moduleWith('slow', { start: () => new Promise<never>(() => {}) })]
|
|
132
|
+
|
|
133
|
+
await expect(startModuleRuntimes({
|
|
134
|
+
modules,
|
|
135
|
+
container,
|
|
136
|
+
role: 'worker',
|
|
137
|
+
log: silent,
|
|
138
|
+
startTimeoutMs: 5,
|
|
139
|
+
env: { OM_MODULE_RUNTIME_START_TIMEOUT_MS: '600000' },
|
|
140
|
+
})).rejects.toThrow(/did not start within 5ms/)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// Refusing to boot over a typo in a tuning knob would be worse than the default it replaces.
|
|
144
|
+
it('falls back to the default timeout, loudly, when the environment value is unusable', async () => {
|
|
145
|
+
const lines: string[] = []
|
|
146
|
+
const modules = [moduleWith('ok', { start: async () => undefined })]
|
|
147
|
+
|
|
148
|
+
await startModuleRuntimes({
|
|
149
|
+
modules,
|
|
150
|
+
container,
|
|
151
|
+
role: 'worker',
|
|
152
|
+
log: (message) => lines.push(message),
|
|
153
|
+
env: { OM_MODULE_RUNTIME_START_TIMEOUT_MS: 'soon', OM_MODULE_RUNTIME_STOP_TIMEOUT_MS: '-1' },
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
expect(lines).toEqual(expect.arrayContaining([
|
|
157
|
+
expect.stringContaining('ignoring OM_MODULE_RUNTIME_START_TIMEOUT_MS="soon"'),
|
|
158
|
+
expect.stringContaining('ignoring OM_MODULE_RUNTIME_STOP_TIMEOUT_MS="-1"'),
|
|
159
|
+
]))
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// The handle from a timed-out start never reaches `started`, so shutdown cannot reach it either.
|
|
163
|
+
// Without trailing the promise, a runtime that ignores `signal` keeps whatever it acquired.
|
|
164
|
+
it('releases the handle of a start that resolves after its timeout', async () => {
|
|
165
|
+
const stop = jest.fn().mockResolvedValue(undefined)
|
|
166
|
+
let resolveStart: (handle: { stop: () => Promise<void> }) => void = () => {}
|
|
167
|
+
const modules = [moduleWith('late', {
|
|
168
|
+
start: () => new Promise((resolve) => { resolveStart = resolve }),
|
|
169
|
+
})]
|
|
170
|
+
|
|
171
|
+
await expect(startModuleRuntimes({ modules, container, role: 'worker', log: silent, startTimeoutMs: 10 }))
|
|
172
|
+
.rejects.toThrow(/did not start within 10ms/)
|
|
173
|
+
|
|
174
|
+
resolveStart({ stop })
|
|
175
|
+
await Promise.resolve()
|
|
176
|
+
await Promise.resolve()
|
|
177
|
+
|
|
178
|
+
expect(stop).toHaveBeenCalledTimes(1)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('stops runtimes in reverse start order', async () => {
|
|
182
|
+
const order: string[] = []
|
|
183
|
+
const runtime = (id: string): ModuleRuntime => ({
|
|
184
|
+
start: async () => ({ stop: async () => { order.push(id) } }),
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const started = await startModuleRuntimes({
|
|
188
|
+
modules: [moduleWith('alpha', runtime('alpha')), moduleWith('bravo', runtime('bravo'))],
|
|
189
|
+
container,
|
|
190
|
+
role: 'worker',
|
|
191
|
+
log: silent,
|
|
192
|
+
})
|
|
193
|
+
await started.stop()
|
|
194
|
+
|
|
195
|
+
expect(order).toEqual(['bravo', 'alpha'])
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('aborts the shared signal before stopping, so a runtime can wind down at its own boundary', async () => {
|
|
199
|
+
let abortedDuringStop: boolean | null = null
|
|
200
|
+
const started = await startModuleRuntimes({
|
|
201
|
+
modules: [moduleWith('watcher', {
|
|
202
|
+
start: async ({ signal }) => ({ stop: async () => { abortedDuringStop = signal.aborted } }),
|
|
203
|
+
})],
|
|
204
|
+
container,
|
|
205
|
+
role: 'worker',
|
|
206
|
+
log: silent,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
await started.stop()
|
|
210
|
+
expect(abortedDuringStop).toBe(true)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('keeps going when one runtime fails to stop, and does not hang on one that never does', async () => {
|
|
214
|
+
const lastStopped = jest.fn().mockResolvedValue(undefined)
|
|
215
|
+
const started = await startModuleRuntimes({
|
|
216
|
+
modules: [
|
|
217
|
+
moduleWith('a_first', { start: async () => ({ stop: lastStopped }) }),
|
|
218
|
+
moduleWith('b_throws', { start: async () => ({ stop: async () => { throw new Error('rude') } }) }),
|
|
219
|
+
moduleWith('c_hangs', { start: async () => ({ stop: () => new Promise<never>(() => {}) }) }),
|
|
220
|
+
],
|
|
221
|
+
container,
|
|
222
|
+
role: 'worker',
|
|
223
|
+
log: silent,
|
|
224
|
+
stopTimeoutMs: 10,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// Resolves rather than rejecting or hanging: shutdown is best-effort by design, because a
|
|
228
|
+
// shutdown that cannot finish would otherwise become one that never finishes.
|
|
229
|
+
await expect(started.stop()).resolves.toBeUndefined()
|
|
230
|
+
expect(lastStopped).toHaveBeenCalledTimes(1)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('stops only once however many times stop is called', async () => {
|
|
234
|
+
const stop = jest.fn().mockResolvedValue(undefined)
|
|
235
|
+
const started = await startModuleRuntimes({
|
|
236
|
+
modules: [moduleWith('once', { start: async () => ({ stop }) })],
|
|
237
|
+
container,
|
|
238
|
+
role: 'worker',
|
|
239
|
+
log: silent,
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
await Promise.all([started.stop(), started.stop(), started.stop()])
|
|
243
|
+
expect(stop).toHaveBeenCalledTimes(1)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('tolerates a runtime that returns no handle', async () => {
|
|
247
|
+
const started = await startModuleRuntimes({
|
|
248
|
+
modules: [moduleWith('fire_and_forget', { start: async () => undefined })],
|
|
249
|
+
container,
|
|
250
|
+
role: 'worker',
|
|
251
|
+
log: silent,
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
await expect(started.stop()).resolves.toBeUndefined()
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
describe('isProductionBuildPhase', () => {
|
|
259
|
+
it('detects a Next production build', () => {
|
|
260
|
+
expect(isProductionBuildPhase({ NEXT_PHASE: 'phase-production-build' } as NodeJS.ProcessEnv)).toBe(true)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('is false for a server, a worker and an unset environment', () => {
|
|
264
|
+
expect(isProductionBuildPhase({ NEXT_PHASE: 'phase-production-server' } as NodeJS.ProcessEnv)).toBe(false)
|
|
265
|
+
expect(isProductionBuildPhase({} as NodeJS.ProcessEnv)).toBe(false)
|
|
266
|
+
})
|
|
267
|
+
})
|
|
@@ -78,6 +78,24 @@ describe('calculateGenerateWatchStructureChecksum', () => {
|
|
|
78
78
|
expect(currentChecksum()).not.toBe(before)
|
|
79
79
|
})
|
|
80
80
|
|
|
81
|
+
// A runtime.ts the watcher cannot see is the failure SPEC-072 exists to remove, reappearing in
|
|
82
|
+
// the dev loop: the registry is not regenerated, `Module.runtime` stays undefined, and the
|
|
83
|
+
// runtime never starts — with no error and no warning.
|
|
84
|
+
it('changes when a module runtime is added, edited and removed', () => {
|
|
85
|
+
const before = currentChecksum()
|
|
86
|
+
const runtimePath = path.join(pkgModule, 'runtime.ts')
|
|
87
|
+
|
|
88
|
+
write(runtimePath, 'export const runtime = { start: async () => {} }\n')
|
|
89
|
+
const afterAdd = currentChecksum()
|
|
90
|
+
expect(afterAdd).not.toBe(before)
|
|
91
|
+
|
|
92
|
+
write(runtimePath, 'export const runtime = { roles: ["worker"], start: async () => {} }\n')
|
|
93
|
+
expect(currentChecksum()).not.toBe(afterAdd)
|
|
94
|
+
|
|
95
|
+
fs.rmSync(runtimePath)
|
|
96
|
+
expect(currentChecksum()).toBe(before)
|
|
97
|
+
})
|
|
98
|
+
|
|
81
99
|
it('changes when a discovered worker is added and removed', () => {
|
|
82
100
|
const before = currentChecksum()
|
|
83
101
|
const workerPath = path.join(pkgModule, 'workers', 'sync-customers.ts')
|
|
@@ -17,7 +17,7 @@ import path from 'node:path'
|
|
|
17
17
|
import os from 'node:os'
|
|
18
18
|
import ts from 'typescript-js'
|
|
19
19
|
import type { PackageResolver, ModuleEntry } from '../../resolver'
|
|
20
|
-
import { generateModuleRegistry, generateModuleRegistryApp } from '../module-registry'
|
|
20
|
+
import { generateModuleRegistry, generateModuleRegistryApp, generateModuleRegistryCli } from '../module-registry'
|
|
21
21
|
|
|
22
22
|
/** Properties both registry variants must agree on, per module. */
|
|
23
23
|
const SHARED_PROPERTIES = [
|
|
@@ -33,6 +33,7 @@ const SHARED_PROPERTIES = [
|
|
|
33
33
|
'features',
|
|
34
34
|
'customEntities',
|
|
35
35
|
'setup',
|
|
36
|
+
'runtime',
|
|
36
37
|
'defaultEncryptionMaps',
|
|
37
38
|
'integrations',
|
|
38
39
|
'bundles',
|
|
@@ -102,6 +103,10 @@ function scaffoldFixture(): ModuleEntry[] {
|
|
|
102
103
|
)
|
|
103
104
|
touchFile(pkgModulePath('orders', 'acl.ts'), "export const features = ['orders.view', 'orders.create']\n")
|
|
104
105
|
touchFile(pkgModulePath('orders', 'setup.ts'), "export const setup = { defaultRoleFeatures: ['orders.view'] }\n")
|
|
106
|
+
touchFile(
|
|
107
|
+
pkgModulePath('orders', 'runtime.ts'),
|
|
108
|
+
"export const runtime = { roles: ['worker'], start: async () => ({ stop: async () => {} }) }\n",
|
|
109
|
+
)
|
|
105
110
|
touchFile(
|
|
106
111
|
pkgModulePath('orders', 'encryption.ts'),
|
|
107
112
|
"export const defaultEncryptionMaps = [{ entityId: 'orders:sales_order', fields: [{ field: 'customer_email' }] }]\nexport default defaultEncryptionMaps\n",
|
|
@@ -190,8 +195,10 @@ describe('module registry variant parity', () => {
|
|
|
190
195
|
const resolver = createMockResolver(scaffoldFixture())
|
|
191
196
|
const mainResult = await generateModuleRegistry({ resolver, quiet: true })
|
|
192
197
|
const appResult = await generateModuleRegistryApp({ resolver, quiet: true })
|
|
198
|
+
const cliResult = await generateModuleRegistryCli({ resolver, quiet: true })
|
|
193
199
|
expect(mainResult.errors).toEqual([])
|
|
194
200
|
expect(appResult.errors).toEqual([])
|
|
201
|
+
expect(cliResult.errors).toEqual([])
|
|
195
202
|
|
|
196
203
|
mainVariant = parseModulePropertiesById(readGenerated('modules.generated.ts'), 'modules.generated.ts')
|
|
197
204
|
runtimeVariant = parseModulePropertiesById(
|
|
@@ -236,6 +243,26 @@ describe('module registry variant parity', () => {
|
|
|
236
243
|
expect(restrictTo(mainVariant.get('bare')!, SHARED_PROPERTIES)).toEqual(['customFieldSets', 'id', 'info'])
|
|
237
244
|
})
|
|
238
245
|
|
|
246
|
+
// The worker wiring reads `getCliModules()` — modules.cli.generated.ts — while the web tier
|
|
247
|
+
// reads the app and bootstrap registries. A convention file that reaches only some of them is a
|
|
248
|
+
// runtime that starts in some processes and not others, with nothing to say so.
|
|
249
|
+
it('emits the module runtime in every registry a process can read', () => {
|
|
250
|
+
const registryFiles = [
|
|
251
|
+
'modules.generated.ts',
|
|
252
|
+
'modules.runtime.generated.ts',
|
|
253
|
+
'modules.app.generated.ts',
|
|
254
|
+
'modules.bootstrap.generated.ts',
|
|
255
|
+
'modules.cli.generated.ts',
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
const carriesRuntime = registryFiles.map((fileName) => {
|
|
259
|
+
const byId = parseModulePropertiesById(readGenerated(fileName), fileName)
|
|
260
|
+
return [fileName, byId.get('orders')?.has('runtime'), byId.get('bare')?.has('runtime')]
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
expect(carriesRuntime).toEqual(registryFiles.map((fileName) => [fileName, true, false]))
|
|
264
|
+
})
|
|
265
|
+
|
|
239
266
|
describe('known, intentional divergences', () => {
|
|
240
267
|
it('only the main variant carries eager api handlers and the cli entry point', () => {
|
|
241
268
|
expect(mainVariant.get('orders')!.has('apis')).toBe(true)
|
|
@@ -2705,6 +2705,13 @@ function buildLegacyModuleSetupValue(importName: string): WriterFunction {
|
|
|
2705
2705
|
])
|
|
2706
2706
|
}
|
|
2707
2707
|
|
|
2708
|
+
function buildLegacyModuleRuntimeValue(importName: string): WriterFunction {
|
|
2709
|
+
return logicalOr([
|
|
2710
|
+
legacyNamespaceMemberFallback(importName, ['default', 'runtime']),
|
|
2711
|
+
identifier('undefined'),
|
|
2712
|
+
])
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2708
2715
|
function buildLegacyIntegrationListValue(
|
|
2709
2716
|
importName: string,
|
|
2710
2717
|
pluralMember: string,
|
|
@@ -3637,6 +3644,7 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
|
|
|
3637
3644
|
let customFieldSetsExpr: string = '[]'
|
|
3638
3645
|
const dashboardWidgets: string[] = []
|
|
3639
3646
|
let setupImportName: string | null = null
|
|
3647
|
+
let moduleRuntimeImportName: string | null = null
|
|
3640
3648
|
let encryptionImportName: string | null = null
|
|
3641
3649
|
let integrationImportName: string | null = null
|
|
3642
3650
|
|
|
@@ -3772,6 +3780,12 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
|
|
|
3772
3780
|
}
|
|
3773
3781
|
}
|
|
3774
3782
|
|
|
3783
|
+
// 11c. Module runtime: runtime.ts (SPEC-072)
|
|
3784
|
+
{
|
|
3785
|
+
const moduleRuntime = resolveConventionFile(discovered.resolve('runtime.ts'), 'RUNTIME', modId, importIdRef, imports, runtimeImports)
|
|
3786
|
+
if (moduleRuntime) moduleRuntimeImportName = moduleRuntime.importName
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3775
3789
|
// 12. Custom fields: data/fields.ts
|
|
3776
3790
|
{
|
|
3777
3791
|
const fields = resolveConventionFile(discovered.resolve('data/fields.ts'), 'F', modId, importIdRef, imports, runtimeImports)
|
|
@@ -3931,6 +3945,9 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
|
|
|
3931
3945
|
if (setupImportName) {
|
|
3932
3946
|
setupAndBeyondEntries.push({ name: 'setup', value: buildLegacyModuleSetupValue(setupImportName) })
|
|
3933
3947
|
}
|
|
3948
|
+
if (moduleRuntimeImportName) {
|
|
3949
|
+
setupAndBeyondEntries.push({ name: 'runtime', value: buildLegacyModuleRuntimeValue(moduleRuntimeImportName) })
|
|
3950
|
+
}
|
|
3934
3951
|
if (encryptionImportName) {
|
|
3935
3952
|
setupAndBeyondEntries.push({
|
|
3936
3953
|
name: 'defaultEncryptionMaps',
|
|
@@ -4328,6 +4345,7 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
|
|
|
4328
4345
|
let customEntitiesImportName: string | null = null
|
|
4329
4346
|
let dashboardWidgetsValue: WriterFunction = emptyArray()
|
|
4330
4347
|
let setupImportName: string | null = null
|
|
4348
|
+
let moduleRuntimeImportName: string | null = null
|
|
4331
4349
|
let encryptionImportName: string | null = null
|
|
4332
4350
|
let integrationImportName: string | null = null
|
|
4333
4351
|
|
|
@@ -4352,6 +4370,13 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
|
|
|
4352
4370
|
if (setup) setupImportName = setup.importName
|
|
4353
4371
|
}
|
|
4354
4372
|
|
|
4373
|
+
// Module runtime: runtime.ts (SPEC-072)
|
|
4374
|
+
{
|
|
4375
|
+
const moduleRuntime = resolveConventionFile(discovered.resolve('runtime.ts'), 'RUNTIME', modId, importIdRef, imports)
|
|
4376
|
+
if (moduleRuntime) bootstrapImports.push(buildImportStatement(`* as ${moduleRuntime.importName}`, moduleRuntime.importPath))
|
|
4377
|
+
if (moduleRuntime) moduleRuntimeImportName = moduleRuntime.importName
|
|
4378
|
+
}
|
|
4379
|
+
|
|
4355
4380
|
{
|
|
4356
4381
|
const encryption = resolveConventionFile(discovered.resolve('encryption.ts'), 'ENCRYPTION', modId, importIdRef, imports)
|
|
4357
4382
|
if (encryption) bootstrapImports.push(buildImportStatement(`* as ${encryption.importName}`, encryption.importPath))
|
|
@@ -4543,6 +4568,16 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
|
|
|
4543
4568
|
}),
|
|
4544
4569
|
})
|
|
4545
4570
|
}
|
|
4571
|
+
if (moduleRuntimeImportName) {
|
|
4572
|
+
moduleEntries.push({
|
|
4573
|
+
name: 'runtime',
|
|
4574
|
+
value: namespaceFallback({
|
|
4575
|
+
importName: moduleRuntimeImportName,
|
|
4576
|
+
members: ['default', 'runtime'],
|
|
4577
|
+
fallback: identifier('undefined'),
|
|
4578
|
+
}),
|
|
4579
|
+
})
|
|
4580
|
+
}
|
|
4546
4581
|
if (encryptionImportName) {
|
|
4547
4582
|
moduleEntries.push({
|
|
4548
4583
|
name: 'defaultEncryptionMaps',
|
|
@@ -4744,6 +4779,7 @@ async function generateModuleRegistryCliFromDiscovery(options: ModuleRegistryRen
|
|
|
4744
4779
|
let vectorImportName: string | null = null
|
|
4745
4780
|
let dashboardWidgetsValue: WriterFunction = emptyArray()
|
|
4746
4781
|
let setupImportName: string | null = null
|
|
4782
|
+
let moduleRuntimeImportName: string | null = null
|
|
4747
4783
|
let encryptionImportName: string | null = null
|
|
4748
4784
|
let integrationImportName: string | null = null
|
|
4749
4785
|
|
|
@@ -4768,6 +4804,12 @@ async function generateModuleRegistryCliFromDiscovery(options: ModuleRegistryRen
|
|
|
4768
4804
|
if (setup) setupImportName = setup.importName
|
|
4769
4805
|
}
|
|
4770
4806
|
|
|
4807
|
+
// Module runtime: runtime.ts (SPEC-072)
|
|
4808
|
+
{
|
|
4809
|
+
const moduleRuntime = resolveConventionFile(discovered.resolve('runtime.ts'), 'RUNTIME', modId, importIdRef, imports)
|
|
4810
|
+
if (moduleRuntime) moduleRuntimeImportName = moduleRuntime.importName
|
|
4811
|
+
}
|
|
4812
|
+
|
|
4771
4813
|
// Module encryption defaults: encryption.ts
|
|
4772
4814
|
{
|
|
4773
4815
|
const encryption = resolveConventionFile(discovered.resolve('encryption.ts'), 'ENCRYPTION', modId, importIdRef, imports)
|
|
@@ -4937,6 +4979,16 @@ async function generateModuleRegistryCliFromDiscovery(options: ModuleRegistryRen
|
|
|
4937
4979
|
}),
|
|
4938
4980
|
})
|
|
4939
4981
|
}
|
|
4982
|
+
if (moduleRuntimeImportName) {
|
|
4983
|
+
moduleEntries.push({
|
|
4984
|
+
name: 'runtime',
|
|
4985
|
+
value: namespaceFallback({
|
|
4986
|
+
importName: moduleRuntimeImportName,
|
|
4987
|
+
members: ['default', 'runtime'],
|
|
4988
|
+
fallback: identifier('undefined'),
|
|
4989
|
+
}),
|
|
4990
|
+
})
|
|
4991
|
+
}
|
|
4940
4992
|
if (encryptionImportName) {
|
|
4941
4993
|
moduleEntries.push({
|
|
4942
4994
|
name: 'defaultEncryptionMaps',
|