@open-mercato/cli 0.5.1-develop.3032.01699048cb → 0.5.1-develop.3043.1a796c3920
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/agentic/shared/AGENTS.md.template +1 -1
- package/dist/lib/__integration__/TC-INT-007.spec.js +201 -0
- package/dist/lib/__integration__/TC-INT-007.spec.js.map +7 -0
- package/dist/lib/dev-env-reload.js +89 -0
- package/dist/lib/dev-env-reload.js.map +7 -0
- package/dist/lib/generators/extensions/ai-agents.js +218 -0
- package/dist/lib/generators/extensions/ai-agents.js.map +7 -0
- package/dist/lib/generators/extensions/ai-tools.js +56 -1
- package/dist/lib/generators/extensions/ai-tools.js.map +2 -2
- package/dist/lib/generators/extensions/index.js +2 -0
- package/dist/lib/generators/extensions/index.js.map +2 -2
- package/dist/lib/testing/integration-discovery.js +102 -5
- package/dist/lib/testing/integration-discovery.js.map +2 -2
- package/dist/mercato.js +153 -79
- package/dist/mercato.js.map +2 -2
- package/package.json +5 -5
- package/src/__tests__/mercato.test.ts +301 -25
- package/src/lib/__integration__/TC-INT-007.spec.ts +228 -0
- package/src/lib/__tests__/dev-env-reload.test.ts +62 -0
- package/src/lib/dev-env-reload.ts +110 -0
- package/src/lib/generators/__tests__/module-subset.test.ts +14 -0
- package/src/lib/generators/__tests__/output-snapshots.test.ts +17 -0
- package/src/lib/generators/__tests__/scanner.test.ts +1 -1
- package/src/lib/generators/__tests__/structural-contracts.test.ts +26 -0
- package/src/lib/generators/extensions/ai-agents.ts +240 -0
- package/src/lib/generators/extensions/ai-tools.ts +72 -1
- package/src/lib/generators/extensions/index.ts +2 -0
- package/src/lib/testing/__tests__/integration-discovery.test.ts +68 -0
- package/src/lib/testing/integration-discovery.ts +127 -3
- package/src/mercato.ts +190 -83
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cli",
|
|
3
|
-
"version": "0.5.1-develop.
|
|
3
|
+
"version": "0.5.1-develop.3043.1a796c3920",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"@mikro-orm/decorators": "^7.0.13",
|
|
60
60
|
"@mikro-orm/migrations": "^7.0.13",
|
|
61
61
|
"@mikro-orm/postgresql": "^7.0.13",
|
|
62
|
-
"@open-mercato/queue": "0.5.1-develop.
|
|
63
|
-
"@open-mercato/shared": "0.5.1-develop.
|
|
62
|
+
"@open-mercato/queue": "0.5.1-develop.3043.1a796c3920",
|
|
63
|
+
"@open-mercato/shared": "0.5.1-develop.3043.1a796c3920",
|
|
64
64
|
"cross-spawn": "^7.0.6",
|
|
65
65
|
"pg": "8.20.0",
|
|
66
66
|
"semver": "^7.7.4",
|
|
@@ -70,10 +70,10 @@
|
|
|
70
70
|
"typescript": "^5.9.3"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"@open-mercato/shared": "0.5.1-develop.
|
|
73
|
+
"@open-mercato/shared": "0.5.1-develop.3043.1a796c3920"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@open-mercato/shared": "0.5.1-develop.
|
|
76
|
+
"@open-mercato/shared": "0.5.1-develop.3043.1a796c3920",
|
|
77
77
|
"@types/jest": "^30.0.0",
|
|
78
78
|
"jest": "^30.3.0",
|
|
79
79
|
"ts-jest": "^29.4.9"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Module } from '@open-mercato/shared/modules/registry'
|
|
1
2
|
import {
|
|
2
3
|
registerCliModules,
|
|
3
4
|
getCliModules,
|
|
@@ -6,6 +7,61 @@ import {
|
|
|
6
7
|
run,
|
|
7
8
|
} from '../mercato'
|
|
8
9
|
|
|
10
|
+
type MockChildAutoExit = { code: number | null; signal?: NodeJS.Signals | null } | undefined
|
|
11
|
+
type MockChildSpawnRouter = (args: string[]) => MockChildAutoExit
|
|
12
|
+
|
|
13
|
+
function buildMockChildProcessModule(routeAutoExit: MockChildSpawnRouter) {
|
|
14
|
+
const { EventEmitter } = jest.requireActual('node:events')
|
|
15
|
+
|
|
16
|
+
const createChild = (spawnargs: string[], autoExit?: MockChildAutoExit) => {
|
|
17
|
+
const child = new EventEmitter() as any
|
|
18
|
+
child.stdout = new EventEmitter()
|
|
19
|
+
child.stderr = new EventEmitter()
|
|
20
|
+
child.spawnargs = spawnargs
|
|
21
|
+
child.killed = false
|
|
22
|
+
child.exitCode = null
|
|
23
|
+
child.signalCode = null
|
|
24
|
+
child.kill = jest.fn((signal: NodeJS.Signals = 'SIGTERM') => {
|
|
25
|
+
child.killed = true
|
|
26
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
child.signalCode = signal
|
|
30
|
+
queueMicrotask(() => {
|
|
31
|
+
child.emit('exit', null, signal)
|
|
32
|
+
})
|
|
33
|
+
return true
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
if (autoExit) {
|
|
37
|
+
queueMicrotask(() => {
|
|
38
|
+
if (child.exitCode !== null || child.signalCode !== null) return
|
|
39
|
+
child.exitCode = autoExit.code
|
|
40
|
+
child.signalCode = autoExit.signal ?? null
|
|
41
|
+
child.emit('exit', child.exitCode, child.signalCode)
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return child
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
spawn: jest.fn((_command: string, args: string[]) => createChild(['node', ...args], routeAutoExit(args))),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const eventsWorkerFixture: Pick<Module, 'id' | 'workers'> = {
|
|
54
|
+
id: 'events',
|
|
55
|
+
workers: [
|
|
56
|
+
{
|
|
57
|
+
id: 'events.test-worker',
|
|
58
|
+
queue: 'events',
|
|
59
|
+
concurrency: 1,
|
|
60
|
+
handler: jest.fn(),
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
}
|
|
64
|
+
|
|
9
65
|
describe('mercato CLI module registration', () => {
|
|
10
66
|
beforeEach(() => {
|
|
11
67
|
// Reset module state by re-importing
|
|
@@ -562,16 +618,19 @@ describe('generate post-step structural cache purge', () => {
|
|
|
562
618
|
|
|
563
619
|
describe('server dev managed process exits', () => {
|
|
564
620
|
const originalAutoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER
|
|
621
|
+
const originalAutoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS
|
|
565
622
|
|
|
566
623
|
beforeEach(() => {
|
|
567
624
|
jest.restoreAllMocks()
|
|
568
625
|
jest.resetModules()
|
|
569
626
|
process.env.AUTO_SPAWN_SCHEDULER = 'false'
|
|
627
|
+
process.env.AUTO_SPAWN_WORKERS = 'true'
|
|
570
628
|
})
|
|
571
629
|
|
|
572
630
|
afterEach(() => {
|
|
573
631
|
jest.dontMock('child_process')
|
|
574
632
|
jest.dontMock('node:fs')
|
|
633
|
+
jest.dontMock('../lib/dev-env-reload')
|
|
575
634
|
jest.dontMock('../lib/generators')
|
|
576
635
|
jest.dontMock('../lib/resolver')
|
|
577
636
|
jest.resetModules()
|
|
@@ -579,12 +638,220 @@ describe('server dev managed process exits', () => {
|
|
|
579
638
|
|
|
580
639
|
afterAll(() => {
|
|
581
640
|
process.env.AUTO_SPAWN_SCHEDULER = originalAutoSpawnScheduler
|
|
641
|
+
process.env.AUTO_SPAWN_WORKERS = originalAutoSpawnWorkers
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
it('skips scheduler auto-start when the module is not enabled', async () => {
|
|
645
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
646
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
647
|
+
process.env.AUTO_SPAWN_SCHEDULER = 'true'
|
|
648
|
+
process.env.AUTO_SPAWN_WORKERS = 'false'
|
|
649
|
+
|
|
650
|
+
jest.doMock('node:fs', () => {
|
|
651
|
+
const actual = jest.requireActual('node:fs')
|
|
652
|
+
return {
|
|
653
|
+
...actual,
|
|
654
|
+
existsSync: jest.fn((candidate: string) =>
|
|
655
|
+
candidate.includes('next/dist/bin/next') || candidate.includes('@open-mercato/cli/bin/mercato'),
|
|
656
|
+
),
|
|
657
|
+
unlinkSync: jest.fn(),
|
|
658
|
+
}
|
|
659
|
+
})
|
|
660
|
+
jest.doMock('../lib/generators', () => ({
|
|
661
|
+
generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
|
|
662
|
+
}))
|
|
663
|
+
jest.doMock('../lib/resolver', () => ({
|
|
664
|
+
resolveEnvironment: () => ({
|
|
665
|
+
appDir: '/tmp/test-app',
|
|
666
|
+
rootDir: '/tmp/test-root',
|
|
667
|
+
}),
|
|
668
|
+
createResolver: () => ({}),
|
|
669
|
+
}))
|
|
670
|
+
jest.doMock('child_process', () =>
|
|
671
|
+
buildMockChildProcessModule((args) =>
|
|
672
|
+
args[0]?.includes('next/dist/bin/next') ? { code: null, signal: 'SIGTERM' } : undefined,
|
|
673
|
+
),
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
const mercato = await import('../mercato')
|
|
677
|
+
const exitCode = await mercato.run(['node', 'mercato', 'server', 'dev'])
|
|
678
|
+
|
|
679
|
+
expect(exitCode).toBe(0)
|
|
680
|
+
expect(consoleErrorSpy).not.toHaveBeenCalled()
|
|
681
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('[server] Skipping scheduler auto-start — module not enabled')
|
|
682
|
+
|
|
683
|
+
consoleErrorSpy.mockRestore()
|
|
684
|
+
consoleLogSpy.mockRestore()
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
it('fails loudly when a managed child exits cleanly but unexpectedly', async () => {
|
|
688
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
689
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
690
|
+
|
|
691
|
+
jest.doMock('node:fs', () => {
|
|
692
|
+
const actual = jest.requireActual('node:fs')
|
|
693
|
+
return {
|
|
694
|
+
...actual,
|
|
695
|
+
existsSync: jest.fn((candidate: string) =>
|
|
696
|
+
candidate.includes('next/dist/bin/next') || candidate.includes('@open-mercato/cli/bin/mercato'),
|
|
697
|
+
),
|
|
698
|
+
unlinkSync: jest.fn(),
|
|
699
|
+
}
|
|
700
|
+
})
|
|
701
|
+
jest.doMock('../lib/generators', () => ({
|
|
702
|
+
generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
|
|
703
|
+
}))
|
|
704
|
+
jest.doMock('../lib/resolver', () => ({
|
|
705
|
+
resolveEnvironment: () => ({
|
|
706
|
+
appDir: '/tmp/test-app',
|
|
707
|
+
rootDir: '/tmp/test-root',
|
|
708
|
+
}),
|
|
709
|
+
createResolver: () => ({}),
|
|
710
|
+
}))
|
|
711
|
+
jest.doMock('child_process', () =>
|
|
712
|
+
buildMockChildProcessModule((args) => {
|
|
713
|
+
if (args.slice(1).join(' ') === 'queue worker --all') {
|
|
714
|
+
return { code: 0 }
|
|
715
|
+
}
|
|
716
|
+
return undefined
|
|
717
|
+
}),
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
const mercato = await import('../mercato')
|
|
721
|
+
mercato.registerCliModules([eventsWorkerFixture as Module])
|
|
722
|
+
|
|
723
|
+
const exitCode = await mercato.run(['node', 'mercato', 'server', 'dev'])
|
|
724
|
+
|
|
725
|
+
expect(exitCode).toBe(1)
|
|
726
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
727
|
+
'💥 Failed: [server] Queue worker (events) exited unexpectedly with exit code 0.',
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
consoleErrorSpy.mockRestore()
|
|
731
|
+
consoleLogSpy.mockRestore()
|
|
732
|
+
})
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
describe('server start managed process exits', () => {
|
|
736
|
+
const originalAutoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER
|
|
737
|
+
const originalAutoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS
|
|
738
|
+
|
|
739
|
+
beforeEach(() => {
|
|
740
|
+
jest.restoreAllMocks()
|
|
741
|
+
jest.resetModules()
|
|
742
|
+
process.env.AUTO_SPAWN_SCHEDULER = 'false'
|
|
743
|
+
process.env.AUTO_SPAWN_WORKERS = 'true'
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
afterEach(() => {
|
|
747
|
+
jest.dontMock('child_process')
|
|
748
|
+
jest.dontMock('node:fs')
|
|
749
|
+
jest.dontMock('../lib/resolver')
|
|
750
|
+
jest.dontMock('../lib/server-start-lock')
|
|
751
|
+
jest.resetModules()
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
afterAll(() => {
|
|
755
|
+
process.env.AUTO_SPAWN_SCHEDULER = originalAutoSpawnScheduler
|
|
756
|
+
process.env.AUTO_SPAWN_WORKERS = originalAutoSpawnWorkers
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
it('skips scheduler auto-start when the module is not enabled', async () => {
|
|
760
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
761
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
762
|
+
process.env.AUTO_SPAWN_SCHEDULER = 'true'
|
|
763
|
+
process.env.AUTO_SPAWN_WORKERS = 'false'
|
|
764
|
+
|
|
765
|
+
jest.doMock('node:fs', () => {
|
|
766
|
+
const actual = jest.requireActual('node:fs')
|
|
767
|
+
return {
|
|
768
|
+
...actual,
|
|
769
|
+
existsSync: jest.fn((candidate: string) =>
|
|
770
|
+
candidate.includes('next/dist/bin/next') || candidate.includes('@open-mercato/cli/bin/mercato'),
|
|
771
|
+
),
|
|
772
|
+
}
|
|
773
|
+
})
|
|
774
|
+
jest.doMock('../lib/resolver', () => ({
|
|
775
|
+
resolveEnvironment: () => ({
|
|
776
|
+
appDir: '/tmp/test-app',
|
|
777
|
+
rootDir: '/tmp/test-root',
|
|
778
|
+
}),
|
|
779
|
+
}))
|
|
780
|
+
jest.doMock('../lib/server-start-lock', () => ({
|
|
781
|
+
acquireServerStartLock: jest.fn(() => ({
|
|
782
|
+
release: jest.fn(),
|
|
783
|
+
})),
|
|
784
|
+
}))
|
|
785
|
+
jest.doMock('child_process', () =>
|
|
786
|
+
buildMockChildProcessModule((args) =>
|
|
787
|
+
args[0]?.includes('next/dist/bin/next') ? { code: null, signal: 'SIGTERM' } : undefined,
|
|
788
|
+
),
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
const mercato = await import('../mercato')
|
|
792
|
+
const exitCode = await mercato.run(['node', 'mercato', 'server', 'start'])
|
|
793
|
+
|
|
794
|
+
expect(exitCode).toBe(0)
|
|
795
|
+
expect(consoleErrorSpy).not.toHaveBeenCalled()
|
|
796
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('[server] Skipping scheduler auto-start — module not enabled')
|
|
797
|
+
|
|
798
|
+
consoleErrorSpy.mockRestore()
|
|
799
|
+
consoleLogSpy.mockRestore()
|
|
582
800
|
})
|
|
583
801
|
|
|
584
802
|
it('fails loudly when a managed child exits cleanly but unexpectedly', async () => {
|
|
585
803
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
586
804
|
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
587
805
|
|
|
806
|
+
jest.doMock('node:fs', () => {
|
|
807
|
+
const actual = jest.requireActual('node:fs')
|
|
808
|
+
return {
|
|
809
|
+
...actual,
|
|
810
|
+
existsSync: jest.fn((candidate: string) =>
|
|
811
|
+
candidate.includes('next/dist/bin/next') || candidate.includes('@open-mercato/cli/bin/mercato'),
|
|
812
|
+
),
|
|
813
|
+
}
|
|
814
|
+
})
|
|
815
|
+
jest.doMock('../lib/resolver', () => ({
|
|
816
|
+
resolveEnvironment: () => ({
|
|
817
|
+
appDir: '/tmp/test-app',
|
|
818
|
+
rootDir: '/tmp/test-root',
|
|
819
|
+
}),
|
|
820
|
+
}))
|
|
821
|
+
jest.doMock('../lib/server-start-lock', () => ({
|
|
822
|
+
acquireServerStartLock: jest.fn(() => ({
|
|
823
|
+
release: jest.fn(),
|
|
824
|
+
})),
|
|
825
|
+
}))
|
|
826
|
+
jest.doMock('child_process', () =>
|
|
827
|
+
buildMockChildProcessModule((args) => {
|
|
828
|
+
if (args.slice(1).join(' ') === 'queue worker --all') {
|
|
829
|
+
return { code: 0 }
|
|
830
|
+
}
|
|
831
|
+
return undefined
|
|
832
|
+
}),
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
const mercato = await import('../mercato')
|
|
836
|
+
mercato.registerCliModules([eventsWorkerFixture as Module])
|
|
837
|
+
|
|
838
|
+
const exitCode = await mercato.run(['node', 'mercato', 'server', 'start'])
|
|
839
|
+
|
|
840
|
+
expect(exitCode).toBe(1)
|
|
841
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
842
|
+
'💥 Failed: [server] Queue worker (events) exited unexpectedly with exit code 0.',
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
consoleErrorSpy.mockRestore()
|
|
846
|
+
consoleLogSpy.mockRestore()
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
it('restarts the managed dev runtime when an app env file changes', async () => {
|
|
850
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
851
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
852
|
+
let envChangeCallback: ((filePath: string) => void) | null = null
|
|
853
|
+
let reloadCount = 0
|
|
854
|
+
|
|
588
855
|
jest.doMock('node:fs', () => {
|
|
589
856
|
const actual = jest.requireActual('node:fs')
|
|
590
857
|
return {
|
|
@@ -595,6 +862,19 @@ describe('server dev managed process exits', () => {
|
|
|
595
862
|
unlinkSync: jest.fn(),
|
|
596
863
|
}
|
|
597
864
|
})
|
|
865
|
+
jest.doMock('../lib/dev-env-reload', () => ({
|
|
866
|
+
createDevEnvReloader: () => ({
|
|
867
|
+
reload: jest.fn(() => {
|
|
868
|
+
reloadCount += 1
|
|
869
|
+
process.env.RESTART_TOKEN = reloadCount === 1 ? 'initial' : 'changed'
|
|
870
|
+
}),
|
|
871
|
+
getWatchedFiles: () => ['/tmp/test-app/.env'],
|
|
872
|
+
}),
|
|
873
|
+
watchDevEnvFiles: jest.fn((_appDir: string, onChange: (filePath: string) => void) => {
|
|
874
|
+
envChangeCallback = onChange
|
|
875
|
+
return jest.fn()
|
|
876
|
+
}),
|
|
877
|
+
}))
|
|
598
878
|
jest.doMock('../lib/generators', () => ({
|
|
599
879
|
generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
|
|
600
880
|
}))
|
|
@@ -607,15 +887,14 @@ describe('server dev managed process exits', () => {
|
|
|
607
887
|
}))
|
|
608
888
|
jest.doMock('child_process', () => {
|
|
609
889
|
const { EventEmitter } = jest.requireActual('node:events')
|
|
890
|
+
let nextSpawnCount = 0
|
|
610
891
|
|
|
611
892
|
const createChild = (
|
|
612
|
-
spawnargs: string[],
|
|
613
893
|
autoExit?: { code: number | null; signal?: NodeJS.Signals | null },
|
|
614
894
|
) => {
|
|
615
895
|
const child = new EventEmitter() as any
|
|
616
896
|
child.stdout = new EventEmitter()
|
|
617
897
|
child.stderr = new EventEmitter()
|
|
618
|
-
child.spawnargs = spawnargs
|
|
619
898
|
child.killed = false
|
|
620
899
|
child.exitCode = null
|
|
621
900
|
child.signalCode = null
|
|
@@ -646,37 +925,34 @@ describe('server dev managed process exits', () => {
|
|
|
646
925
|
return {
|
|
647
926
|
spawn: jest.fn((_command: string, args: string[]) => {
|
|
648
927
|
if (args[0]?.includes('next/dist/bin/next')) {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
928
|
+
nextSpawnCount += 1
|
|
929
|
+
if (nextSpawnCount === 1) {
|
|
930
|
+
queueMicrotask(() => envChangeCallback?.('/tmp/test-app/.env'))
|
|
931
|
+
return createChild()
|
|
932
|
+
}
|
|
933
|
+
return createChild({ code: null, signal: 'SIGTERM' })
|
|
653
934
|
}
|
|
654
|
-
return createChild(
|
|
935
|
+
return createChild()
|
|
655
936
|
}),
|
|
656
937
|
}
|
|
657
938
|
})
|
|
658
939
|
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
queue: 'events',
|
|
666
|
-
concurrency: 1,
|
|
667
|
-
handler: jest.fn(),
|
|
668
|
-
},
|
|
669
|
-
],
|
|
670
|
-
} as any,
|
|
671
|
-
])
|
|
672
|
-
|
|
673
|
-
const exitCode = await mercato.run(['node', 'mercato', 'server', 'dev'])
|
|
940
|
+
const mercado = await import('../mercato')
|
|
941
|
+
const exitCode = await mercado.run(['node', 'mercato', 'server', 'dev'])
|
|
942
|
+
const { spawn } = await import('child_process')
|
|
943
|
+
const nextSpawns = (spawn as jest.Mock).mock.calls.filter((call) =>
|
|
944
|
+
call[1]?.[0]?.includes('next/dist/bin/next'),
|
|
945
|
+
)
|
|
674
946
|
|
|
675
|
-
expect(exitCode).toBe(
|
|
676
|
-
expect(
|
|
677
|
-
|
|
947
|
+
expect(exitCode).toBe(0)
|
|
948
|
+
expect(nextSpawns).toHaveLength(2)
|
|
949
|
+
expect(nextSpawns[0][2].env.RESTART_TOKEN).toBe('initial')
|
|
950
|
+
expect(nextSpawns[1][2].env.RESTART_TOKEN).toBe('changed')
|
|
951
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
952
|
+
'[server] Detected environment file change (.env). Restarting app runtime...',
|
|
678
953
|
)
|
|
679
954
|
|
|
955
|
+
delete process.env.RESTART_TOKEN
|
|
680
956
|
consoleErrorSpy.mockRestore()
|
|
681
957
|
consoleLogSpy.mockRestore()
|
|
682
958
|
})
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test'
|
|
2
|
+
import { execFileSync } from 'node:child_process'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const repoRoot = path.resolve(__dirname, '..', '..', '..', '..', '..')
|
|
10
|
+
const cliBin = path.join(repoRoot, 'packages', 'cli', 'dist', 'bin.js')
|
|
11
|
+
const fixtureModulePackage = path.join(
|
|
12
|
+
repoRoot,
|
|
13
|
+
'packages',
|
|
14
|
+
'cli',
|
|
15
|
+
'src',
|
|
16
|
+
'lib',
|
|
17
|
+
'__fixtures__',
|
|
18
|
+
'official-module-package',
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
function yarnBinary(): string {
|
|
22
|
+
return process.platform === 'win32' ? 'yarn.cmd' : 'yarn'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function runCommand(command: string, args: string[], cwd: string): string {
|
|
26
|
+
const yarnCacheFolder = path.join(cwd, '.yarn', 'cache')
|
|
27
|
+
return execFileSync(command, args, {
|
|
28
|
+
cwd,
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
env: {
|
|
31
|
+
...process.env,
|
|
32
|
+
FORCE_COLOR: '0',
|
|
33
|
+
NODE_NO_WARNINGS: '1',
|
|
34
|
+
YARN_CACHE_FOLDER: yarnCacheFolder,
|
|
35
|
+
YARN_ENABLE_GLOBAL_CACHE: '0',
|
|
36
|
+
YARN_ENABLE_IMMUTABLE_INSTALLS: '0',
|
|
37
|
+
YARN_NODE_LINKER: 'node-modules',
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function runMercato(args: string[], cwd: string): string {
|
|
43
|
+
return runCommand(process.execPath, [cliBin, ...args], cwd)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function writeFile(filePath: string, content: string): void {
|
|
47
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
48
|
+
fs.writeFileSync(filePath, content)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createCoreWorkspacePackage(rootDir: string, relativeDir: string): string {
|
|
52
|
+
const coreDir = path.join(rootDir, relativeDir)
|
|
53
|
+
writeFile(
|
|
54
|
+
path.join(coreDir, 'package.json'),
|
|
55
|
+
JSON.stringify(
|
|
56
|
+
{
|
|
57
|
+
name: '@open-mercato/core',
|
|
58
|
+
version: '0.4.7',
|
|
59
|
+
type: 'module',
|
|
60
|
+
},
|
|
61
|
+
null,
|
|
62
|
+
2,
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
return coreDir
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createMonorepoFixture(rootDir: string): string {
|
|
69
|
+
writeFile(
|
|
70
|
+
path.join(rootDir, 'package.json'),
|
|
71
|
+
JSON.stringify(
|
|
72
|
+
{
|
|
73
|
+
name: 'cli-module-monorepo-fixture',
|
|
74
|
+
private: true,
|
|
75
|
+
workspaces: ['apps/*', 'packages/*'],
|
|
76
|
+
},
|
|
77
|
+
null,
|
|
78
|
+
2,
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
writeFile(
|
|
82
|
+
path.join(rootDir, '.yarnrc.yml'),
|
|
83
|
+
['nodeLinker: node-modules', 'enableGlobalCache: false', 'cacheFolder: ./.yarn/cache', ''].join('\n'),
|
|
84
|
+
)
|
|
85
|
+
createCoreWorkspacePackage(rootDir, path.join('packages', 'core'))
|
|
86
|
+
|
|
87
|
+
const appDir = path.join(rootDir, 'apps', 'mercato')
|
|
88
|
+
writeFile(
|
|
89
|
+
path.join(appDir, 'package.json'),
|
|
90
|
+
JSON.stringify(
|
|
91
|
+
{
|
|
92
|
+
name: '@open-mercato/app',
|
|
93
|
+
version: '0.0.0',
|
|
94
|
+
private: true,
|
|
95
|
+
},
|
|
96
|
+
null,
|
|
97
|
+
2,
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
writeFile(path.join(appDir, 'src', 'modules.ts'), 'export const enabledModules = []\n')
|
|
101
|
+
return appDir
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createStandaloneFixture(rootDir: string): string {
|
|
105
|
+
writeFile(
|
|
106
|
+
path.join(rootDir, '.yarnrc.yml'),
|
|
107
|
+
['nodeLinker: node-modules', 'enableGlobalCache: false', 'cacheFolder: ./.yarn/cache', ''].join('\n'),
|
|
108
|
+
)
|
|
109
|
+
createCoreWorkspacePackage(rootDir, path.join('vendor', 'core'))
|
|
110
|
+
writeFile(
|
|
111
|
+
path.join(rootDir, 'package.json'),
|
|
112
|
+
JSON.stringify(
|
|
113
|
+
{
|
|
114
|
+
name: 'cli-module-standalone-fixture',
|
|
115
|
+
private: true,
|
|
116
|
+
dependencies: {
|
|
117
|
+
'@open-mercato/core': 'file:./vendor/core',
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
null,
|
|
121
|
+
2,
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
writeFile(path.join(rootDir, 'src', 'modules.ts'), 'export const enabledModules = []\n')
|
|
125
|
+
return rootDir
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function readFile(filePath: string): string {
|
|
129
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
test.describe('TC-INT-007: CLI official module install and eject flows', () => {
|
|
133
|
+
test('module add installs and registers a package-backed module in a monorepo app', () => {
|
|
134
|
+
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-cli-monorepo-'))
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const appDir = createMonorepoFixture(rootDir)
|
|
138
|
+
runCommand(yarnBinary(), ['install'], rootDir)
|
|
139
|
+
|
|
140
|
+
runMercato(
|
|
141
|
+
[
|
|
142
|
+
'module',
|
|
143
|
+
'add',
|
|
144
|
+
`@open-mercato/test-package@file:${fixtureModulePackage}`,
|
|
145
|
+
],
|
|
146
|
+
rootDir,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
const modulesSource = readFile(path.join(appDir, 'src', 'modules.ts'))
|
|
150
|
+
const cssSource = readFile(path.join(appDir, '.mercato', 'generated', 'module-package-sources.css'))
|
|
151
|
+
const appPackageJson = JSON.parse(readFile(path.join(appDir, 'package.json'))) as {
|
|
152
|
+
dependencies?: Record<string, string>
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
expect(modulesSource).toContain("{ id: 'test_package', from: '@open-mercato/test-package' }")
|
|
156
|
+
expect(cssSource).toContain('node_modules/@open-mercato/test-package/src/**/*.{ts,tsx}')
|
|
157
|
+
expect(appPackageJson.dependencies?.['@open-mercato/test-package']).toBeTruthy()
|
|
158
|
+
} finally {
|
|
159
|
+
fs.rmSync(rootDir, { recursive: true, force: true })
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('module add --eject copies module source and omits package CSS entries', () => {
|
|
164
|
+
const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-cli-standalone-source-'))
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
createStandaloneFixture(appDir)
|
|
168
|
+
runCommand(yarnBinary(), ['install'], appDir)
|
|
169
|
+
|
|
170
|
+
runMercato(
|
|
171
|
+
[
|
|
172
|
+
'module',
|
|
173
|
+
'add',
|
|
174
|
+
`@open-mercato/test-package@file:${fixtureModulePackage}`,
|
|
175
|
+
'--eject',
|
|
176
|
+
],
|
|
177
|
+
appDir,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
const modulesSource = readFile(path.join(appDir, 'src', 'modules.ts'))
|
|
181
|
+
const cssSource = readFile(path.join(appDir, '.mercato', 'generated', 'module-package-sources.css'))
|
|
182
|
+
|
|
183
|
+
expect(modulesSource).toContain("{ id: 'test_package', from: '@app' }")
|
|
184
|
+
expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'test_package', 'index.ts'))).toBe(true)
|
|
185
|
+
expect(cssSource).toBe('')
|
|
186
|
+
} finally {
|
|
187
|
+
fs.rmSync(appDir, { recursive: true, force: true })
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('module enable supports package-backed and ejected flows plus both eject entrypoints', () => {
|
|
192
|
+
const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-cli-standalone-eject-'))
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
createStandaloneFixture(appDir)
|
|
196
|
+
runCommand(yarnBinary(), ['install'], appDir)
|
|
197
|
+
runCommand(
|
|
198
|
+
yarnBinary(),
|
|
199
|
+
['add', `@open-mercato/test-package@file:${fixtureModulePackage}`],
|
|
200
|
+
appDir,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
runMercato(['module', 'enable', '@open-mercato/test-package'], appDir)
|
|
204
|
+
expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toContain(
|
|
205
|
+
"{ id: 'test_package', from: '@open-mercato/test-package' }",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
writeFile(path.join(appDir, 'src', 'modules.ts'), 'export const enabledModules = []\n')
|
|
209
|
+
fs.rmSync(path.join(appDir, 'src', 'modules', 'test_package'), { recursive: true, force: true })
|
|
210
|
+
runMercato(['module', 'enable', '@open-mercato/test-package', '--eject'], appDir)
|
|
211
|
+
expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toContain("{ id: 'test_package', from: '@app' }")
|
|
212
|
+
expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'test_package', 'index.ts'))).toBe(true)
|
|
213
|
+
expect(readFile(path.join(appDir, '.mercato', 'generated', 'module-package-sources.css'))).toBe('')
|
|
214
|
+
|
|
215
|
+
writeFile(path.join(appDir, 'src', 'modules.ts'), "export const enabledModules = [{ id: 'test_package', from: '@open-mercato/test-package' }]\n")
|
|
216
|
+
fs.rmSync(path.join(appDir, 'src', 'modules', 'test_package'), { recursive: true, force: true })
|
|
217
|
+
runMercato(['module', 'eject', 'test_package'], appDir)
|
|
218
|
+
expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toContain("{ id: 'test_package', from: '@app' }")
|
|
219
|
+
|
|
220
|
+
writeFile(path.join(appDir, 'src', 'modules.ts'), "export const enabledModules = [{ id: 'test_package', from: '@open-mercato/test-package' }]\n")
|
|
221
|
+
fs.rmSync(path.join(appDir, 'src', 'modules', 'test_package'), { recursive: true, force: true })
|
|
222
|
+
runMercato(['eject', 'test_package'], appDir)
|
|
223
|
+
expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toContain("{ id: 'test_package', from: '@app' }")
|
|
224
|
+
} finally {
|
|
225
|
+
fs.rmSync(appDir, { recursive: true, force: true })
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
})
|