@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,104 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { ensureModuleRegistration, setModuleRegistrationSource } from '../modules-config'
|
|
5
|
+
|
|
6
|
+
describe('modules-config', () => {
|
|
7
|
+
let tmpDir: string
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'modules-config-test-'))
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('adds a package-backed module without touching conditional pushes', () => {
|
|
18
|
+
const filePath = path.join(tmpDir, 'modules.ts')
|
|
19
|
+
fs.writeFileSync(
|
|
20
|
+
filePath,
|
|
21
|
+
[
|
|
22
|
+
"export const enabledModules = [",
|
|
23
|
+
" { id: 'auth', from: '@open-mercato/core' },",
|
|
24
|
+
"]",
|
|
25
|
+
'',
|
|
26
|
+
"if (process.env.OM_ENABLE_ENTERPRISE_MODULES === 'true') {",
|
|
27
|
+
" enabledModules.push({ id: 'record_locks', from: '@open-mercato/enterprise' })",
|
|
28
|
+
'}',
|
|
29
|
+
].join('\n'),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const result = ensureModuleRegistration(filePath, {
|
|
33
|
+
id: 'test_package',
|
|
34
|
+
from: '@open-mercato/test-package',
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
expect(result).toEqual({
|
|
38
|
+
changed: true,
|
|
39
|
+
registeredAs: '@open-mercato/test-package',
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const updated = fs.readFileSync(filePath, 'utf8')
|
|
43
|
+
expect(updated).toContain("{ id: 'test_package', from: '@open-mercato/test-package' }")
|
|
44
|
+
expect(updated).toContain("enabledModules.push({ id: 'record_locks', from: '@open-mercato/enterprise' })")
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns no-op when the same registration already exists', () => {
|
|
48
|
+
const filePath = path.join(tmpDir, 'modules.ts')
|
|
49
|
+
fs.writeFileSync(
|
|
50
|
+
filePath,
|
|
51
|
+
[
|
|
52
|
+
"export const enabledModules = [",
|
|
53
|
+
" { id: 'test_package', from: '@open-mercato/test-package' },",
|
|
54
|
+
"]",
|
|
55
|
+
].join('\n'),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const result = ensureModuleRegistration(filePath, {
|
|
59
|
+
id: 'test_package',
|
|
60
|
+
from: '@open-mercato/test-package',
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
expect(result).toEqual({
|
|
64
|
+
changed: false,
|
|
65
|
+
registeredAs: '@open-mercato/test-package',
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('fails when the module is already registered from another source', () => {
|
|
70
|
+
const filePath = path.join(tmpDir, 'modules.ts')
|
|
71
|
+
fs.writeFileSync(
|
|
72
|
+
filePath,
|
|
73
|
+
[
|
|
74
|
+
"export const enabledModules = [",
|
|
75
|
+
" { id: 'test_package', from: '@app' },",
|
|
76
|
+
"]",
|
|
77
|
+
].join('\n'),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
expect(() =>
|
|
81
|
+
ensureModuleRegistration(filePath, {
|
|
82
|
+
id: 'test_package',
|
|
83
|
+
from: '@open-mercato/test-package',
|
|
84
|
+
}),
|
|
85
|
+
).toThrow('already registered from "@app"')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('updates an existing module source to @app', () => {
|
|
89
|
+
const filePath = path.join(tmpDir, 'modules.ts')
|
|
90
|
+
fs.writeFileSync(
|
|
91
|
+
filePath,
|
|
92
|
+
[
|
|
93
|
+
"export const enabledModules = [",
|
|
94
|
+
" { id: 'test_package', from: '@open-mercato/test-package' },",
|
|
95
|
+
"]",
|
|
96
|
+
].join('\n'),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const result = setModuleRegistrationSource(filePath, 'test_package', '@app')
|
|
100
|
+
|
|
101
|
+
expect(result).toEqual({ changed: true })
|
|
102
|
+
expect(fs.readFileSync(filePath, 'utf8')).toContain("{ id: 'test_package', from: '@app' }")
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { realpathSync } from 'node:fs'
|
|
4
|
+
import { mkdtemp, mkdir, rm, symlink } from 'node:fs/promises'
|
|
5
|
+
import { resolveEnvironment } from '../resolver'
|
|
6
|
+
|
|
7
|
+
const normalizePath = (p: string) => p.replace(/\\/g, '/')
|
|
8
|
+
|
|
9
|
+
async function makeDir(root: string, ...segments: string[]): Promise<string> {
|
|
10
|
+
const dir = path.join(root, ...segments)
|
|
11
|
+
await mkdir(dir, { recursive: true })
|
|
12
|
+
return dir
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('resolveEnvironment', () => {
|
|
16
|
+
let tempRoot = ''
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
// realpathSync resolves macOS /var -> /private/var symlink so comparisons are stable
|
|
20
|
+
tempRoot = realpathSync(await mkdtemp(path.join(os.tmpdir(), 'om-resolve-env-')))
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
if (tempRoot) {
|
|
25
|
+
await rm(tempRoot, { recursive: true, force: true })
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('standalone — local node_modules', () => {
|
|
30
|
+
it('returns standalone mode with rootDir = cwd when @open-mercato/core is a real directory at cwd', async () => {
|
|
31
|
+
await makeDir(tempRoot, 'node_modules', '@open-mercato', 'core')
|
|
32
|
+
|
|
33
|
+
const env = resolveEnvironment(tempRoot)
|
|
34
|
+
|
|
35
|
+
expect(env.mode).toBe('standalone')
|
|
36
|
+
expect(normalizePath(env.rootDir)).toBe(normalizePath(tempRoot))
|
|
37
|
+
expect(normalizePath(env.appDir)).toBe(normalizePath(tempRoot))
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('resolves packageRoot under node_modules in standalone mode', async () => {
|
|
41
|
+
await makeDir(tempRoot, 'node_modules', '@open-mercato', 'core')
|
|
42
|
+
|
|
43
|
+
const env = resolveEnvironment(tempRoot)
|
|
44
|
+
|
|
45
|
+
expect(normalizePath(env.packageRoot('@open-mercato/core'))).toBe(
|
|
46
|
+
normalizePath(path.join(tempRoot, 'node_modules', '@open-mercato', 'core')),
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// In local mode (cwd === nodeModulesRoot), appDir === cwd.
|
|
51
|
+
// apps/ subdirectory detection only fires in the hoisted case — see hoisted tests below.
|
|
52
|
+
it('sets appDir to cwd even when apps/mercato subdirectory exists', async () => {
|
|
53
|
+
await makeDir(tempRoot, 'node_modules', '@open-mercato', 'core')
|
|
54
|
+
await makeDir(tempRoot, 'apps', 'mercato')
|
|
55
|
+
|
|
56
|
+
const env = resolveEnvironment(tempRoot)
|
|
57
|
+
|
|
58
|
+
expect(env.mode).toBe('standalone')
|
|
59
|
+
expect(normalizePath(env.appDir)).toBe(normalizePath(tempRoot))
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('standalone — hoisted node_modules', () => {
|
|
64
|
+
it('returns standalone mode with rootDir set to parent when @open-mercato/core is hoisted', async () => {
|
|
65
|
+
// Structure: tempRoot/node_modules/@open-mercato/core (real dir)
|
|
66
|
+
// tempRoot/myapp/ (the cwd)
|
|
67
|
+
await makeDir(tempRoot, 'node_modules', '@open-mercato', 'core')
|
|
68
|
+
const childDir = await makeDir(tempRoot, 'myapp')
|
|
69
|
+
|
|
70
|
+
const env = resolveEnvironment(childDir)
|
|
71
|
+
|
|
72
|
+
expect(env.mode).toBe('standalone')
|
|
73
|
+
expect(normalizePath(env.rootDir)).toBe(normalizePath(tempRoot))
|
|
74
|
+
// appDir falls back to childDir since no apps/ found at rootDir
|
|
75
|
+
expect(normalizePath(env.appDir)).toBe(normalizePath(childDir))
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('detects apps/mercato as appDir when hoisted and apps/mercato exists at rootDir', async () => {
|
|
79
|
+
// Structure: tempRoot/node_modules/@open-mercato/core (real dir)
|
|
80
|
+
// tempRoot/apps/mercato/ (app directory)
|
|
81
|
+
// tempRoot/myapp/ (the cwd — simulates running CLI from a sub-directory)
|
|
82
|
+
await makeDir(tempRoot, 'node_modules', '@open-mercato', 'core')
|
|
83
|
+
await makeDir(tempRoot, 'apps', 'mercato')
|
|
84
|
+
const childDir = await makeDir(tempRoot, 'myapp')
|
|
85
|
+
|
|
86
|
+
const env = resolveEnvironment(childDir)
|
|
87
|
+
|
|
88
|
+
expect(env.mode).toBe('standalone')
|
|
89
|
+
expect(normalizePath(env.rootDir)).toBe(normalizePath(tempRoot))
|
|
90
|
+
expect(normalizePath(env.appDir)).toBe(normalizePath(path.join(tempRoot, 'apps', 'mercato')))
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('resolves packageRoot under hoisted node_modules', async () => {
|
|
94
|
+
await makeDir(tempRoot, 'node_modules', '@open-mercato', 'core')
|
|
95
|
+
const childDir = await makeDir(tempRoot, 'myapp')
|
|
96
|
+
|
|
97
|
+
const env = resolveEnvironment(childDir)
|
|
98
|
+
|
|
99
|
+
expect(normalizePath(env.packageRoot('@open-mercato/core'))).toBe(
|
|
100
|
+
normalizePath(path.join(tempRoot, 'node_modules', '@open-mercato', 'core')),
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('monorepo — symlinked node_modules', () => {
|
|
106
|
+
it('returns monorepo mode when @open-mercato/core is a symlink', async () => {
|
|
107
|
+
// Structure: tempRoot/packages/core (real source)
|
|
108
|
+
// tempRoot/node_modules/@open-mercato/core -> ../../packages/core (symlink)
|
|
109
|
+
const packagesCore = await makeDir(tempRoot, 'packages', 'core')
|
|
110
|
+
const nmOpen = await makeDir(tempRoot, 'node_modules', '@open-mercato')
|
|
111
|
+
await symlink(packagesCore, path.join(nmOpen, 'core'))
|
|
112
|
+
|
|
113
|
+
const env = resolveEnvironment(tempRoot)
|
|
114
|
+
|
|
115
|
+
expect(env.mode).toBe('monorepo')
|
|
116
|
+
expect(normalizePath(env.rootDir)).toBe(normalizePath(tempRoot))
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('resolves packageRoot under packages/ in monorepo mode', async () => {
|
|
120
|
+
const packagesCore = await makeDir(tempRoot, 'packages', 'core')
|
|
121
|
+
const nmOpen = await makeDir(tempRoot, 'node_modules', '@open-mercato')
|
|
122
|
+
await symlink(packagesCore, path.join(nmOpen, 'core'))
|
|
123
|
+
|
|
124
|
+
const env = resolveEnvironment(tempRoot)
|
|
125
|
+
|
|
126
|
+
expect(normalizePath(env.packageRoot('@open-mercato/core'))).toBe(
|
|
127
|
+
normalizePath(path.join(tempRoot, 'packages', 'core')),
|
|
128
|
+
)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('no @open-mercato packages present', () => {
|
|
133
|
+
it('falls back to standalone mode with rootDir = cwd', async () => {
|
|
134
|
+
const env = resolveEnvironment(tempRoot)
|
|
135
|
+
|
|
136
|
+
expect(env.mode).toBe('standalone')
|
|
137
|
+
expect(normalizePath(env.rootDir)).toBe(normalizePath(tempRoot))
|
|
138
|
+
expect(normalizePath(env.appDir)).toBe(normalizePath(tempRoot))
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
})
|
package/src/lib/eject.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
|
+
import { setModuleRegistrationSource } from './modules-config'
|
|
3
4
|
import type { PackageResolver, ModuleEntry } from './resolver'
|
|
5
|
+
import { resolveInstalledOfficialModulePackage } from './module-package'
|
|
4
6
|
|
|
5
7
|
type ModuleMetadata = {
|
|
6
8
|
ejectable?: boolean
|
|
@@ -11,11 +13,15 @@ type ModuleMetadata = {
|
|
|
11
13
|
const SKIP_DIRS = new Set(['__tests__', '__mocks__', 'node_modules'])
|
|
12
14
|
const SOURCE_FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']
|
|
13
15
|
|
|
16
|
+
function shouldSkipEntryName(name: string): boolean {
|
|
17
|
+
return SKIP_DIRS.has(name) || name === '.DS_Store' || name.startsWith('._')
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
function collectSourceFiles(dir: string): string[] {
|
|
15
21
|
const files: string[] = []
|
|
16
22
|
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
17
23
|
for (const entry of entries) {
|
|
18
|
-
if (
|
|
24
|
+
if (shouldSkipEntryName(entry.name)) continue
|
|
19
25
|
const fullPath = path.join(dir, entry.name)
|
|
20
26
|
if (entry.isDirectory()) {
|
|
21
27
|
files.push(...collectSourceFiles(fullPath))
|
|
@@ -159,7 +165,7 @@ export function copyDirRecursive(src: string, dest: string): void {
|
|
|
159
165
|
|
|
160
166
|
const entries = fs.readdirSync(src, { withFileTypes: true })
|
|
161
167
|
for (const entry of entries) {
|
|
162
|
-
if (
|
|
168
|
+
if (shouldSkipEntryName(entry.name)) continue
|
|
163
169
|
|
|
164
170
|
const srcPath = path.join(src, entry.name)
|
|
165
171
|
const destPath = path.join(dest, entry.name)
|
|
@@ -173,58 +179,47 @@ export function copyDirRecursive(src: string, dest: string): void {
|
|
|
173
179
|
}
|
|
174
180
|
|
|
175
181
|
export function updateModulesTs(modulesPath: string, moduleId: string): void {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const source = fs.readFileSync(modulesPath, 'utf8')
|
|
181
|
-
const objectPattern = /\{[^{}]*\}/g
|
|
182
|
-
|
|
183
|
-
let match: RegExpExecArray | null
|
|
184
|
-
let updatedSource: string | null = null
|
|
185
|
-
|
|
186
|
-
while ((match = objectPattern.exec(source)) !== null) {
|
|
187
|
-
const objectLiteral = match[0]
|
|
188
|
-
const idMatch = objectLiteral.match(/\bid\s*:\s*(['"])([^'"]+)\1/)
|
|
189
|
-
if (!idMatch || idMatch[2] !== moduleId) {
|
|
190
|
-
continue
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const updatedObject = upsertModuleSource(objectLiteral)
|
|
194
|
-
updatedSource =
|
|
195
|
-
source.slice(0, match.index) +
|
|
196
|
-
updatedObject +
|
|
197
|
-
source.slice(match.index + objectLiteral.length)
|
|
198
|
-
break
|
|
199
|
-
}
|
|
182
|
+
setModuleRegistrationSource(modulesPath, moduleId, '@app')
|
|
183
|
+
}
|
|
200
184
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
185
|
+
export type EjectableModule = {
|
|
186
|
+
id: string
|
|
187
|
+
title?: string
|
|
188
|
+
description?: string
|
|
189
|
+
from: string
|
|
190
|
+
}
|
|
207
191
|
|
|
208
|
-
|
|
192
|
+
type ResolvedModuleSource = {
|
|
193
|
+
pkgBase: string
|
|
194
|
+
metadata: ModuleMetadata
|
|
209
195
|
}
|
|
210
196
|
|
|
211
|
-
function
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
197
|
+
function resolveModuleSource(
|
|
198
|
+
resolver: PackageResolver,
|
|
199
|
+
entry: ModuleEntry,
|
|
200
|
+
): ResolvedModuleSource {
|
|
201
|
+
const { pkgBase } = resolver.getModulePaths(entry)
|
|
202
|
+
const fallbackMetadata = parseModuleMetadata(path.join(pkgBase, 'index.ts'))
|
|
203
|
+
const from = entry.from || '@open-mercato/core'
|
|
215
204
|
|
|
216
|
-
if (/
|
|
217
|
-
return
|
|
205
|
+
if (from === '@app' || from === '@open-mercato/core') {
|
|
206
|
+
return { pkgBase, metadata: fallbackMetadata }
|
|
218
207
|
}
|
|
219
208
|
|
|
220
|
-
|
|
221
|
-
|
|
209
|
+
try {
|
|
210
|
+
const modulePackage = resolveInstalledOfficialModulePackage(resolver, from, entry.id)
|
|
222
211
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
212
|
+
return {
|
|
213
|
+
pkgBase: modulePackage.sourceModuleDir,
|
|
214
|
+
metadata: {
|
|
215
|
+
ejectable: modulePackage.metadata.ejectable,
|
|
216
|
+
title: modulePackage.moduleInfo.title ?? modulePackage.metadata.moduleId,
|
|
217
|
+
description: modulePackage.moduleInfo.description,
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
return { pkgBase, metadata: fallbackMetadata }
|
|
222
|
+
}
|
|
228
223
|
}
|
|
229
224
|
|
|
230
225
|
export function listEjectableModules(resolver: PackageResolver): EjectableModule[] {
|
|
@@ -234,9 +229,7 @@ export function listEjectableModules(resolver: PackageResolver): EjectableModule
|
|
|
234
229
|
for (const entry of modules) {
|
|
235
230
|
if (entry.from === '@app') continue
|
|
236
231
|
|
|
237
|
-
const {
|
|
238
|
-
const indexPath = path.join(pkgBase, 'index.ts')
|
|
239
|
-
const metadata = parseModuleMetadata(indexPath)
|
|
232
|
+
const { metadata } = resolveModuleSource(resolver, entry)
|
|
240
233
|
|
|
241
234
|
if (metadata.ejectable) {
|
|
242
235
|
ejectable.push({
|
|
@@ -268,7 +261,8 @@ export function ejectModule(resolver: PackageResolver, moduleId: string): void {
|
|
|
268
261
|
)
|
|
269
262
|
}
|
|
270
263
|
|
|
271
|
-
const {
|
|
264
|
+
const { appBase } = resolver.getModulePaths(entry)
|
|
265
|
+
const { pkgBase, metadata } = resolveModuleSource(resolver, entry)
|
|
272
266
|
|
|
273
267
|
if (!fs.existsSync(pkgBase)) {
|
|
274
268
|
throw new Error(
|
|
@@ -277,13 +271,9 @@ export function ejectModule(resolver: PackageResolver, moduleId: string): void {
|
|
|
277
271
|
)
|
|
278
272
|
}
|
|
279
273
|
|
|
280
|
-
const indexPath = path.join(pkgBase, 'index.ts')
|
|
281
|
-
const metadata = parseModuleMetadata(indexPath)
|
|
282
|
-
|
|
283
274
|
if (!metadata.ejectable) {
|
|
284
275
|
throw new Error(
|
|
285
|
-
`Module "${moduleId}" is not marked as ejectable.
|
|
286
|
-
`Only modules with \`ejectable: true\` in their metadata can be ejected.`,
|
|
276
|
+
`Module "${moduleId}" is not marked as ejectable. Only modules with \`ejectable: true\` in their metadata can be ejected.`,
|
|
287
277
|
)
|
|
288
278
|
}
|
|
289
279
|
|
|
@@ -26,6 +26,11 @@ describe('generators', () => {
|
|
|
26
26
|
expect(typeof module.generateModuleDi).toBe('function')
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
+
it('should export generateModulePackageSources', async () => {
|
|
30
|
+
const module = await import('../module-package-sources')
|
|
31
|
+
expect(typeof module.generateModulePackageSources).toBe('function')
|
|
32
|
+
})
|
|
33
|
+
|
|
29
34
|
// Note: api-client uses openapi-typescript which is ESM-only
|
|
30
35
|
// and doesn't work with Jest's CommonJS environment
|
|
31
36
|
it.skip('should export generateApiClient', async () => {
|
|
@@ -172,6 +177,12 @@ describe('generator file output patterns', () => {
|
|
|
172
177
|
expect(expectedPath).toContain('injection-widgets.generated.ts')
|
|
173
178
|
})
|
|
174
179
|
|
|
180
|
+
it('should output module package CSS sources', () => {
|
|
181
|
+
const outputDir = '/project/generated'
|
|
182
|
+
const expectedPath = `${outputDir}/module-package-sources.css`
|
|
183
|
+
expect(expectedPath).toContain('module-package-sources.css')
|
|
184
|
+
})
|
|
185
|
+
|
|
175
186
|
it('should output search config', () => {
|
|
176
187
|
const outputDir = '/project/generated'
|
|
177
188
|
const expectedPath = `${outputDir}/search.generated.ts`
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { generateModulePackageSources } from '../module-package-sources'
|
|
5
|
+
import type { PackageResolver } from '../../resolver'
|
|
6
|
+
|
|
7
|
+
const fixturePackageRoot = path.resolve(
|
|
8
|
+
__dirname,
|
|
9
|
+
'..',
|
|
10
|
+
'..',
|
|
11
|
+
'__fixtures__',
|
|
12
|
+
'official-module-package',
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
function copyDir(src: string, dest: string): void {
|
|
16
|
+
fs.mkdirSync(dest, { recursive: true })
|
|
17
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
18
|
+
const srcPath = path.join(src, entry.name)
|
|
19
|
+
const destPath = path.join(dest, entry.name)
|
|
20
|
+
if (entry.isDirectory()) {
|
|
21
|
+
copyDir(srcPath, destPath)
|
|
22
|
+
} else {
|
|
23
|
+
fs.copyFileSync(srcPath, destPath)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createResolver(tmpDir: string, packageRoot: string, from: string): PackageResolver {
|
|
29
|
+
const appDir = path.join(tmpDir, 'app')
|
|
30
|
+
const outputDir = path.join(appDir, '.mercato', 'generated')
|
|
31
|
+
fs.mkdirSync(outputDir, { recursive: true })
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
isMonorepo: () => false,
|
|
35
|
+
getRootDir: () => appDir,
|
|
36
|
+
getAppDir: () => appDir,
|
|
37
|
+
getOutputDir: () => outputDir,
|
|
38
|
+
getModulesConfigPath: () => path.join(appDir, 'src', 'modules.ts'),
|
|
39
|
+
discoverPackages: () => [],
|
|
40
|
+
loadEnabledModules: () => [{ id: 'test_package', from }],
|
|
41
|
+
getModulePaths: () => ({
|
|
42
|
+
appBase: path.join(appDir, 'src', 'modules', 'test_package'),
|
|
43
|
+
pkgBase: path.join(packageRoot, 'src', 'modules', 'test_package'),
|
|
44
|
+
}),
|
|
45
|
+
getModuleImportBase: () => ({
|
|
46
|
+
appBase: '@/modules/test_package',
|
|
47
|
+
pkgBase: `${from}/modules/test_package`,
|
|
48
|
+
}),
|
|
49
|
+
getPackageOutputDir: () => outputDir,
|
|
50
|
+
getPackageRoot: () => packageRoot,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('generateModulePackageSources', () => {
|
|
55
|
+
let tmpDir: string
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'module-package-sources-test-'))
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('writes @source entries for official package-backed modules', async () => {
|
|
66
|
+
const packageRoot = path.join(tmpDir, 'node_modules', '@open-mercato', 'test-package')
|
|
67
|
+
copyDir(fixturePackageRoot, packageRoot)
|
|
68
|
+
const resolver = createResolver(tmpDir, packageRoot, '@open-mercato/test-package')
|
|
69
|
+
|
|
70
|
+
const result = await generateModulePackageSources({ resolver, quiet: true })
|
|
71
|
+
expect(result.errors).toEqual([])
|
|
72
|
+
|
|
73
|
+
const output = fs.readFileSync(path.join(resolver.getOutputDir(), 'module-package-sources.css'), 'utf8')
|
|
74
|
+
expect(output).toContain('@source')
|
|
75
|
+
expect(output).toContain('node_modules/@open-mercato/test-package/src/**/*.{ts,tsx}')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('skips app-backed modules', async () => {
|
|
79
|
+
const packageRoot = path.join(tmpDir, 'node_modules', '@open-mercato', 'test-package')
|
|
80
|
+
copyDir(fixturePackageRoot, packageRoot)
|
|
81
|
+
const resolver = createResolver(tmpDir, packageRoot, '@app')
|
|
82
|
+
|
|
83
|
+
await generateModulePackageSources({ resolver, quiet: true })
|
|
84
|
+
|
|
85
|
+
const output = fs.readFileSync(path.join(resolver.getOutputDir(), 'module-package-sources.css'), 'utf8')
|
|
86
|
+
expect(output).toBe('')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('resolves hoisted package-backed modules for monorepo apps', async () => {
|
|
90
|
+
const appDir = path.join(tmpDir, 'apps', 'mercato')
|
|
91
|
+
const outputDir = path.join(appDir, '.mercato', 'generated')
|
|
92
|
+
const installedPackageRoot = path.join(tmpDir, 'node_modules', '@open-mercato', 'test-package')
|
|
93
|
+
copyDir(fixturePackageRoot, installedPackageRoot)
|
|
94
|
+
fs.mkdirSync(outputDir, { recursive: true })
|
|
95
|
+
|
|
96
|
+
const resolver = {
|
|
97
|
+
isMonorepo: () => true,
|
|
98
|
+
getRootDir: () => tmpDir,
|
|
99
|
+
getAppDir: () => appDir,
|
|
100
|
+
getOutputDir: () => outputDir,
|
|
101
|
+
getModulesConfigPath: () => path.join(appDir, 'src', 'modules.ts'),
|
|
102
|
+
discoverPackages: () => [],
|
|
103
|
+
loadEnabledModules: () => [{ id: 'test_package', from: '@open-mercato/test-package' }],
|
|
104
|
+
getModulePaths: () => ({
|
|
105
|
+
appBase: path.join(appDir, 'src', 'modules', 'test_package'),
|
|
106
|
+
pkgBase: path.join(installedPackageRoot, 'src', 'modules', 'test_package'),
|
|
107
|
+
}),
|
|
108
|
+
getModuleImportBase: () => ({
|
|
109
|
+
appBase: '@/modules/test_package',
|
|
110
|
+
pkgBase: '@open-mercato/test-package/modules/test_package',
|
|
111
|
+
}),
|
|
112
|
+
getPackageOutputDir: () => outputDir,
|
|
113
|
+
getPackageRoot: () => installedPackageRoot,
|
|
114
|
+
} as PackageResolver
|
|
115
|
+
|
|
116
|
+
await generateModulePackageSources({ resolver, quiet: true })
|
|
117
|
+
|
|
118
|
+
const output = fs.readFileSync(path.join(outputDir, 'module-package-sources.css'), 'utf8')
|
|
119
|
+
expect(output).toContain('node_modules/@open-mercato/test-package/src/**/*.{ts,tsx}')
|
|
120
|
+
})
|
|
121
|
+
})
|
|
@@ -2,4 +2,5 @@ export { generateEntityIds, type EntityIdsOptions } from './entity-ids'
|
|
|
2
2
|
export { generateModuleRegistry, generateModuleRegistryCli, type ModuleRegistryOptions } from './module-registry'
|
|
3
3
|
export { generateModuleEntities, type ModuleEntitiesOptions } from './module-entities'
|
|
4
4
|
export { generateModuleDi, type ModuleDiOptions } from './module-di'
|
|
5
|
+
export { generateModulePackageSources, type ModulePackageSourcesOptions } from './module-package-sources'
|
|
5
6
|
export { generateOpenApi, type GenerateOpenApiOptions } from './openapi'
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { readOfficialModulePackageFromRoot, resolveInstalledPackageRoot } from '../module-package'
|
|
4
|
+
import type { PackageResolver } from '../resolver'
|
|
5
|
+
import { calculateStructureChecksum, createGeneratorResult, type GeneratorResult, writeGeneratedFile } from '../utils'
|
|
6
|
+
|
|
7
|
+
export interface ModulePackageSourcesOptions {
|
|
8
|
+
resolver: PackageResolver
|
|
9
|
+
quiet?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeCssSourcePath(value: string): string {
|
|
13
|
+
const normalized = value.split(path.sep).join('/')
|
|
14
|
+
return normalized.startsWith('.') ? normalized : `./${normalized}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function generateModulePackageSources(
|
|
18
|
+
options: ModulePackageSourcesOptions,
|
|
19
|
+
): Promise<GeneratorResult> {
|
|
20
|
+
const { resolver, quiet } = options
|
|
21
|
+
const result = createGeneratorResult()
|
|
22
|
+
const outputDir = resolver.getOutputDir()
|
|
23
|
+
const outFile = path.join(outputDir, 'module-package-sources.css')
|
|
24
|
+
const checksumFile = path.join(outputDir, 'module-package-sources.checksum')
|
|
25
|
+
const sourcePaths = new Set<string>()
|
|
26
|
+
const checksumTargets: string[] = []
|
|
27
|
+
|
|
28
|
+
for (const entry of resolver.loadEnabledModules()) {
|
|
29
|
+
if (!entry.from || entry.from === '@app' || entry.from === '@open-mercato/core') continue
|
|
30
|
+
|
|
31
|
+
const packageRoot = resolveInstalledPackageRoot(resolver, entry.from)
|
|
32
|
+
checksumTargets.push(packageRoot)
|
|
33
|
+
|
|
34
|
+
let modulePackage
|
|
35
|
+
try {
|
|
36
|
+
modulePackage = readOfficialModulePackageFromRoot(packageRoot, entry.from, entry.id)
|
|
37
|
+
} catch {
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const packageSourceRoot = path.join(modulePackage.packageRoot, 'src')
|
|
42
|
+
const relativeSourcePath = normalizeCssSourcePath(path.relative(path.dirname(outFile), packageSourceRoot))
|
|
43
|
+
sourcePaths.add(`@source "${relativeSourcePath}/**/*.{ts,tsx}";`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const content = `${Array.from(sourcePaths).sort((left, right) => left.localeCompare(right)).join('\n')}${sourcePaths.size > 0 ? '\n' : ''}`
|
|
47
|
+
const structureChecksum = calculateStructureChecksum(checksumTargets)
|
|
48
|
+
|
|
49
|
+
writeGeneratedFile({
|
|
50
|
+
outFile,
|
|
51
|
+
checksumFile,
|
|
52
|
+
content,
|
|
53
|
+
structureChecksum,
|
|
54
|
+
result,
|
|
55
|
+
quiet,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
return result
|
|
59
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type ParsedModuleInstallArgs = {
|
|
2
|
+
packageSpec: string | null
|
|
3
|
+
eject: boolean
|
|
4
|
+
moduleId: string | null
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function parseModuleInstallArgs(args: string[]): ParsedModuleInstallArgs {
|
|
8
|
+
let packageSpec: string | null = null
|
|
9
|
+
let eject = false
|
|
10
|
+
let moduleId: string | null = null
|
|
11
|
+
|
|
12
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
13
|
+
const arg = args[index]
|
|
14
|
+
if (!arg) continue
|
|
15
|
+
|
|
16
|
+
if (arg === '--eject') {
|
|
17
|
+
eject = true
|
|
18
|
+
continue
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (arg.startsWith('--eject=')) {
|
|
22
|
+
throw new Error('--eject does not accept a value')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (arg === '--module') {
|
|
26
|
+
const next = args[index + 1]
|
|
27
|
+
if (next && !next.startsWith('-')) {
|
|
28
|
+
moduleId = next
|
|
29
|
+
index += 1
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`--module requires a moduleId value`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (arg.startsWith('--module=')) {
|
|
36
|
+
moduleId = arg.slice('--module='.length) || null
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (arg.startsWith('-')) {
|
|
41
|
+
throw new Error(`Unsupported option: ${arg}`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!arg.startsWith('-') && !packageSpec) {
|
|
45
|
+
packageSpec = arg
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { packageSpec, eject, moduleId }
|
|
50
|
+
}
|