@open-mercato/cli 0.4.9-develop-94fb251ed3 → 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,234 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import { copyDirRecursive, rewriteCrossModuleImports } from './eject'
|
|
5
|
+
import {
|
|
6
|
+
parsePackageNameFromSpec,
|
|
7
|
+
resolveInstalledOfficialModulePackage,
|
|
8
|
+
validateEjectBoundaries,
|
|
9
|
+
type ValidatedOfficialModulePackage,
|
|
10
|
+
} from './module-package'
|
|
11
|
+
import { ensureModuleRegistration } from './modules-config'
|
|
12
|
+
import type { PackageResolver } from './resolver'
|
|
13
|
+
|
|
14
|
+
type ModuleCommandResult = {
|
|
15
|
+
moduleId: string
|
|
16
|
+
packageName: string
|
|
17
|
+
from: string
|
|
18
|
+
registrationChanged: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type InstallTarget = {
|
|
22
|
+
cwd: string
|
|
23
|
+
args: string[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveYarnBinary(): string {
|
|
27
|
+
return process.platform === 'win32' ? 'yarn.cmd' : 'yarn'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readAppPackageName(appDir: string): string {
|
|
31
|
+
const packageJsonPath = path.join(appDir, 'package.json')
|
|
32
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
33
|
+
throw new Error(`App package.json not found at ${packageJsonPath}.`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as { name?: string }
|
|
37
|
+
if (!packageJson.name) {
|
|
38
|
+
throw new Error(`App package.json at ${packageJsonPath} is missing the "name" field.`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return packageJson.name
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveInstallTarget(resolver: PackageResolver): InstallTarget {
|
|
45
|
+
if (!resolver.isMonorepo()) {
|
|
46
|
+
return {
|
|
47
|
+
cwd: resolver.getAppDir(),
|
|
48
|
+
args: ['add'],
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
cwd: resolver.getRootDir(),
|
|
54
|
+
args: ['workspace', readAppPackageName(resolver.getAppDir()), 'add'],
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function runCommand(
|
|
59
|
+
command: string,
|
|
60
|
+
args: string[],
|
|
61
|
+
cwd: string,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const child = spawn(command, args, {
|
|
65
|
+
cwd,
|
|
66
|
+
env: process.env,
|
|
67
|
+
stdio: 'inherit',
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
child.on('error', reject)
|
|
71
|
+
child.on('exit', (code) => {
|
|
72
|
+
if (code === 0) {
|
|
73
|
+
resolve()
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
reject(new Error(`Command failed: ${command} ${args.join(' ')} (exit ${code ?? 'unknown'}).`))
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function installPackageSpec(
|
|
83
|
+
resolver: PackageResolver,
|
|
84
|
+
packageSpec: string,
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
const target = resolveInstallTarget(resolver)
|
|
87
|
+
await runCommand(resolveYarnBinary(), [...target.args, packageSpec], target.cwd)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function runModuleGenerators(
|
|
91
|
+
resolver: PackageResolver,
|
|
92
|
+
quiet = true,
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
const {
|
|
95
|
+
generateEntityIds,
|
|
96
|
+
generateModuleDi,
|
|
97
|
+
generateModuleEntities,
|
|
98
|
+
generateModulePackageSources,
|
|
99
|
+
generateModuleRegistry,
|
|
100
|
+
generateModuleRegistryCli,
|
|
101
|
+
generateOpenApi,
|
|
102
|
+
} = await import('./generators')
|
|
103
|
+
|
|
104
|
+
await generateEntityIds({ resolver, quiet })
|
|
105
|
+
await generateModuleRegistry({ resolver, quiet })
|
|
106
|
+
await generateModuleRegistryCli({ resolver, quiet })
|
|
107
|
+
await generateModuleEntities({ resolver, quiet })
|
|
108
|
+
await generateModuleDi({ resolver, quiet })
|
|
109
|
+
await generateModulePackageSources({ resolver, quiet })
|
|
110
|
+
await generateOpenApi({ resolver, quiet })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function runGeneratorsWithRegistrationNotice(
|
|
114
|
+
resolver: PackageResolver,
|
|
115
|
+
moduleId: string,
|
|
116
|
+
): Promise<void> {
|
|
117
|
+
try {
|
|
118
|
+
await runModuleGenerators(resolver)
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Module "${moduleId}" is enabled, but generated artifacts are stale because generation failed. Rerun "yarn mercato generate" after fixing the underlying error. ${message}`,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function assertPackageName(packageName: string | null): asserts packageName is string {
|
|
128
|
+
if (!packageName || !packageName.startsWith('@open-mercato/')) {
|
|
129
|
+
throw new Error('Only @open-mercato/* package specs are supported by "mercato module add".')
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function validateBeforeRegistration(
|
|
134
|
+
modulePackage: ValidatedOfficialModulePackage,
|
|
135
|
+
resolver: PackageResolver,
|
|
136
|
+
eject: boolean,
|
|
137
|
+
): string {
|
|
138
|
+
const appModuleDir = path.join(resolver.getAppDir(), 'src', 'modules', modulePackage.metadata.moduleId)
|
|
139
|
+
|
|
140
|
+
if (eject && fs.existsSync(appModuleDir)) {
|
|
141
|
+
throw new Error(`Destination directory already exists: ${appModuleDir}. Remove it first or rerun without --eject.`)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return appModuleDir
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function prepareRegistrationSource(
|
|
148
|
+
modulePackage: ValidatedOfficialModulePackage,
|
|
149
|
+
resolver: PackageResolver,
|
|
150
|
+
packageName: string,
|
|
151
|
+
eject: boolean,
|
|
152
|
+
): string {
|
|
153
|
+
const appModuleDir = validateBeforeRegistration(modulePackage, resolver, eject)
|
|
154
|
+
|
|
155
|
+
if (!eject) {
|
|
156
|
+
return packageName
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!modulePackage.metadata.ejectable) {
|
|
160
|
+
throw new Error(`Package "${packageName}" is not ejectable. --eject requires open-mercato.ejectable === true.`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
validateEjectBoundaries(modulePackage)
|
|
164
|
+
copyDirRecursive(modulePackage.sourceModuleDir, appModuleDir)
|
|
165
|
+
rewriteCrossModuleImports(
|
|
166
|
+
modulePackage.sourceModuleDir,
|
|
167
|
+
appModuleDir,
|
|
168
|
+
modulePackage.metadata.moduleId,
|
|
169
|
+
packageName,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return '@app'
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function registerResolvedOfficialModule(
|
|
176
|
+
resolver: PackageResolver,
|
|
177
|
+
modulePackage: ValidatedOfficialModulePackage,
|
|
178
|
+
packageName: string,
|
|
179
|
+
eject: boolean,
|
|
180
|
+
): Promise<ModuleCommandResult> {
|
|
181
|
+
const from = prepareRegistrationSource(modulePackage, resolver, packageName, eject)
|
|
182
|
+
|
|
183
|
+
const registration = ensureModuleRegistration(
|
|
184
|
+
resolver.getModulesConfigPath(),
|
|
185
|
+
{
|
|
186
|
+
id: modulePackage.metadata.moduleId,
|
|
187
|
+
from,
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if (!registration.changed) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Module "${modulePackage.metadata.moduleId}" from "${from}" is already enabled in modules.ts.`,
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await runGeneratorsWithRegistrationNotice(resolver, modulePackage.metadata.moduleId)
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
moduleId: modulePackage.metadata.moduleId,
|
|
201
|
+
packageName,
|
|
202
|
+
from,
|
|
203
|
+
registrationChanged: registration.changed,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function addOfficialModule(
|
|
208
|
+
resolver: PackageResolver,
|
|
209
|
+
packageSpec: string,
|
|
210
|
+
eject: boolean,
|
|
211
|
+
moduleId?: string,
|
|
212
|
+
): Promise<ModuleCommandResult> {
|
|
213
|
+
const packageName = parsePackageNameFromSpec(packageSpec)
|
|
214
|
+
assertPackageName(packageName)
|
|
215
|
+
|
|
216
|
+
await installPackageSpec(resolver, packageSpec)
|
|
217
|
+
|
|
218
|
+
const modulePackage = resolveInstalledOfficialModulePackage(resolver, packageName, moduleId)
|
|
219
|
+
return registerResolvedOfficialModule(resolver, modulePackage, packageName, eject)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function enableOfficialModule(
|
|
223
|
+
resolver: PackageResolver,
|
|
224
|
+
packageName: string,
|
|
225
|
+
moduleId?: string,
|
|
226
|
+
eject = false,
|
|
227
|
+
): Promise<ModuleCommandResult> {
|
|
228
|
+
if (!packageName.startsWith('@open-mercato/')) {
|
|
229
|
+
throw new Error('Only @open-mercato/* packages can be enabled with "mercato module enable".')
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const modulePackage = resolveInstalledOfficialModulePackage(resolver, packageName, moduleId)
|
|
233
|
+
return registerResolvedOfficialModule(resolver, modulePackage, packageName, eject)
|
|
234
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import type { PackageResolver } from './resolver'
|
|
5
|
+
|
|
6
|
+
type PackageJsonRecord = {
|
|
7
|
+
name?: string
|
|
8
|
+
version?: string
|
|
9
|
+
dependencies?: Record<string, string>
|
|
10
|
+
peerDependencies?: Record<string, string>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ModulePackageMetadata = {
|
|
14
|
+
moduleId: string
|
|
15
|
+
ejectable: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type ModuleInfoSnapshot = {
|
|
19
|
+
title?: string
|
|
20
|
+
description?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ValidatedOfficialModulePackage = {
|
|
24
|
+
packageName: string
|
|
25
|
+
packageRoot: string
|
|
26
|
+
packageJson: PackageJsonRecord
|
|
27
|
+
metadata: ModulePackageMetadata
|
|
28
|
+
moduleInfo: ModuleInfoSnapshot
|
|
29
|
+
sourceModuleDir: string
|
|
30
|
+
distModuleDir: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type DiscoveredModule = {
|
|
34
|
+
moduleId: string
|
|
35
|
+
ejectable: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SOURCE_FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']
|
|
39
|
+
const SKIP_DIRS = new Set(['__tests__', '__mocks__', 'node_modules'])
|
|
40
|
+
const MODULE_ID_PATTERN = /^[a-z0-9]+(?:_[a-z0-9]+)*$/
|
|
41
|
+
const requireFromCli = createRequire(path.join(process.cwd(), 'package.json'))
|
|
42
|
+
|
|
43
|
+
function shouldSkipEntryName(name: string): boolean {
|
|
44
|
+
return SKIP_DIRS.has(name) || name === '.DS_Store' || name.startsWith('._')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readPackageJson(packageJsonPath: string): PackageJsonRecord {
|
|
48
|
+
try {
|
|
49
|
+
const raw = fs.readFileSync(packageJsonPath, 'utf8')
|
|
50
|
+
return JSON.parse(raw) as PackageJsonRecord
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
53
|
+
throw new Error(`Failed to read package manifest at ${packageJsonPath}: ${message}`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseModuleInfo(indexPath: string): { title?: string; description?: string; ejectable?: boolean } {
|
|
58
|
+
if (!fs.existsSync(indexPath)) return {}
|
|
59
|
+
|
|
60
|
+
const source = fs.readFileSync(indexPath, 'utf8')
|
|
61
|
+
const result: { title?: string; description?: string; ejectable?: boolean } = {}
|
|
62
|
+
|
|
63
|
+
const titleMatch = source.match(/\btitle\s*:\s*['"]([^'"]+)['"]/)
|
|
64
|
+
if (titleMatch) result.title = titleMatch[1]
|
|
65
|
+
|
|
66
|
+
const descriptionMatch = source.match(/\bdescription\s*:\s*['"]([^'"]+)['"]/)
|
|
67
|
+
if (descriptionMatch) result.description = descriptionMatch[1]
|
|
68
|
+
|
|
69
|
+
const ejectableMatch = source.match(/\bejectable\s*:\s*(true|false)/)
|
|
70
|
+
if (ejectableMatch) result.ejectable = ejectableMatch[1] === 'true'
|
|
71
|
+
|
|
72
|
+
return result
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function discoverModulesInPackage(packageRoot: string): DiscoveredModule[] {
|
|
76
|
+
const srcModulesDir = path.join(packageRoot, 'src', 'modules')
|
|
77
|
+
const distModulesDir = path.join(packageRoot, 'dist', 'modules')
|
|
78
|
+
|
|
79
|
+
const enumerationDir = fs.existsSync(srcModulesDir)
|
|
80
|
+
? srcModulesDir
|
|
81
|
+
: fs.existsSync(distModulesDir)
|
|
82
|
+
? distModulesDir
|
|
83
|
+
: null
|
|
84
|
+
|
|
85
|
+
if (!enumerationDir) return []
|
|
86
|
+
|
|
87
|
+
const modules: DiscoveredModule[] = []
|
|
88
|
+
|
|
89
|
+
for (const entry of fs.readdirSync(enumerationDir, { withFileTypes: true })) {
|
|
90
|
+
if (!entry.isDirectory() || shouldSkipEntryName(entry.name) || !MODULE_ID_PATTERN.test(entry.name)) continue
|
|
91
|
+
|
|
92
|
+
const moduleId = entry.name
|
|
93
|
+
const srcIndexPath = path.join(packageRoot, 'src', 'modules', moduleId, 'index.ts')
|
|
94
|
+
const distIndexPath = path.join(packageRoot, 'dist', 'modules', moduleId, 'index.js')
|
|
95
|
+
const indexPath = fs.existsSync(srcIndexPath) ? srcIndexPath : distIndexPath
|
|
96
|
+
|
|
97
|
+
const info = parseModuleInfo(indexPath)
|
|
98
|
+
modules.push({ moduleId, ejectable: info.ejectable ?? false })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return modules
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveRelativeImportTarget(sourceFile: string, importPath: string): string | null {
|
|
105
|
+
if (!importPath.startsWith('.')) return null
|
|
106
|
+
|
|
107
|
+
const basePath = path.resolve(path.dirname(sourceFile), importPath)
|
|
108
|
+
const candidates = [basePath]
|
|
109
|
+
|
|
110
|
+
for (const ext of SOURCE_FILE_EXTENSIONS) {
|
|
111
|
+
candidates.push(`${basePath}${ext}`)
|
|
112
|
+
candidates.push(path.join(basePath, `index${ext}`))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const candidate of candidates) {
|
|
116
|
+
if (fs.existsSync(candidate)) {
|
|
117
|
+
return candidate
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function collectSourceFiles(dir: string): string[] {
|
|
125
|
+
const files: string[] = []
|
|
126
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
127
|
+
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
if (shouldSkipEntryName(entry.name)) continue
|
|
130
|
+
|
|
131
|
+
const fullPath = path.join(dir, entry.name)
|
|
132
|
+
if (entry.isDirectory()) {
|
|
133
|
+
files.push(...collectSourceFiles(fullPath))
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const ext = path.extname(entry.name)
|
|
138
|
+
if (!SOURCE_FILE_EXTENSIONS.includes(ext)) continue
|
|
139
|
+
files.push(fullPath)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return files
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function collectBoundaryViolations(moduleDir: string): string[] {
|
|
146
|
+
const modulesRoot = path.resolve(moduleDir, '..')
|
|
147
|
+
const modulePrefix = `${moduleDir}${path.sep}`
|
|
148
|
+
const modulesPrefix = `${modulesRoot}${path.sep}`
|
|
149
|
+
const files = collectSourceFiles(moduleDir)
|
|
150
|
+
const violations = new Set<string>()
|
|
151
|
+
|
|
152
|
+
for (const filePath of files) {
|
|
153
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
154
|
+
const matches = [
|
|
155
|
+
...content.matchAll(/\bfrom\s*['"]([^'"]+)['"]/g),
|
|
156
|
+
...content.matchAll(/\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g),
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
for (const match of matches) {
|
|
160
|
+
const specifier = match[1]
|
|
161
|
+
if (!specifier) continue
|
|
162
|
+
const resolvedTarget = resolveRelativeImportTarget(filePath, specifier)
|
|
163
|
+
if (!resolvedTarget) continue
|
|
164
|
+
if (resolvedTarget.startsWith(modulePrefix)) continue
|
|
165
|
+
if (resolvedTarget.startsWith(modulesPrefix)) continue
|
|
166
|
+
|
|
167
|
+
const relativeSource = path.relative(moduleDir, filePath).split(path.sep).join('/')
|
|
168
|
+
const relativeTarget = path.relative(path.dirname(filePath), resolvedTarget).split(path.sep).join('/')
|
|
169
|
+
violations.add(`${relativeSource} -> ${relativeTarget}`)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return Array.from(violations).sort((left, right) => left.localeCompare(right))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function findResolvedPackageRoot(
|
|
177
|
+
resolvedPath: string,
|
|
178
|
+
packageName: string,
|
|
179
|
+
): string | null {
|
|
180
|
+
let currentPath = fs.statSync(resolvedPath).isDirectory()
|
|
181
|
+
? resolvedPath
|
|
182
|
+
: path.dirname(resolvedPath)
|
|
183
|
+
|
|
184
|
+
while (currentPath !== path.dirname(currentPath)) {
|
|
185
|
+
const packageJsonPath = path.join(currentPath, 'package.json')
|
|
186
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
187
|
+
const packageJson = readPackageJson(packageJsonPath)
|
|
188
|
+
if (packageJson.name === packageName) {
|
|
189
|
+
return currentPath
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
currentPath = path.dirname(currentPath)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function resolveInstalledPackageRootWithRequire(packageName: string, appDir: string): string | null {
|
|
199
|
+
const specifiers = [`${packageName}/package.json`, packageName]
|
|
200
|
+
|
|
201
|
+
for (const specifier of specifiers) {
|
|
202
|
+
try {
|
|
203
|
+
const resolvedPath = requireFromCli.resolve(specifier, {
|
|
204
|
+
paths: [appDir],
|
|
205
|
+
})
|
|
206
|
+
const packageRoot =
|
|
207
|
+
specifier === packageName
|
|
208
|
+
? findResolvedPackageRoot(resolvedPath, packageName)
|
|
209
|
+
: path.dirname(resolvedPath)
|
|
210
|
+
|
|
211
|
+
if (packageRoot) {
|
|
212
|
+
return packageRoot
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function parsePackageNameFromSpec(packageSpec: string): string | null {
|
|
223
|
+
const trimmed = packageSpec.trim()
|
|
224
|
+
if (!trimmed) return null
|
|
225
|
+
|
|
226
|
+
if (trimmed.startsWith('@')) {
|
|
227
|
+
const slashIndex = trimmed.indexOf('/')
|
|
228
|
+
if (slashIndex < 0) return null
|
|
229
|
+
const versionSeparator = trimmed.indexOf('@', slashIndex + 1)
|
|
230
|
+
return versionSeparator < 0 ? trimmed : trimmed.slice(0, versionSeparator)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const versionSeparator = trimmed.indexOf('@')
|
|
234
|
+
return versionSeparator < 0 ? trimmed : trimmed.slice(0, versionSeparator)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function resolveInstalledPackageRoot(
|
|
238
|
+
resolver: PackageResolver,
|
|
239
|
+
packageName: string,
|
|
240
|
+
): string {
|
|
241
|
+
const resolvedWithRequire = resolveInstalledPackageRootWithRequire(packageName, resolver.getAppDir())
|
|
242
|
+
if (resolvedWithRequire) {
|
|
243
|
+
return resolvedWithRequire
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const fallback = resolver.getPackageRoot(packageName)
|
|
247
|
+
if (fs.existsSync(path.join(fallback, 'package.json'))) {
|
|
248
|
+
return fallback
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// In monorepo mode, getPackageRoot resolves to packages/<name> (workspace convention),
|
|
252
|
+
// but externally installed packages land in root node_modules — check there too.
|
|
253
|
+
const nodeModulesFallback = path.join(resolver.getRootDir(), 'node_modules', packageName)
|
|
254
|
+
if (fs.existsSync(path.join(nodeModulesFallback, 'package.json'))) {
|
|
255
|
+
return nodeModulesFallback
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
throw new Error(`Package "${packageName}" is not installed in ${resolver.getAppDir()}.`)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function readOfficialModulePackageFromRoot(
|
|
262
|
+
packageRoot: string,
|
|
263
|
+
expectedPackageName?: string,
|
|
264
|
+
targetModuleId?: string,
|
|
265
|
+
): ValidatedOfficialModulePackage {
|
|
266
|
+
const packageJsonPath = path.join(packageRoot, 'package.json')
|
|
267
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
268
|
+
throw new Error(`Package manifest not found at ${packageJsonPath}.`)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const packageJson = readPackageJson(packageJsonPath)
|
|
272
|
+
const packageName = packageJson.name
|
|
273
|
+
if (!packageName || !packageName.startsWith('@open-mercato/')) {
|
|
274
|
+
throw new Error(`Package at ${packageRoot} is not under the @open-mercato scope.`)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (expectedPackageName && packageName !== expectedPackageName) {
|
|
278
|
+
throw new Error(`Resolved package "${packageName}" does not match requested package "${expectedPackageName}".`)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const discoveredModules = discoverModulesInPackage(packageRoot)
|
|
282
|
+
|
|
283
|
+
if (discoveredModules.length === 0) {
|
|
284
|
+
throw new Error(`Package "${packageName}" has no modules in src/modules/ or dist/modules/.`)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let selected: DiscoveredModule
|
|
288
|
+
|
|
289
|
+
if (targetModuleId) {
|
|
290
|
+
const found = discoveredModules.find((m) => m.moduleId === targetModuleId)
|
|
291
|
+
if (!found) {
|
|
292
|
+
const available = discoveredModules.map((m) => m.moduleId).join(', ')
|
|
293
|
+
throw new Error(
|
|
294
|
+
`Package "${packageName}" does not contain module "${targetModuleId}". Available: ${available}`,
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
selected = found
|
|
298
|
+
} else {
|
|
299
|
+
if (discoveredModules.length > 1) {
|
|
300
|
+
const available = discoveredModules.map((m) => m.moduleId).join(', ')
|
|
301
|
+
throw new Error(
|
|
302
|
+
`Package "${packageName}" contains multiple modules (${available}). Specify one with --module <moduleId>.`,
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
selected = discoveredModules[0]
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const { moduleId } = selected
|
|
309
|
+
const sourceModuleDir = path.join(packageRoot, 'src', 'modules', moduleId)
|
|
310
|
+
const distModuleDir = path.join(packageRoot, 'dist', 'modules', moduleId)
|
|
311
|
+
|
|
312
|
+
if (!fs.existsSync(sourceModuleDir)) {
|
|
313
|
+
throw new Error(`Package "${packageName}" is missing src/modules/${moduleId}.`)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!fs.existsSync(distModuleDir)) {
|
|
317
|
+
throw new Error(`Package "${packageName}" is missing dist/modules/${moduleId}.`)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const info = parseModuleInfo(path.join(sourceModuleDir, 'index.ts'))
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
packageName,
|
|
324
|
+
packageRoot,
|
|
325
|
+
packageJson,
|
|
326
|
+
metadata: { moduleId, ejectable: selected.ejectable },
|
|
327
|
+
moduleInfo: { title: info.title, description: info.description },
|
|
328
|
+
sourceModuleDir,
|
|
329
|
+
distModuleDir,
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function resolveInstalledOfficialModulePackage(
|
|
334
|
+
resolver: PackageResolver,
|
|
335
|
+
packageName: string,
|
|
336
|
+
moduleId?: string,
|
|
337
|
+
): ValidatedOfficialModulePackage {
|
|
338
|
+
const packageRoot = resolveInstalledPackageRoot(resolver, packageName)
|
|
339
|
+
return readOfficialModulePackageFromRoot(packageRoot, packageName, moduleId)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function validateEjectBoundaries(modulePackage: ValidatedOfficialModulePackage): void {
|
|
343
|
+
const violations = collectBoundaryViolations(modulePackage.sourceModuleDir)
|
|
344
|
+
if (violations.length === 0) {
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
throw new Error(
|
|
349
|
+
[
|
|
350
|
+
`Package "${modulePackage.packageName}" cannot be added with --eject because it imports files outside src/modules/${modulePackage.metadata.moduleId}.`,
|
|
351
|
+
'Invalid imports:',
|
|
352
|
+
...violations.map((violation) => `- ${violation}`),
|
|
353
|
+
].join('\n'),
|
|
354
|
+
)
|
|
355
|
+
}
|