@open-mercato/cli 0.4.9-develop-db9ecc46fc → 0.4.9-develop-d989387b7a
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/dist/agentic/shared/AGENTS.md.template +2 -0
- package/dist/agentic/shared/ai/skills/eject-and-customize/SKILL.md +3 -1
- package/dist/bin.js +1 -0
- package/dist/bin.js.map +2 -2
- package/dist/lib/__fixtures__/official-module-package/src/index.js +1 -0
- package/dist/lib/__fixtures__/official-module-package/src/index.js.map +7 -0
- package/dist/lib/__fixtures__/official-module-package/src/modules/test_package/index.js +10 -0
- package/dist/lib/__fixtures__/official-module-package/src/modules/test_package/index.js.map +7 -0
- package/dist/lib/eject.js +30 -38
- package/dist/lib/eject.js.map +2 -2
- package/dist/lib/generators/index.js +2 -0
- package/dist/lib/generators/index.js.map +2 -2
- package/dist/lib/generators/module-package-sources.js +45 -0
- package/dist/lib/generators/module-package-sources.js.map +7 -0
- package/dist/lib/module-install-args.js +40 -0
- package/dist/lib/module-install-args.js.map +7 -0
- package/dist/lib/module-install.js +157 -0
- package/dist/lib/module-install.js.map +7 -0
- package/dist/lib/module-package.js +245 -0
- package/dist/lib/module-package.js.map +7 -0
- package/dist/lib/modules-config.js +255 -0
- package/dist/lib/modules-config.js.map +7 -0
- package/dist/lib/resolver.js +19 -5
- package/dist/lib/resolver.js.map +2 -2
- package/dist/lib/testing/integration-discovery.js +20 -9
- package/dist/lib/testing/integration-discovery.js.map +2 -2
- package/dist/lib/testing/integration.js +86 -47
- package/dist/lib/testing/integration.js.map +2 -2
- package/dist/mercato.js +120 -43
- package/dist/mercato.js.map +3 -3
- package/package.json +5 -4
- package/src/__tests__/mercato.test.ts +6 -1
- package/src/bin.ts +1 -0
- package/src/lib/__fixtures__/official-module-package/dist/modules/test_package/index.js +2 -0
- package/src/lib/__fixtures__/official-module-package/package.json +33 -0
- package/src/lib/__fixtures__/official-module-package/src/index.ts +1 -0
- package/src/lib/__fixtures__/official-module-package/src/modules/test_package/index.ts +6 -0
- package/src/lib/__fixtures__/official-module-package/src/modules/test_package/widgets/injection/test/widget.tsx +3 -0
- package/src/lib/__tests__/eject.test.ts +107 -1
- package/src/lib/__tests__/module-install-args.test.ts +35 -0
- package/src/lib/__tests__/module-install.test.ts +217 -0
- package/src/lib/__tests__/module-package.test.ts +215 -0
- package/src/lib/__tests__/modules-config.test.ts +104 -0
- package/src/lib/__tests__/resolve-environment.test.ts +141 -0
- package/src/lib/eject.ts +45 -55
- package/src/lib/generators/__tests__/generators.test.ts +11 -0
- package/src/lib/generators/__tests__/module-package-sources.test.ts +121 -0
- package/src/lib/generators/index.ts +1 -0
- package/src/lib/generators/module-package-sources.ts +59 -0
- package/src/lib/module-install-args.ts +50 -0
- package/src/lib/module-install.ts +234 -0
- package/src/lib/module-package.ts +355 -0
- package/src/lib/modules-config.ts +393 -0
- package/src/lib/resolver.ts +46 -4
- package/src/lib/testing/__tests__/integration-discovery.test.ts +30 -0
- package/src/lib/testing/integration-discovery.ts +23 -8
- package/src/lib/testing/integration.ts +97 -57
- package/src/mercato.ts +128 -49
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import ts from 'typescript'
|
|
3
|
+
import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
|
|
4
|
+
|
|
5
|
+
export type ModuleEntry = {
|
|
6
|
+
id: string
|
|
7
|
+
from?: '@open-mercato/core' | '@app' | string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type ModuleOccurrence = {
|
|
11
|
+
entry: ModuleEntry
|
|
12
|
+
node: ts.ObjectLiteralExpression
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type ModuleConfigShape = {
|
|
16
|
+
arrayNode: ts.ArrayLiteralExpression
|
|
17
|
+
occurrences: ModuleOccurrence[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseModuleEntryFromObjectLiteral(node: ts.ObjectLiteralExpression): ModuleEntry | null {
|
|
21
|
+
let id: string | null = null
|
|
22
|
+
let from: string | null = null
|
|
23
|
+
|
|
24
|
+
for (const property of node.properties) {
|
|
25
|
+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) continue
|
|
26
|
+
|
|
27
|
+
if (property.name.text === 'id' && ts.isStringLiteralLike(property.initializer)) {
|
|
28
|
+
id = property.initializer.text
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (property.name.text === 'from' && ts.isStringLiteralLike(property.initializer)) {
|
|
32
|
+
from = property.initializer.text
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!id) return null
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
id,
|
|
40
|
+
from: from ?? '@open-mercato/core',
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseProcessEnvAccess(
|
|
45
|
+
node: ts.Expression,
|
|
46
|
+
env: NodeJS.ProcessEnv,
|
|
47
|
+
): { matched: boolean; value: string | undefined } {
|
|
48
|
+
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name)) {
|
|
49
|
+
const target = node.expression
|
|
50
|
+
if (
|
|
51
|
+
ts.isPropertyAccessExpression(target)
|
|
52
|
+
&& ts.isIdentifier(target.expression)
|
|
53
|
+
&& target.expression.text === 'process'
|
|
54
|
+
&& target.name.text === 'env'
|
|
55
|
+
) {
|
|
56
|
+
return { matched: true, value: env[node.name.text] }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (
|
|
61
|
+
ts.isElementAccessExpression(node)
|
|
62
|
+
&& ts.isPropertyAccessExpression(node.expression)
|
|
63
|
+
&& ts.isIdentifier(node.expression.expression)
|
|
64
|
+
&& node.expression.expression.text === 'process'
|
|
65
|
+
&& node.expression.name.text === 'env'
|
|
66
|
+
&& ts.isStringLiteralLike(node.argumentExpression)
|
|
67
|
+
) {
|
|
68
|
+
return { matched: true, value: env[node.argumentExpression.text] }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { matched: false, value: undefined }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function evaluateStaticExpressionWithScope(
|
|
75
|
+
node: ts.Expression,
|
|
76
|
+
env: NodeJS.ProcessEnv,
|
|
77
|
+
scope: Map<string, unknown>,
|
|
78
|
+
): unknown {
|
|
79
|
+
if (ts.isParenthesizedExpression(node)) {
|
|
80
|
+
return evaluateStaticExpressionWithScope(node.expression, env, scope)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (ts.isIdentifier(node)) {
|
|
84
|
+
return scope.get(node.text)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (ts.isStringLiteralLike(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
88
|
+
return node.text
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (ts.isNumericLiteral(node)) {
|
|
92
|
+
return Number(node.text)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return true
|
|
96
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return false
|
|
97
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) return null
|
|
98
|
+
|
|
99
|
+
const envAccess = parseProcessEnvAccess(node, env)
|
|
100
|
+
if (envAccess.matched) {
|
|
101
|
+
return envAccess.value
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken) {
|
|
105
|
+
return !Boolean(evaluateStaticExpressionWithScope(node.operand, env, scope))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (ts.isBinaryExpression(node)) {
|
|
109
|
+
if (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
110
|
+
return Boolean(evaluateStaticExpressionWithScope(node.left, env, scope))
|
|
111
|
+
&& Boolean(evaluateStaticExpressionWithScope(node.right, env, scope))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (node.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
115
|
+
return Boolean(evaluateStaticExpressionWithScope(node.left, env, scope))
|
|
116
|
+
|| Boolean(evaluateStaticExpressionWithScope(node.right, env, scope))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (node.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken) {
|
|
120
|
+
return evaluateStaticExpressionWithScope(node.left, env, scope)
|
|
121
|
+
=== evaluateStaticExpressionWithScope(node.right, env, scope)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (node.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken) {
|
|
125
|
+
return evaluateStaticExpressionWithScope(node.left, env, scope)
|
|
126
|
+
!== evaluateStaticExpressionWithScope(node.right, env, scope)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
ts.isCallExpression(node)
|
|
132
|
+
&& ts.isIdentifier(node.expression)
|
|
133
|
+
&& node.expression.text === 'parseBooleanWithDefault'
|
|
134
|
+
) {
|
|
135
|
+
const rawValueNode = node.arguments[0]
|
|
136
|
+
const fallbackNode = node.arguments[1]
|
|
137
|
+
const rawValue = rawValueNode
|
|
138
|
+
? evaluateStaticExpressionWithScope(rawValueNode, env, scope)
|
|
139
|
+
: undefined
|
|
140
|
+
const fallbackValue = fallbackNode
|
|
141
|
+
? evaluateStaticExpressionWithScope(fallbackNode, env, scope)
|
|
142
|
+
: false
|
|
143
|
+
|
|
144
|
+
return parseBooleanWithDefault(
|
|
145
|
+
typeof rawValue === 'string' ? rawValue : undefined,
|
|
146
|
+
Boolean(fallbackValue),
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return undefined
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function evaluateStaticCondition(
|
|
154
|
+
node: ts.Expression,
|
|
155
|
+
env: NodeJS.ProcessEnv,
|
|
156
|
+
scope: Map<string, unknown>,
|
|
157
|
+
): boolean {
|
|
158
|
+
return Boolean(evaluateStaticExpressionWithScope(node, env, scope))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function collectPushEntriesFromStatement(
|
|
162
|
+
statement: ts.Statement,
|
|
163
|
+
env: NodeJS.ProcessEnv,
|
|
164
|
+
targetVariableName: string,
|
|
165
|
+
scope: Map<string, unknown>,
|
|
166
|
+
): ModuleOccurrence[] {
|
|
167
|
+
if (ts.isBlock(statement)) {
|
|
168
|
+
return statement.statements.flatMap((child) =>
|
|
169
|
+
collectPushEntriesFromStatement(child, env, targetVariableName, scope),
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (ts.isIfStatement(statement)) {
|
|
174
|
+
if (evaluateStaticCondition(statement.expression, env, scope)) {
|
|
175
|
+
return collectPushEntriesFromStatement(statement.thenStatement, env, targetVariableName, scope)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (statement.elseStatement) {
|
|
179
|
+
return collectPushEntriesFromStatement(statement.elseStatement, env, targetVariableName, scope)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return []
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!ts.isExpressionStatement(statement)) return []
|
|
186
|
+
|
|
187
|
+
const expression = statement.expression
|
|
188
|
+
if (!ts.isCallExpression(expression) || !ts.isPropertyAccessExpression(expression.expression)) {
|
|
189
|
+
return []
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const pushTarget = expression.expression.expression
|
|
193
|
+
const pushMethod = expression.expression.name
|
|
194
|
+
|
|
195
|
+
if (!ts.isIdentifier(pushTarget) || pushTarget.text !== targetVariableName || pushMethod.text !== 'push') {
|
|
196
|
+
return []
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return expression.arguments.flatMap((argument) => {
|
|
200
|
+
if (!ts.isObjectLiteralExpression(argument)) return []
|
|
201
|
+
const entry = parseModuleEntryFromObjectLiteral(argument)
|
|
202
|
+
return entry ? [{ entry, node: argument }] : []
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parseModuleConfigShape(
|
|
207
|
+
source: string,
|
|
208
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
209
|
+
): ModuleConfigShape {
|
|
210
|
+
const sourceFile = ts.createSourceFile('modules.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
|
211
|
+
const variableName = 'enabledModules'
|
|
212
|
+
const occurrences: ModuleOccurrence[] = []
|
|
213
|
+
const scope = new Map<string, unknown>()
|
|
214
|
+
let arrayNode: ts.ArrayLiteralExpression | null = null
|
|
215
|
+
let foundDeclaration = false
|
|
216
|
+
|
|
217
|
+
for (const statement of sourceFile.statements) {
|
|
218
|
+
if (ts.isVariableStatement(statement)) {
|
|
219
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
220
|
+
if (!ts.isIdentifier(declaration.name)) continue
|
|
221
|
+
|
|
222
|
+
if (declaration.name.text === variableName) {
|
|
223
|
+
if (!declaration.initializer || !ts.isArrayLiteralExpression(declaration.initializer)) continue
|
|
224
|
+
|
|
225
|
+
arrayNode = declaration.initializer
|
|
226
|
+
occurrences.push(...declaration.initializer.elements.flatMap((element) => {
|
|
227
|
+
if (!ts.isObjectLiteralExpression(element)) return []
|
|
228
|
+
const entry = parseModuleEntryFromObjectLiteral(element)
|
|
229
|
+
return entry ? [{ entry, node: element }] : []
|
|
230
|
+
}))
|
|
231
|
+
foundDeclaration = true
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!declaration.initializer) continue
|
|
236
|
+
|
|
237
|
+
scope.set(
|
|
238
|
+
declaration.name.text,
|
|
239
|
+
evaluateStaticExpressionWithScope(declaration.initializer, env, scope),
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!foundDeclaration) continue
|
|
246
|
+
|
|
247
|
+
occurrences.push(...collectPushEntriesFromStatement(statement, env, variableName, scope))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!arrayNode) {
|
|
251
|
+
throw new Error('Could not find enabledModules array declaration in src/modules.ts')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { arrayNode, occurrences }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizeModuleEntry(entry: ModuleEntry): ModuleEntry {
|
|
258
|
+
return {
|
|
259
|
+
id: entry.id,
|
|
260
|
+
from: entry.from ?? '@open-mercato/core',
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function escapeSingleQuotedString(value: string): string {
|
|
265
|
+
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function buildModuleEntryLiteral(entry: ModuleEntry): string {
|
|
269
|
+
const normalized = normalizeModuleEntry(entry)
|
|
270
|
+
return `{ id: '${escapeSingleQuotedString(normalized.id)}', from: '${escapeSingleQuotedString(normalized.from ?? '@open-mercato/core')}' }`
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function lineIndentAt(source: string, position: number): string {
|
|
274
|
+
const lineStart = source.lastIndexOf('\n', Math.max(0, position - 1)) + 1
|
|
275
|
+
const lineToPosition = source.slice(lineStart, position)
|
|
276
|
+
const match = lineToPosition.match(/^\s*/)
|
|
277
|
+
return match?.[0] ?? ''
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function replaceArrayLiteral(
|
|
281
|
+
source: string,
|
|
282
|
+
arrayNode: ts.ArrayLiteralExpression,
|
|
283
|
+
entry: ModuleEntry,
|
|
284
|
+
sourceFile: ts.SourceFile,
|
|
285
|
+
): string {
|
|
286
|
+
const existingElements = arrayNode.elements.map((element) =>
|
|
287
|
+
source.slice(element.getStart(sourceFile), element.end).trim(),
|
|
288
|
+
)
|
|
289
|
+
const closingIndent = lineIndentAt(source, arrayNode.end - 1)
|
|
290
|
+
const elementIndent =
|
|
291
|
+
arrayNode.elements.length > 0
|
|
292
|
+
? lineIndentAt(source, arrayNode.elements[0].getStart(sourceFile))
|
|
293
|
+
: `${closingIndent} `
|
|
294
|
+
const updatedArray = [
|
|
295
|
+
'[',
|
|
296
|
+
...[...existingElements, buildModuleEntryLiteral(entry)].map((value) => `${elementIndent}${value},`),
|
|
297
|
+
`${closingIndent}]`,
|
|
298
|
+
].join('\n')
|
|
299
|
+
|
|
300
|
+
return source.slice(0, arrayNode.getStart(sourceFile)) + updatedArray + source.slice(arrayNode.end)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function upsertModuleSource(objectLiteral: string, from: string): string {
|
|
304
|
+
if (/from\s*:\s*'[^']*'/.test(objectLiteral)) {
|
|
305
|
+
return objectLiteral.replace(/from\s*:\s*'[^']*'/, `from: '${escapeSingleQuotedString(from)}'`)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (/from\s*:\s*"[^"]*"/.test(objectLiteral)) {
|
|
309
|
+
return objectLiteral.replace(/from\s*:\s*"[^"]*"/, `from: "${from.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return objectLiteral.replace(/\}\s*$/, `, from: '${escapeSingleQuotedString(from)}' }`)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function ensureModuleRegistration(
|
|
316
|
+
modulesPath: string,
|
|
317
|
+
entry: ModuleEntry,
|
|
318
|
+
): { changed: boolean; registeredAs: string } {
|
|
319
|
+
if (!fs.existsSync(modulesPath)) {
|
|
320
|
+
throw new Error(`modules.ts not found at ${modulesPath}`)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const source = fs.readFileSync(modulesPath, 'utf8')
|
|
324
|
+
const sourceFile = ts.createSourceFile('modules.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
|
325
|
+
const config = parseModuleConfigShape(source)
|
|
326
|
+
const normalized = normalizeModuleEntry(entry)
|
|
327
|
+
const matches = config.occurrences.filter((occurrence) => occurrence.entry.id === normalized.id)
|
|
328
|
+
|
|
329
|
+
if (matches.length > 1) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Module "${normalized.id}" is registered multiple times in ${modulesPath}. Resolve duplicates manually before running this command.`,
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (matches.length === 1) {
|
|
336
|
+
const existing = normalizeModuleEntry(matches[0].entry)
|
|
337
|
+
if (existing.from === normalized.from) {
|
|
338
|
+
return { changed: false, registeredAs: normalized.from ?? '@open-mercato/core' }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
throw new Error(
|
|
342
|
+
`Module "${normalized.id}" is already registered from "${existing.from}". Remove or eject the existing registration first.`,
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const updated = replaceArrayLiteral(source, config.arrayNode, normalized, sourceFile)
|
|
347
|
+
fs.writeFileSync(modulesPath, updated)
|
|
348
|
+
|
|
349
|
+
return { changed: true, registeredAs: normalized.from ?? '@open-mercato/core' }
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function setModuleRegistrationSource(
|
|
353
|
+
modulesPath: string,
|
|
354
|
+
moduleId: string,
|
|
355
|
+
from: string,
|
|
356
|
+
): { changed: boolean } {
|
|
357
|
+
if (!fs.existsSync(modulesPath)) {
|
|
358
|
+
throw new Error(`modules.ts not found at ${modulesPath}`)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const source = fs.readFileSync(modulesPath, 'utf8')
|
|
362
|
+
const config = parseModuleConfigShape(source)
|
|
363
|
+
const matches = config.occurrences.filter((occurrence) => occurrence.entry.id === moduleId)
|
|
364
|
+
|
|
365
|
+
if (matches.length === 0) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
`Could not find module entry for "${moduleId}" in ${modulesPath}. Expected a pattern like: { id: '${moduleId}', from: '...' } or { id: '${moduleId}' }`,
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (matches.length > 1) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
`Module "${moduleId}" is registered multiple times in ${modulesPath}. Resolve duplicates manually before running this command.`,
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const match = matches[0]
|
|
378
|
+
const current = normalizeModuleEntry(match.entry)
|
|
379
|
+
if (current.from === from) {
|
|
380
|
+
return { changed: false }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const objectLiteral = source.slice(match.node.getStart(), match.node.end)
|
|
384
|
+
const updatedObjectLiteral = upsertModuleSource(objectLiteral, from)
|
|
385
|
+
const updated =
|
|
386
|
+
source.slice(0, match.node.getStart()) +
|
|
387
|
+
updatedObjectLiteral +
|
|
388
|
+
source.slice(match.node.end)
|
|
389
|
+
|
|
390
|
+
fs.writeFileSync(modulesPath, updated)
|
|
391
|
+
|
|
392
|
+
return { changed: true }
|
|
393
|
+
}
|
package/src/lib/resolver.ts
CHANGED
|
@@ -3,6 +3,25 @@ import fs from 'node:fs'
|
|
|
3
3
|
import ts from 'typescript'
|
|
4
4
|
import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Resolved execution environment for the CLI.
|
|
8
|
+
* Produced once by resolveEnvironment() and consumed by all subsystems.
|
|
9
|
+
* Eliminates per-subsystem isMonorepo() branching and hardcoded path segments.
|
|
10
|
+
*/
|
|
11
|
+
export type CliEnvironment = {
|
|
12
|
+
/** Whether the CLI is running inside a Yarn/npm workspace monorepo. */
|
|
13
|
+
mode: 'monorepo' | 'standalone'
|
|
14
|
+
/** Workspace or project root (where package.json / node_modules live). */
|
|
15
|
+
rootDir: string
|
|
16
|
+
/** Next.js application directory (contains src/, package.json, next.config.*). */
|
|
17
|
+
appDir: string
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the root directory of an installed @open-mercato package.
|
|
20
|
+
* Monorepo: packages/<pkg>/ Standalone: node_modules/<pkg>/
|
|
21
|
+
*/
|
|
22
|
+
packageRoot: (packageName: string) => string
|
|
23
|
+
}
|
|
24
|
+
|
|
6
25
|
export type ModuleEntry = {
|
|
7
26
|
id: string
|
|
8
27
|
from?: '@open-mercato/core' | '@app' | string
|
|
@@ -419,18 +438,24 @@ function detectMonorepoFromNodeModules(appDir: string): { isMonorepo: boolean; m
|
|
|
419
438
|
|
|
420
439
|
export function createResolver(cwd: string = process.cwd()): PackageResolver {
|
|
421
440
|
// First detect if we're in a monorepo by checking if node_modules packages are symlinks
|
|
422
|
-
const { isMonorepo: _isMonorepo, monorepoRoot } = detectMonorepoFromNodeModules(cwd)
|
|
423
|
-
|
|
441
|
+
const { isMonorepo: _isMonorepo, monorepoRoot, nodeModulesRoot } = detectMonorepoFromNodeModules(cwd)
|
|
442
|
+
// In workspaces with hoisted real directories, package sources live under the discovered node_modules root.
|
|
443
|
+
const rootDir = monorepoRoot ?? nodeModulesRoot ?? cwd
|
|
444
|
+
|
|
445
|
+
// shouldResolveAppFromRoot: true when we have a workspace/install root that differs from cwd
|
|
446
|
+
// (monorepo symlinks detected, or node_modules root found above cwd)
|
|
447
|
+
const shouldResolveAppFromRoot =
|
|
448
|
+
_isMonorepo || (nodeModulesRoot !== null && path.resolve(nodeModulesRoot) !== path.resolve(cwd))
|
|
424
449
|
|
|
425
450
|
// The app directory depends on context:
|
|
426
451
|
// - In monorepo: use detectAppDir to find apps/mercato or similar
|
|
427
452
|
// - When symlinks not detected (e.g. Docker volume node_modules): still use apps/mercato if present at rootDir
|
|
428
453
|
// - Otherwise: app is at cwd
|
|
429
|
-
const candidateAppDir = detectAppDir(rootDir, true)
|
|
454
|
+
const candidateAppDir = shouldResolveAppFromRoot ? detectAppDir(rootDir, true) : rootDir
|
|
430
455
|
const appDir =
|
|
431
456
|
_isMonorepo
|
|
432
457
|
? candidateAppDir
|
|
433
|
-
: candidateAppDir !== rootDir && fs.existsSync(candidateAppDir)
|
|
458
|
+
: shouldResolveAppFromRoot && candidateAppDir !== rootDir && fs.existsSync(candidateAppDir)
|
|
434
459
|
? candidateAppDir
|
|
435
460
|
: cwd
|
|
436
461
|
|
|
@@ -486,3 +511,20 @@ export function createResolver(cwd: string = process.cwd()): PackageResolver {
|
|
|
486
511
|
},
|
|
487
512
|
}
|
|
488
513
|
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Resolve the CLI execution environment as a plain value-object.
|
|
517
|
+
* Call once at subsystem init; use the returned object for all path decisions.
|
|
518
|
+
*/
|
|
519
|
+
export function resolveEnvironment(cwd: string = process.cwd()): CliEnvironment {
|
|
520
|
+
const resolver = createResolver(cwd)
|
|
521
|
+
const _isMonorepo = resolver.isMonorepo()
|
|
522
|
+
const rootDir = resolver.getRootDir()
|
|
523
|
+
const appDir = resolver.getAppDir()
|
|
524
|
+
return {
|
|
525
|
+
mode: _isMonorepo ? 'monorepo' : 'standalone',
|
|
526
|
+
rootDir,
|
|
527
|
+
appDir,
|
|
528
|
+
packageRoot: (packageName: string) => pkgRootFor(rootDir, packageName, _isMonorepo),
|
|
529
|
+
}
|
|
530
|
+
}
|
|
@@ -109,4 +109,34 @@ describe('integration discovery', () => {
|
|
|
109
109
|
'packages/create-app/template/src/modules/auth/__integration__/TC-AUTH-001.spec.ts',
|
|
110
110
|
])
|
|
111
111
|
})
|
|
112
|
+
|
|
113
|
+
it('discovers standalone app integration tests from src/modules', async () => {
|
|
114
|
+
await writeTestFile(tempRoot, 'src/modules/auth/.gitkeep')
|
|
115
|
+
await writeTestFile(tempRoot, 'src/modules/sales/.gitkeep')
|
|
116
|
+
await writeTestFile(
|
|
117
|
+
tempRoot,
|
|
118
|
+
'src/modules/sales/__integration__/TC-SALES-020.spec.ts',
|
|
119
|
+
'export {}\n',
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
const discovered = discoverIntegrationSpecFiles(tempRoot, path.join(tempRoot, '.ai', 'qa', 'tests'))
|
|
123
|
+
expect(discovered.map((entry) => entry.path)).toEqual([
|
|
124
|
+
'src/modules/sales/__integration__/TC-SALES-020.spec.ts',
|
|
125
|
+
])
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('discovers standalone package integration tests from node_modules/@open-mercato', async () => {
|
|
129
|
+
await writeTestFile(tempRoot, 'src/modules/customers/.gitkeep')
|
|
130
|
+
await writeTestFile(tempRoot, 'node_modules/@open-mercato/core/src/modules/customers/.gitkeep')
|
|
131
|
+
await writeTestFile(
|
|
132
|
+
tempRoot,
|
|
133
|
+
'node_modules/@open-mercato/core/src/modules/customers/__integration__/TC-CRM-020.spec.ts',
|
|
134
|
+
'export {}\n',
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const discovered = discoverIntegrationSpecFiles(tempRoot, path.join(tempRoot, '.ai', 'qa', 'tests'))
|
|
138
|
+
expect(discovered.map((entry) => entry.path)).toEqual([
|
|
139
|
+
'node_modules/@open-mercato/core/src/modules/customers/__integration__/TC-CRM-020.spec.ts',
|
|
140
|
+
])
|
|
141
|
+
})
|
|
112
142
|
})
|
|
@@ -96,31 +96,44 @@ function collectDirectDirectoryNames(directoryPath: string): string[] {
|
|
|
96
96
|
.map((entry) => entry.name)
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
function collectModuleIdsFromModulesRoot(modulesRoot: string, enabledModules: Set<string>): void {
|
|
100
|
+
for (const moduleName of collectDirectDirectoryNames(modulesRoot)) {
|
|
101
|
+
enabledModules.add(normalizeModuleId(moduleName))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
99
105
|
function resolveEnabledModuleIds(projectRoot: string): Set<string> {
|
|
100
106
|
const enabledModules = new Set<string>()
|
|
107
|
+
const appModulesRoot = path.join(projectRoot, 'src', 'modules')
|
|
101
108
|
const appsRoot = path.join(projectRoot, 'apps')
|
|
102
109
|
const packagesRoot = path.join(projectRoot, 'packages')
|
|
110
|
+
const installedPackagesRoot = path.join(projectRoot, 'node_modules', '@open-mercato')
|
|
103
111
|
const enterpriseEnabled = isEnterpriseModulesEnabled()
|
|
104
112
|
|
|
113
|
+
// Standalone app: src/modules at project root
|
|
114
|
+
collectModuleIdsFromModulesRoot(appModulesRoot, enabledModules)
|
|
115
|
+
|
|
105
116
|
for (const appName of collectDirectDirectoryNames(appsRoot)) {
|
|
106
117
|
const moduleRoot = path.join(appsRoot, appName, 'src', 'modules')
|
|
107
|
-
|
|
108
|
-
enabledModules.add(normalizeModuleId(moduleName))
|
|
109
|
-
}
|
|
118
|
+
collectModuleIdsFromModulesRoot(moduleRoot, enabledModules)
|
|
110
119
|
}
|
|
111
120
|
for (const packageName of collectDirectDirectoryNames(packagesRoot)) {
|
|
112
121
|
if (packageName === 'enterprise' && !enterpriseEnabled) {
|
|
113
122
|
continue
|
|
114
123
|
}
|
|
115
124
|
const moduleRoot = path.join(packagesRoot, packageName, 'src', 'modules')
|
|
116
|
-
|
|
117
|
-
|
|
125
|
+
collectModuleIdsFromModulesRoot(moduleRoot, enabledModules)
|
|
126
|
+
}
|
|
127
|
+
// Standalone app: installed @open-mercato packages in node_modules
|
|
128
|
+
for (const packageName of collectDirectDirectoryNames(installedPackagesRoot)) {
|
|
129
|
+
if (packageName === 'enterprise' && !enterpriseEnabled) {
|
|
130
|
+
continue
|
|
118
131
|
}
|
|
132
|
+
const moduleRoot = path.join(installedPackagesRoot, packageName, 'src', 'modules')
|
|
133
|
+
collectModuleIdsFromModulesRoot(moduleRoot, enabledModules)
|
|
119
134
|
}
|
|
120
135
|
const createAppTemplateModulesRoot = path.join(projectRoot, 'packages', 'create-app', 'template', 'src', 'modules')
|
|
121
|
-
|
|
122
|
-
enabledModules.add(normalizeModuleId(moduleName))
|
|
123
|
-
}
|
|
136
|
+
collectModuleIdsFromModulesRoot(createAppTemplateModulesRoot, enabledModules)
|
|
124
137
|
|
|
125
138
|
return enabledModules
|
|
126
139
|
}
|
|
@@ -223,8 +236,10 @@ export function discoverIntegrationSpecFiles(projectRoot: string, legacyIntegrat
|
|
|
223
236
|
const overlayRoot = resolveOverlayRootPath()
|
|
224
237
|
const enterpriseEnabled = isEnterpriseModulesEnabled()
|
|
225
238
|
const discoveryRoots = [
|
|
239
|
+
path.join(projectRoot, 'src', 'modules'),
|
|
226
240
|
path.join(projectRoot, 'apps'),
|
|
227
241
|
path.join(projectRoot, 'packages'),
|
|
242
|
+
path.join(projectRoot, 'node_modules', '@open-mercato'),
|
|
228
243
|
]
|
|
229
244
|
|
|
230
245
|
for (const specFile of collectSpecFilesFromDirectory(legacyIntegrationRoot)) {
|