@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
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import os from 'node:os'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
parseModuleMetadata,
|
|
6
|
+
copyDirRecursive,
|
|
7
|
+
rewriteCrossModuleImports,
|
|
8
|
+
updateModulesTs,
|
|
9
|
+
listEjectableModules,
|
|
10
|
+
ejectModule,
|
|
11
|
+
} from '../eject'
|
|
12
|
+
import type { PackageResolver } from '../resolver'
|
|
5
13
|
|
|
6
14
|
describe('eject', () => {
|
|
7
15
|
let tmpDir: string
|
|
@@ -84,6 +92,23 @@ describe('eject', () => {
|
|
|
84
92
|
expect(fs.existsSync(path.join(dest, 'node_modules'))).toBe(false)
|
|
85
93
|
expect(fs.existsSync(path.join(dest, 'keep.ts'))).toBe(true)
|
|
86
94
|
})
|
|
95
|
+
|
|
96
|
+
it('skips AppleDouble and Finder metadata files', () => {
|
|
97
|
+
const src = path.join(tmpDir, 'src')
|
|
98
|
+
const dest = path.join(tmpDir, 'dest')
|
|
99
|
+
fs.mkdirSync(path.join(src, 'nested'), { recursive: true })
|
|
100
|
+
fs.writeFileSync(path.join(src, '._setup.ts'), 'junk')
|
|
101
|
+
fs.writeFileSync(path.join(src, '.DS_Store'), 'junk')
|
|
102
|
+
fs.writeFileSync(path.join(src, 'nested', '._page.tsx'), 'junk')
|
|
103
|
+
fs.writeFileSync(path.join(src, 'setup.ts'), 'export const setup = true')
|
|
104
|
+
|
|
105
|
+
copyDirRecursive(src, dest)
|
|
106
|
+
|
|
107
|
+
expect(fs.existsSync(path.join(dest, '._setup.ts'))).toBe(false)
|
|
108
|
+
expect(fs.existsSync(path.join(dest, '.DS_Store'))).toBe(false)
|
|
109
|
+
expect(fs.existsSync(path.join(dest, 'nested', '._page.tsx'))).toBe(false)
|
|
110
|
+
expect(fs.existsSync(path.join(dest, 'setup.ts'))).toBe(true)
|
|
111
|
+
})
|
|
87
112
|
})
|
|
88
113
|
|
|
89
114
|
describe('updateModulesTs', () => {
|
|
@@ -256,4 +281,85 @@ describe('eject', () => {
|
|
|
256
281
|
expect(rewritten).toContain("import('../../unknown/missing')")
|
|
257
282
|
})
|
|
258
283
|
})
|
|
284
|
+
|
|
285
|
+
describe('official module packages', () => {
|
|
286
|
+
function createOfficialModuleResolver(baseDir: string): PackageResolver {
|
|
287
|
+
const appDir = path.join(baseDir, 'apps', 'mercato')
|
|
288
|
+
const modulesTsPath = path.join(appDir, 'src', 'modules.ts')
|
|
289
|
+
const installedPackageRoot = path.join(baseDir, 'node_modules', '@open-mercato', 'test-package')
|
|
290
|
+
|
|
291
|
+
fs.mkdirSync(path.join(appDir, 'src'), { recursive: true })
|
|
292
|
+
fs.mkdirSync(path.join(installedPackageRoot, 'src', 'modules', 'test_package'), { recursive: true })
|
|
293
|
+
fs.mkdirSync(path.join(installedPackageRoot, 'dist', 'modules', 'test_package'), { recursive: true })
|
|
294
|
+
|
|
295
|
+
fs.writeFileSync(
|
|
296
|
+
path.join(appDir, 'src', 'modules.ts'),
|
|
297
|
+
"export const enabledModules = [{ id: 'test_package', from: '@open-mercato/test-package' }]\n",
|
|
298
|
+
)
|
|
299
|
+
fs.writeFileSync(
|
|
300
|
+
path.join(installedPackageRoot, 'package.json'),
|
|
301
|
+
JSON.stringify(
|
|
302
|
+
{
|
|
303
|
+
name: '@open-mercato/test-package',
|
|
304
|
+
version: '0.1.0',
|
|
305
|
+
peerDependencies: {
|
|
306
|
+
'@open-mercato/core': '>=0.4.7 <0.5.0',
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
null,
|
|
310
|
+
2,
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
fs.writeFileSync(
|
|
314
|
+
path.join(installedPackageRoot, 'src', 'modules', 'test_package', 'index.ts'),
|
|
315
|
+
"export const metadata = { title: 'Test Package', description: 'Fixture package', ejectable: true }\n",
|
|
316
|
+
)
|
|
317
|
+
fs.writeFileSync(
|
|
318
|
+
path.join(installedPackageRoot, 'src', 'modules', 'test_package', 'widget.ts'),
|
|
319
|
+
'export const widget = true\n',
|
|
320
|
+
)
|
|
321
|
+
fs.writeFileSync(
|
|
322
|
+
path.join(installedPackageRoot, 'dist', 'modules', 'test_package', 'index.js'),
|
|
323
|
+
'export const metadata = {};\n',
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
getAppDir: () => appDir,
|
|
328
|
+
getModulesConfigPath: () => modulesTsPath,
|
|
329
|
+
loadEnabledModules: () => [{ id: 'test_package', from: '@open-mercato/test-package' }],
|
|
330
|
+
getModulePaths: () => ({
|
|
331
|
+
appBase: path.join(appDir, 'src', 'modules', 'test_package'),
|
|
332
|
+
pkgBase: path.join(baseDir, 'packages', 'test-package', 'src', 'modules', 'test_package'),
|
|
333
|
+
}),
|
|
334
|
+
getPackageRoot: () => path.join(baseDir, 'packages', 'test-package'),
|
|
335
|
+
} as unknown as PackageResolver
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
it('lists module packages as ejectable from module index.ts metadata', () => {
|
|
339
|
+
const resolver = createOfficialModuleResolver(tmpDir)
|
|
340
|
+
|
|
341
|
+
expect(listEjectableModules(resolver)).toEqual([
|
|
342
|
+
{
|
|
343
|
+
id: 'test_package',
|
|
344
|
+
title: 'Test Package',
|
|
345
|
+
description: 'Fixture package',
|
|
346
|
+
from: '@open-mercato/test-package',
|
|
347
|
+
},
|
|
348
|
+
])
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('ejects module packages using module index.ts ejectable flag', () => {
|
|
352
|
+
const resolver = createOfficialModuleResolver(tmpDir)
|
|
353
|
+
const appModuleDir = path.join(tmpDir, 'apps', 'mercato', 'src', 'modules', 'test_package')
|
|
354
|
+
const modulesTsPath = path.join(tmpDir, 'apps', 'mercato', 'src', 'modules.ts')
|
|
355
|
+
|
|
356
|
+
ejectModule(resolver, 'test_package')
|
|
357
|
+
|
|
358
|
+
expect(fs.existsSync(path.join(appModuleDir, 'index.ts'))).toBe(true)
|
|
359
|
+
expect(fs.existsSync(path.join(appModuleDir, 'widget.ts'))).toBe(true)
|
|
360
|
+
expect(fs.readFileSync(modulesTsPath, 'utf8')).toContain(
|
|
361
|
+
"{ id: 'test_package', from: '@app' }",
|
|
362
|
+
)
|
|
363
|
+
})
|
|
364
|
+
})
|
|
259
365
|
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { parseModuleInstallArgs } from '../module-install-args'
|
|
2
|
+
|
|
3
|
+
describe('parseModuleInstallArgs', () => {
|
|
4
|
+
it('defaults to package-backed install without ejecting source', () => {
|
|
5
|
+
expect(parseModuleInstallArgs(['@open-mercato/test-package'])).toMatchObject({
|
|
6
|
+
packageSpec: '@open-mercato/test-package',
|
|
7
|
+
eject: false,
|
|
8
|
+
})
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('accepts --eject as a boolean flag', () => {
|
|
12
|
+
expect(parseModuleInstallArgs(['@open-mercato/test-package', '--eject'])).toMatchObject({
|
|
13
|
+
eject: true,
|
|
14
|
+
})
|
|
15
|
+
expect(parseModuleInstallArgs(['--eject', '@open-mercato/test-package'])).toMatchObject({
|
|
16
|
+
packageSpec: '@open-mercato/test-package',
|
|
17
|
+
eject: true,
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('rejects --eject values', () => {
|
|
22
|
+
expect(() => parseModuleInstallArgs(['@open-mercato/test-package', '--eject=true'])).toThrow(
|
|
23
|
+
'--eject does not accept a value',
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('rejects unsupported options', () => {
|
|
28
|
+
expect(() => parseModuleInstallArgs(['@open-mercato/test-package', '--unknown'])).toThrow(
|
|
29
|
+
'Unsupported option: --unknown',
|
|
30
|
+
)
|
|
31
|
+
expect(() => parseModuleInstallArgs(['@open-mercato/test-package', '--installed'])).toThrow(
|
|
32
|
+
'Unsupported option: --installed',
|
|
33
|
+
)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import type { PackageResolver } from '../resolver'
|
|
5
|
+
import { enableOfficialModule } from '../module-install'
|
|
6
|
+
|
|
7
|
+
function buildPackageFixture(
|
|
8
|
+
packageRoot: string,
|
|
9
|
+
moduleId: string,
|
|
10
|
+
options?: {
|
|
11
|
+
ejectable?: boolean
|
|
12
|
+
extraSourceFiles?: Array<{ relativePath: string; content: string }>
|
|
13
|
+
},
|
|
14
|
+
): void {
|
|
15
|
+
const ejectable = options?.ejectable ?? false
|
|
16
|
+
for (const base of ['src', 'dist']) {
|
|
17
|
+
fs.mkdirSync(path.join(packageRoot, base, 'modules', moduleId), { recursive: true })
|
|
18
|
+
}
|
|
19
|
+
fs.writeFileSync(
|
|
20
|
+
path.join(packageRoot, 'package.json'),
|
|
21
|
+
JSON.stringify({ name: '@open-mercato/test-package', version: '0.1.0' }),
|
|
22
|
+
)
|
|
23
|
+
fs.writeFileSync(
|
|
24
|
+
path.join(packageRoot, 'src', 'modules', moduleId, 'index.ts'),
|
|
25
|
+
`export const metadata = { title: 'Test', ejectable: ${ejectable ? 'true' : 'false'} }\n`,
|
|
26
|
+
)
|
|
27
|
+
fs.writeFileSync(
|
|
28
|
+
path.join(packageRoot, 'dist', 'modules', moduleId, 'index.js'),
|
|
29
|
+
`exports.metadata = {};\n`,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
for (const file of options?.extraSourceFiles ?? []) {
|
|
33
|
+
const targetPath = path.join(packageRoot, 'src', 'modules', moduleId, file.relativePath)
|
|
34
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true })
|
|
35
|
+
fs.writeFileSync(targetPath, file.content)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildResolver(
|
|
40
|
+
appDir: string,
|
|
41
|
+
packageRoot: string,
|
|
42
|
+
modulesTsContent: string,
|
|
43
|
+
): PackageResolver {
|
|
44
|
+
const modulesTsPath = path.join(appDir, 'src', 'modules.ts')
|
|
45
|
+
fs.mkdirSync(path.join(appDir, 'src'), { recursive: true })
|
|
46
|
+
fs.writeFileSync(modulesTsPath, modulesTsContent)
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
getAppDir: () => appDir,
|
|
50
|
+
getModulesConfigPath: () => modulesTsPath,
|
|
51
|
+
getPackageRoot: () => packageRoot,
|
|
52
|
+
isMonorepo: () => false,
|
|
53
|
+
getRootDir: () => appDir,
|
|
54
|
+
loadEnabledModules: () => [],
|
|
55
|
+
getModulePaths: () => ({ appBase: '', pkgBase: '' }),
|
|
56
|
+
getOutputDir: () => path.join(appDir, '.mercato', 'generated'),
|
|
57
|
+
} as unknown as PackageResolver
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('enableOfficialModule', () => {
|
|
61
|
+
let tmpDir: string
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'module-install-test-'))
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('throws when module is already enabled in modules.ts with the same source', async () => {
|
|
72
|
+
const packageRoot = path.join(tmpDir, 'node_modules', '@open-mercato', 'test-package')
|
|
73
|
+
const appDir = path.join(tmpDir, 'app')
|
|
74
|
+
buildPackageFixture(packageRoot, 'test_package')
|
|
75
|
+
|
|
76
|
+
const resolver = buildResolver(
|
|
77
|
+
appDir,
|
|
78
|
+
packageRoot,
|
|
79
|
+
"export const enabledModules = [{ id: 'test_package', from: '@open-mercato/test-package' }]\n",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
await expect(
|
|
83
|
+
enableOfficialModule(resolver, '@open-mercato/test-package'),
|
|
84
|
+
).rejects.toThrow('already enabled in modules.ts')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('throws when module is already enabled with different casing in the source field', async () => {
|
|
88
|
+
const packageRoot = path.join(tmpDir, 'node_modules', '@open-mercato', 'test-package')
|
|
89
|
+
const appDir = path.join(tmpDir, 'app')
|
|
90
|
+
buildPackageFixture(packageRoot, 'test_package')
|
|
91
|
+
|
|
92
|
+
// registered without explicit from (defaults to @open-mercato/core internally) — different from what enable would write
|
|
93
|
+
// This triggers the "already registered from different source" error from ensureModuleRegistration
|
|
94
|
+
const resolver = buildResolver(
|
|
95
|
+
appDir,
|
|
96
|
+
packageRoot,
|
|
97
|
+
"export const enabledModules = [{ id: 'test_package', from: '@open-mercato/other' }]\n",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
await expect(
|
|
101
|
+
enableOfficialModule(resolver, '@open-mercato/test-package'),
|
|
102
|
+
).rejects.toThrow('already registered from')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('error message includes the module id and package name', async () => {
|
|
106
|
+
const packageRoot = path.join(tmpDir, 'node_modules', '@open-mercato', 'test-package')
|
|
107
|
+
const appDir = path.join(tmpDir, 'app')
|
|
108
|
+
buildPackageFixture(packageRoot, 'test_package')
|
|
109
|
+
|
|
110
|
+
const resolver = buildResolver(
|
|
111
|
+
appDir,
|
|
112
|
+
packageRoot,
|
|
113
|
+
"export const enabledModules = [{ id: 'test_package', from: '@open-mercato/test-package' }]\n",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
await expect(
|
|
117
|
+
enableOfficialModule(resolver, '@open-mercato/test-package'),
|
|
118
|
+
).rejects.toThrow('"test_package"')
|
|
119
|
+
|
|
120
|
+
await expect(
|
|
121
|
+
enableOfficialModule(resolver, '@open-mercato/test-package'),
|
|
122
|
+
).rejects.toThrow('"@open-mercato/test-package"')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('throws when the specific module is already enabled in a multi-module package', async () => {
|
|
126
|
+
const packageRoot = path.join(tmpDir, 'node_modules', '@open-mercato', 'multi')
|
|
127
|
+
const appDir = path.join(tmpDir, 'app')
|
|
128
|
+
|
|
129
|
+
// package with two modules
|
|
130
|
+
for (const moduleId of ['alpha', 'beta']) {
|
|
131
|
+
fs.mkdirSync(path.join(packageRoot, 'src', 'modules', moduleId), { recursive: true })
|
|
132
|
+
fs.mkdirSync(path.join(packageRoot, 'dist', 'modules', moduleId), { recursive: true })
|
|
133
|
+
fs.writeFileSync(
|
|
134
|
+
path.join(packageRoot, 'src', 'modules', moduleId, 'index.ts'),
|
|
135
|
+
`export const metadata = { ejectable: false }\n`,
|
|
136
|
+
)
|
|
137
|
+
fs.writeFileSync(
|
|
138
|
+
path.join(packageRoot, 'dist', 'modules', moduleId, 'index.js'),
|
|
139
|
+
`exports.metadata = {};\n`,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
fs.writeFileSync(
|
|
143
|
+
path.join(packageRoot, 'package.json'),
|
|
144
|
+
JSON.stringify({ name: '@open-mercato/multi', version: '0.1.0' }),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
// alpha is already enabled, beta is not
|
|
148
|
+
const resolver = buildResolver(
|
|
149
|
+
appDir,
|
|
150
|
+
packageRoot,
|
|
151
|
+
"export const enabledModules = [{ id: 'alpha', from: '@open-mercato/multi' }]\n",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
await expect(
|
|
155
|
+
enableOfficialModule(resolver, '@open-mercato/multi', 'alpha'),
|
|
156
|
+
).rejects.toThrow('already enabled in modules.ts')
|
|
157
|
+
|
|
158
|
+
// beta is not yet enabled — must not throw "already enabled", even if generators fail afterwards
|
|
159
|
+
try {
|
|
160
|
+
await enableOfficialModule(resolver, '@open-mercato/multi', 'beta')
|
|
161
|
+
} catch (error) {
|
|
162
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
163
|
+
expect(message).not.toContain('already enabled in modules.ts')
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('copies module source into the app when enabling with --eject', async () => {
|
|
168
|
+
const packageRoot = path.join(tmpDir, 'node_modules', '@open-mercato', 'test-package')
|
|
169
|
+
const appDir = path.join(tmpDir, 'app')
|
|
170
|
+
buildPackageFixture(packageRoot, 'test_package', {
|
|
171
|
+
ejectable: true,
|
|
172
|
+
extraSourceFiles: [
|
|
173
|
+
{
|
|
174
|
+
relativePath: path.join('backend', 'page.tsx'),
|
|
175
|
+
content: 'export default function TestPage() { return null }\n',
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const resolver = buildResolver(
|
|
181
|
+
appDir,
|
|
182
|
+
packageRoot,
|
|
183
|
+
"export const enabledModules = []\n",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
await expect(
|
|
187
|
+
enableOfficialModule(
|
|
188
|
+
resolver,
|
|
189
|
+
'@open-mercato/test-package',
|
|
190
|
+
undefined,
|
|
191
|
+
true,
|
|
192
|
+
),
|
|
193
|
+
).rejects.toThrow('generated artifacts are stale')
|
|
194
|
+
|
|
195
|
+
expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'test_package', 'index.ts'))).toBe(true)
|
|
196
|
+
expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'test_package', 'backend', 'page.tsx'))).toBe(true)
|
|
197
|
+
expect(fs.readFileSync(path.join(appDir, 'src', 'modules.ts'), 'utf8')).toContain(
|
|
198
|
+
"{ id: 'test_package', from: '@app' }",
|
|
199
|
+
)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('rejects enabling with --eject when the package is not ejectable', async () => {
|
|
203
|
+
const packageRoot = path.join(tmpDir, 'node_modules', '@open-mercato', 'test-package')
|
|
204
|
+
const appDir = path.join(tmpDir, 'app')
|
|
205
|
+
buildPackageFixture(packageRoot, 'test_package')
|
|
206
|
+
|
|
207
|
+
const resolver = buildResolver(
|
|
208
|
+
appDir,
|
|
209
|
+
packageRoot,
|
|
210
|
+
"export const enabledModules = []\n",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
await expect(
|
|
214
|
+
enableOfficialModule(resolver, '@open-mercato/test-package', undefined, true),
|
|
215
|
+
).rejects.toThrow('--eject requires open-mercato.ejectable === true')
|
|
216
|
+
})
|
|
217
|
+
})
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import type { PackageResolver } from '../resolver'
|
|
5
|
+
import {
|
|
6
|
+
discoverModulesInPackage,
|
|
7
|
+
parsePackageNameFromSpec,
|
|
8
|
+
readOfficialModulePackageFromRoot,
|
|
9
|
+
resolveInstalledOfficialModulePackage,
|
|
10
|
+
validateEjectBoundaries,
|
|
11
|
+
} from '../module-package'
|
|
12
|
+
|
|
13
|
+
const fixturePackageRoot = path.resolve(
|
|
14
|
+
__dirname,
|
|
15
|
+
'..',
|
|
16
|
+
'__fixtures__',
|
|
17
|
+
'official-module-package',
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
function copyDir(src: string, dest: string): void {
|
|
21
|
+
fs.mkdirSync(dest, { recursive: true })
|
|
22
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
23
|
+
const srcPath = path.join(src, entry.name)
|
|
24
|
+
const destPath = path.join(dest, entry.name)
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
copyDir(srcPath, destPath)
|
|
27
|
+
} else {
|
|
28
|
+
fs.copyFileSync(srcPath, destPath)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('module-package', () => {
|
|
34
|
+
let tmpDir: string
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'module-package-test-'))
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('parses official package names from package specs', () => {
|
|
45
|
+
expect(parsePackageNameFromSpec('@open-mercato/test-package')).toBe('@open-mercato/test-package')
|
|
46
|
+
expect(parsePackageNameFromSpec('@open-mercato/test-package@preview')).toBe('@open-mercato/test-package')
|
|
47
|
+
expect(parsePackageNameFromSpec('@open-mercato/test-package@file:/tmp/pkg')).toBe('@open-mercato/test-package')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('reads and validates an official module package manifest', () => {
|
|
51
|
+
const modulePackage = readOfficialModulePackageFromRoot(fixturePackageRoot, '@open-mercato/test-package')
|
|
52
|
+
|
|
53
|
+
expect(modulePackage.packageName).toBe('@open-mercato/test-package')
|
|
54
|
+
expect(modulePackage.metadata.moduleId).toBe('test_package')
|
|
55
|
+
expect(modulePackage.metadata.ejectable).toBe(true)
|
|
56
|
+
expect(modulePackage.sourceModuleDir).toContain(path.join('src', 'modules', 'test_package'))
|
|
57
|
+
expect(modulePackage.distModuleDir).toContain(path.join('dist', 'modules', 'test_package'))
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('resolves a hoisted installed module package from a monorepo app', () => {
|
|
61
|
+
const appDir = path.join(tmpDir, 'apps', 'mercato')
|
|
62
|
+
const installedPackageRoot = path.join(tmpDir, 'node_modules', '@open-mercato', 'test-package')
|
|
63
|
+
fs.mkdirSync(appDir, { recursive: true })
|
|
64
|
+
copyDir(fixturePackageRoot, installedPackageRoot)
|
|
65
|
+
|
|
66
|
+
const resolver = {
|
|
67
|
+
getAppDir: () => appDir,
|
|
68
|
+
getPackageRoot: () => installedPackageRoot,
|
|
69
|
+
} as unknown as PackageResolver
|
|
70
|
+
|
|
71
|
+
const modulePackage = resolveInstalledOfficialModulePackage(
|
|
72
|
+
resolver,
|
|
73
|
+
'@open-mercato/test-package',
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
expect(fs.realpathSync(modulePackage.packageRoot)).toBe(fs.realpathSync(installedPackageRoot))
|
|
77
|
+
expect(modulePackage.metadata.moduleId).toBe('test_package')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('discovers all modules in a multi-module package', () => {
|
|
81
|
+
const packageRoot = path.join(tmpDir, 'multi-module-package')
|
|
82
|
+
fs.mkdirSync(path.join(packageRoot, 'src', 'modules', 'alpha'), { recursive: true })
|
|
83
|
+
fs.mkdirSync(path.join(packageRoot, 'src', 'modules', 'beta'), { recursive: true })
|
|
84
|
+
fs.writeFileSync(
|
|
85
|
+
path.join(packageRoot, 'src', 'modules', 'alpha', 'index.ts'),
|
|
86
|
+
"export const metadata = { ejectable: true }\n",
|
|
87
|
+
)
|
|
88
|
+
fs.writeFileSync(
|
|
89
|
+
path.join(packageRoot, 'src', 'modules', 'beta', 'index.ts'),
|
|
90
|
+
"export const metadata = { ejectable: false }\n",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const modules = discoverModulesInPackage(packageRoot)
|
|
94
|
+
expect(modules).toHaveLength(2)
|
|
95
|
+
expect(modules.find((m) => m.moduleId === 'alpha')?.ejectable).toBe(true)
|
|
96
|
+
expect(modules.find((m) => m.moduleId === 'beta')?.ejectable).toBe(false)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('selects a specific module from a multi-module package via targetModuleId', () => {
|
|
100
|
+
const packageRoot = path.join(tmpDir, 'multi-module-pkg')
|
|
101
|
+
for (const moduleId of ['customers', 'sales']) {
|
|
102
|
+
fs.mkdirSync(path.join(packageRoot, 'src', 'modules', moduleId), { recursive: true })
|
|
103
|
+
fs.mkdirSync(path.join(packageRoot, 'dist', 'modules', moduleId), { recursive: true })
|
|
104
|
+
fs.writeFileSync(
|
|
105
|
+
path.join(packageRoot, 'src', 'modules', moduleId, 'index.ts'),
|
|
106
|
+
`export const metadata = { ejectable: false }\n`,
|
|
107
|
+
)
|
|
108
|
+
fs.writeFileSync(
|
|
109
|
+
path.join(packageRoot, 'dist', 'modules', moduleId, 'index.js'),
|
|
110
|
+
`exports.metadata = {};\n`,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
fs.writeFileSync(
|
|
114
|
+
path.join(packageRoot, 'package.json'),
|
|
115
|
+
JSON.stringify({ name: '@open-mercato/core' }),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const result = readOfficialModulePackageFromRoot(packageRoot, undefined, 'sales')
|
|
119
|
+
expect(result.metadata.moduleId).toBe('sales')
|
|
120
|
+
expect(result.sourceModuleDir).toContain(path.join('modules', 'sales'))
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('throws when targetModuleId is not found in package', () => {
|
|
124
|
+
const packageRoot = path.join(tmpDir, 'single-module-pkg')
|
|
125
|
+
fs.mkdirSync(path.join(packageRoot, 'src', 'modules', 'customers'), { recursive: true })
|
|
126
|
+
fs.mkdirSync(path.join(packageRoot, 'dist', 'modules', 'customers'), { recursive: true })
|
|
127
|
+
fs.writeFileSync(
|
|
128
|
+
path.join(packageRoot, 'src', 'modules', 'customers', 'index.ts'),
|
|
129
|
+
`export const metadata = { ejectable: false }\n`,
|
|
130
|
+
)
|
|
131
|
+
fs.writeFileSync(
|
|
132
|
+
path.join(packageRoot, 'dist', 'modules', 'customers', 'index.js'),
|
|
133
|
+
`exports.metadata = {};\n`,
|
|
134
|
+
)
|
|
135
|
+
fs.writeFileSync(
|
|
136
|
+
path.join(packageRoot, 'package.json'),
|
|
137
|
+
JSON.stringify({ name: '@open-mercato/core' }),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
expect(() =>
|
|
141
|
+
readOfficialModulePackageFromRoot(packageRoot, undefined, 'nonexistent'),
|
|
142
|
+
).toThrow('does not contain module "nonexistent"')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('throws when a multi-module package is used without specifying a module', () => {
|
|
146
|
+
const packageRoot = path.join(tmpDir, 'ambiguous-pkg')
|
|
147
|
+
for (const moduleId of ['alpha', 'beta']) {
|
|
148
|
+
fs.mkdirSync(path.join(packageRoot, 'src', 'modules', moduleId), { recursive: true })
|
|
149
|
+
fs.mkdirSync(path.join(packageRoot, 'dist', 'modules', moduleId), { recursive: true })
|
|
150
|
+
fs.writeFileSync(
|
|
151
|
+
path.join(packageRoot, 'src', 'modules', moduleId, 'index.ts'),
|
|
152
|
+
`export const metadata = {}\n`,
|
|
153
|
+
)
|
|
154
|
+
fs.writeFileSync(
|
|
155
|
+
path.join(packageRoot, 'dist', 'modules', moduleId, 'index.js'),
|
|
156
|
+
`exports.metadata = {};\n`,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
fs.writeFileSync(
|
|
160
|
+
path.join(packageRoot, 'package.json'),
|
|
161
|
+
JSON.stringify({ name: '@open-mercato/multi' }),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
expect(() =>
|
|
165
|
+
readOfficialModulePackageFromRoot(packageRoot),
|
|
166
|
+
).toThrow('contains multiple modules')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('rejects --eject when a module imports files outside its module directory', () => {
|
|
170
|
+
const invalidPackageRoot = path.join(tmpDir, 'invalid-package')
|
|
171
|
+
copyDir(fixturePackageRoot, invalidPackageRoot)
|
|
172
|
+
|
|
173
|
+
fs.mkdirSync(path.join(invalidPackageRoot, 'src', 'lib'), { recursive: true })
|
|
174
|
+
fs.writeFileSync(
|
|
175
|
+
path.join(invalidPackageRoot, 'src', 'lib', 'shared.ts'),
|
|
176
|
+
'export const sharedValue = 1\n',
|
|
177
|
+
)
|
|
178
|
+
fs.mkdirSync(path.join(invalidPackageRoot, 'src', 'modules', 'test_package', 'lib'), {
|
|
179
|
+
recursive: true,
|
|
180
|
+
})
|
|
181
|
+
fs.writeFileSync(
|
|
182
|
+
path.join(invalidPackageRoot, 'src', 'modules', 'test_package', 'lib', 'broken.ts'),
|
|
183
|
+
"import { sharedValue } from '../../../lib/shared'\nexport const brokenValue = sharedValue\n",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
expect(() =>
|
|
187
|
+
validateEjectBoundaries(
|
|
188
|
+
readOfficialModulePackageFromRoot(invalidPackageRoot, '@open-mercato/test-package'),
|
|
189
|
+
),
|
|
190
|
+
).toThrow('cannot be added with --eject')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('ignores AppleDouble metadata files when validating --eject boundaries', () => {
|
|
194
|
+
const packageRoot = path.join(tmpDir, 'package-with-sidecars')
|
|
195
|
+
copyDir(fixturePackageRoot, packageRoot)
|
|
196
|
+
|
|
197
|
+
fs.writeFileSync(
|
|
198
|
+
path.join(packageRoot, 'src', 'modules', 'test_package', '._setup.ts'),
|
|
199
|
+
"import { sharedValue } from '../../../lib/shared'\n",
|
|
200
|
+
)
|
|
201
|
+
fs.mkdirSync(path.join(packageRoot, 'src', 'modules', 'test_package', 'backend', 'test-packag'), {
|
|
202
|
+
recursive: true,
|
|
203
|
+
})
|
|
204
|
+
fs.writeFileSync(
|
|
205
|
+
path.join(packageRoot, 'src', 'modules', 'test_package', 'backend', 'test-packag', '._page.tsx'),
|
|
206
|
+
"import { sharedValue } from '../../../../lib/shared'\n",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
expect(() =>
|
|
210
|
+
validateEjectBoundaries(
|
|
211
|
+
readOfficialModulePackageFromRoot(packageRoot, '@open-mercato/test-package'),
|
|
212
|
+
),
|
|
213
|
+
).not.toThrow()
|
|
214
|
+
})
|
|
215
|
+
})
|