@graphcommerce/next-config 9.0.0-canary.98 → 9.0.0
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/CHANGELOG.md +219 -1149
- package/__tests__/commands/copyFiles.ts +512 -0
- package/__tests__/config/utils/__snapshots__/mergeEnvIntoConfig.ts.snap +6 -0
- package/__tests__/config/utils/mergeEnvIntoConfig.ts +9 -20
- package/__tests__/config/utils/rewriteLegancyEnv.ts +32 -36
- package/__tests__/interceptors/findPlugins.ts +80 -74
- package/__tests__/interceptors/generateInterceptors.ts +78 -135
- package/__tests__/interceptors/parseStructure.ts +2 -2
- package/__tests__/utils/resolveDependenciesSync.ts +11 -10
- package/dist/commands/codegen.js +18 -0
- package/dist/commands/copyFiles.js +292 -0
- package/dist/commands/copyRoutes.js +20 -0
- package/dist/config/commands/exportConfig.js +1 -1
- package/dist/config/commands/generateConfig.js +2 -2
- package/dist/config/demoConfig.js +2 -2
- package/dist/config/utils/mergeEnvIntoConfig.js +18 -20
- package/dist/config/utils/rewriteLegacyEnv.js +2 -2
- package/dist/generated/config.js +13 -1
- package/dist/index.js +3 -1
- package/dist/interceptors/InterceptorPlugin.js +4 -3
- package/dist/interceptors/Visitor.js +5 -9
- package/dist/interceptors/commands/codegenInterceptors.js +2 -2
- package/dist/interceptors/extractExports.js +9 -54
- package/dist/interceptors/findOriginalSource.js +2 -1
- package/dist/interceptors/findPlugins.js +5 -8
- package/dist/interceptors/generateInterceptor.js +12 -10
- package/dist/interceptors/generateInterceptors.js +3 -2
- package/dist/interceptors/parseStructure.js +1 -1
- package/dist/interceptors/writeInterceptors.js +2 -2
- package/dist/utils/TopologicalSort.js +4 -0
- package/dist/utils/isMonorepo.js +40 -6
- package/dist/utils/resolveDependenciesSync.js +9 -2
- package/dist/utils/sig.js +34 -0
- package/dist/withGraphCommerce.js +3 -2
- package/package.json +17 -16
- package/src/commands/codegen.ts +18 -0
- package/src/commands/copyFiles.ts +328 -0
- package/src/config/commands/exportConfig.ts +1 -1
- package/src/config/commands/generateConfig.ts +3 -3
- package/src/config/demoConfig.ts +5 -5
- package/src/config/index.ts +1 -1
- package/src/config/utils/exportConfigToEnv.ts +1 -1
- package/src/config/utils/mergeEnvIntoConfig.ts +22 -25
- package/src/config/utils/replaceConfigInString.ts +1 -1
- package/src/config/utils/rewriteLegacyEnv.ts +5 -4
- package/src/generated/config.ts +36 -0
- package/src/index.ts +6 -5
- package/src/interceptors/InterceptorPlugin.ts +10 -7
- package/src/interceptors/RenameVisitor.ts +1 -1
- package/src/interceptors/Visitor.ts +10 -15
- package/src/interceptors/commands/codegenInterceptors.ts +2 -2
- package/src/interceptors/extractExports.ts +4 -46
- package/src/interceptors/findOriginalSource.ts +5 -4
- package/src/interceptors/findPlugins.ts +8 -9
- package/src/interceptors/generateInterceptor.ts +15 -12
- package/src/interceptors/generateInterceptors.ts +7 -13
- package/src/interceptors/parseStructure.ts +4 -4
- package/src/interceptors/swc.ts +2 -1
- package/src/interceptors/writeInterceptors.ts +3 -3
- package/src/utils/TopologicalSort.ts +4 -0
- package/src/utils/isMonorepo.ts +46 -5
- package/src/utils/packageRoots.ts +1 -1
- package/src/utils/resolveDependenciesSync.ts +14 -2
- package/src/utils/sig.ts +37 -0
- package/src/withGraphCommerce.ts +7 -5
- package/dist/config/commands/generateIntercetors.js +0 -9
- package/dist/interceptors/commands/generateIntercetors.js +0 -9
- package/dist/runtimeCachingOptimizations.js +0 -28
- package/src/runtimeCachingOptimizations.ts +0 -27
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import fs from 'fs/promises'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { copyFiles } from '../../src/commands/copyFiles'
|
|
4
|
+
import { resolveDependenciesSync } from '../../src/utils/resolveDependenciesSync'
|
|
5
|
+
|
|
6
|
+
// Mock fs/promises
|
|
7
|
+
jest.mock('fs/promises', () => ({
|
|
8
|
+
readFile: jest.fn(),
|
|
9
|
+
writeFile: jest.fn(),
|
|
10
|
+
mkdir: jest.fn(),
|
|
11
|
+
readdir: jest.fn(),
|
|
12
|
+
stat: jest.fn(),
|
|
13
|
+
unlink: jest.fn(),
|
|
14
|
+
rmdir: jest.fn(),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
// Mock fast-glob
|
|
18
|
+
jest.mock('fast-glob', () => ({
|
|
19
|
+
__esModule: true,
|
|
20
|
+
default: jest.fn(),
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
// Mock resolveDependenciesSync
|
|
24
|
+
jest.mock('../../src/utils/resolveDependenciesSync', () => ({
|
|
25
|
+
resolveDependenciesSync: jest.fn(),
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
// Mock performance.now
|
|
29
|
+
const mockPerformanceNow = jest.fn()
|
|
30
|
+
global.performance = { now: mockPerformanceNow } as unknown as typeof performance
|
|
31
|
+
|
|
32
|
+
// Mock process.cwd
|
|
33
|
+
const mockCwd = '/mock/cwd'
|
|
34
|
+
const originalCwd = process.cwd
|
|
35
|
+
beforeAll(() => {
|
|
36
|
+
process.cwd = jest.fn().mockReturnValue(mockCwd)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
afterAll(() => {
|
|
40
|
+
process.cwd = originalCwd
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('copyFiles', () => {
|
|
44
|
+
let consoleLog: jest.SpyInstance
|
|
45
|
+
let consoleError: jest.SpyInstance
|
|
46
|
+
let processExit: jest.SpyInstance
|
|
47
|
+
let originalDebug: string | undefined
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
jest.clearAllMocks()
|
|
51
|
+
originalDebug = process.env.DEBUG
|
|
52
|
+
process.env.DEBUG = undefined
|
|
53
|
+
;(resolveDependenciesSync as jest.Mock).mockReturnValue(
|
|
54
|
+
new Map([
|
|
55
|
+
['@graphcommerce/package1', 'packages/package1'],
|
|
56
|
+
['@graphcommerce/package2', 'packages/package2'],
|
|
57
|
+
]),
|
|
58
|
+
)
|
|
59
|
+
consoleLog = jest.spyOn(console, 'log').mockImplementation(() => {})
|
|
60
|
+
consoleError = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
61
|
+
processExit = jest.spyOn(process, 'exit').mockImplementation(() => undefined as never)
|
|
62
|
+
|
|
63
|
+
// Setup default .gitignore mock
|
|
64
|
+
;(fs.readFile as jest.Mock).mockImplementation((filePath: string) => {
|
|
65
|
+
if (filePath === path.join(mockCwd, '.gitignore')) {
|
|
66
|
+
return Promise.resolve('existing\ngitignore\ncontent')
|
|
67
|
+
}
|
|
68
|
+
return Promise.reject(new Error(`ENOENT: no such file or directory, open '${filePath}'`))
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Setup performance.now mock
|
|
72
|
+
let time = 0
|
|
73
|
+
mockPerformanceNow.mockImplementation(() => {
|
|
74
|
+
time += 50 // Increment by 50ms each call
|
|
75
|
+
return time
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
process.env.DEBUG = originalDebug
|
|
81
|
+
consoleLog.mockRestore()
|
|
82
|
+
consoleError.mockRestore()
|
|
83
|
+
processExit.mockRestore()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should handle empty source directories', async () => {
|
|
87
|
+
const fg = jest.requireMock('fast-glob').default as jest.Mock
|
|
88
|
+
fg.mockResolvedValue([])
|
|
89
|
+
|
|
90
|
+
await copyFiles()
|
|
91
|
+
|
|
92
|
+
expect(fg).toHaveBeenCalledWith('**/*', {
|
|
93
|
+
cwd: mockCwd,
|
|
94
|
+
dot: true,
|
|
95
|
+
ignore: ['**/dist/**', '**/build/**', '**/.next/**', '**/.git/**', '**/node_modules/**'],
|
|
96
|
+
onlyFiles: true,
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should copy files and add management comments', async () => {
|
|
101
|
+
const fg = jest.requireMock('fast-glob').default as jest.Mock
|
|
102
|
+
fg.mockResolvedValueOnce([]) // First scan for existing files
|
|
103
|
+
.mockResolvedValueOnce(['file.ts']) // Second scan for package files
|
|
104
|
+
;(fs.readFile as jest.Mock).mockImplementation((filePath: string) => {
|
|
105
|
+
if (filePath === path.join('packages/package1/copy', 'file.ts')) {
|
|
106
|
+
return Promise.resolve(Buffer.from('content'))
|
|
107
|
+
}
|
|
108
|
+
if (filePath === path.join(mockCwd, '.gitignore')) {
|
|
109
|
+
return Promise.resolve('existing\ngitignore\ncontent')
|
|
110
|
+
}
|
|
111
|
+
return Promise.reject(new Error(`ENOENT: no such file or directory, open '${filePath}'`))
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const mockStat = fs.stat as jest.Mock
|
|
115
|
+
mockStat.mockResolvedValue({ isDirectory: () => false })
|
|
116
|
+
|
|
117
|
+
await copyFiles()
|
|
118
|
+
|
|
119
|
+
// Verify file was written with management comments
|
|
120
|
+
const writeCall = (fs.writeFile as jest.Mock).mock.calls.find(
|
|
121
|
+
(call) => call[0] === path.join(mockCwd, 'file.ts'),
|
|
122
|
+
)
|
|
123
|
+
expect(writeCall).toBeTruthy()
|
|
124
|
+
const content = writeCall[1].toString()
|
|
125
|
+
expect(content).toContain('// managed by: graphcommerce')
|
|
126
|
+
expect(content).toContain('// to modify this file, change it to managed by: local')
|
|
127
|
+
expect(content).toContain('content')
|
|
128
|
+
|
|
129
|
+
// Verify .gitignore was updated
|
|
130
|
+
const gitignoreCall = (fs.writeFile as jest.Mock).mock.calls.find(
|
|
131
|
+
(call) => call[0] === path.join(mockCwd, '.gitignore'),
|
|
132
|
+
)
|
|
133
|
+
expect(gitignoreCall).toBeTruthy()
|
|
134
|
+
const gitignoreContent = gitignoreCall[1].toString()
|
|
135
|
+
expect(gitignoreContent).toContain('# managed by: graphcommerce')
|
|
136
|
+
expect(gitignoreContent).toContain('file.ts')
|
|
137
|
+
expect(gitignoreContent).toContain('# end managed by: graphcommerce')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('should handle existing managed files with identical content', async () => {
|
|
141
|
+
const fg = jest.requireMock('fast-glob').default as jest.Mock
|
|
142
|
+
fg.mockResolvedValueOnce([]) // First scan for existing files
|
|
143
|
+
.mockResolvedValueOnce(['file.ts']) // Second scan for package files
|
|
144
|
+
|
|
145
|
+
const sourceContent = Buffer.from('content')
|
|
146
|
+
const managedContent = Buffer.concat([
|
|
147
|
+
Buffer.from(
|
|
148
|
+
'// managed by: graphcommerce\n// to modify this file, change it to managed by: local\n\n',
|
|
149
|
+
),
|
|
150
|
+
sourceContent,
|
|
151
|
+
])
|
|
152
|
+
|
|
153
|
+
;(fs.readFile as jest.Mock).mockImplementation((filePath: string) => {
|
|
154
|
+
if (filePath === path.join('packages/package1/copy', 'file.ts')) {
|
|
155
|
+
return Promise.resolve(sourceContent)
|
|
156
|
+
}
|
|
157
|
+
if (filePath === path.join(mockCwd, 'file.ts')) {
|
|
158
|
+
return Promise.resolve(managedContent)
|
|
159
|
+
}
|
|
160
|
+
if (filePath === path.join(mockCwd, '.gitignore')) {
|
|
161
|
+
return Promise.resolve('existing\ngitignore\ncontent')
|
|
162
|
+
}
|
|
163
|
+
return Promise.reject(new Error(`ENOENT: no such file or directory, open '${filePath}'`))
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
await copyFiles()
|
|
167
|
+
|
|
168
|
+
// Should not write to the file since content is identical
|
|
169
|
+
expect(fs.writeFile).toHaveBeenCalledTimes(1) // Only .gitignore should be written
|
|
170
|
+
expect(fs.writeFile).toHaveBeenCalledWith(path.join(mockCwd, '.gitignore'), expect.any(String))
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('should update existing managed files with different content', async () => {
|
|
174
|
+
const fg = jest.requireMock('fast-glob').default as jest.Mock
|
|
175
|
+
fg.mockResolvedValueOnce([]) // First scan for existing files
|
|
176
|
+
.mockResolvedValueOnce(['file.ts']) // Second scan for package files
|
|
177
|
+
|
|
178
|
+
const sourceContent = Buffer.from('new content')
|
|
179
|
+
const oldContent = Buffer.concat([
|
|
180
|
+
Buffer.from(
|
|
181
|
+
'// managed by: graphcommerce\n// to modify this file, change it to managed by: local\n\n',
|
|
182
|
+
),
|
|
183
|
+
Buffer.from('old content'),
|
|
184
|
+
])
|
|
185
|
+
|
|
186
|
+
;(fs.readFile as jest.Mock).mockImplementation((filePath: string) => {
|
|
187
|
+
if (filePath === path.join('packages/package1/copy', 'file.ts')) {
|
|
188
|
+
return Promise.resolve(sourceContent)
|
|
189
|
+
}
|
|
190
|
+
if (filePath === path.join(mockCwd, 'file.ts')) {
|
|
191
|
+
return Promise.resolve(oldContent)
|
|
192
|
+
}
|
|
193
|
+
if (filePath === path.join(mockCwd, '.gitignore')) {
|
|
194
|
+
return Promise.resolve('existing\ngitignore\ncontent')
|
|
195
|
+
}
|
|
196
|
+
return Promise.reject(new Error(`ENOENT: no such file or directory, open '${filePath}'`))
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
await copyFiles()
|
|
200
|
+
|
|
201
|
+
// Should write the new content
|
|
202
|
+
const writeCall = (fs.writeFile as jest.Mock).mock.calls.find(
|
|
203
|
+
(call) => call[0] === path.join(mockCwd, 'file.ts'),
|
|
204
|
+
)
|
|
205
|
+
expect(writeCall).toBeTruthy()
|
|
206
|
+
const content = writeCall[1].toString()
|
|
207
|
+
expect(content).toContain('new content')
|
|
208
|
+
expect(consoleLog).toHaveBeenCalledWith('Updated managed file: file.ts')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('should create new files with management comments', async () => {
|
|
212
|
+
const fg = jest.requireMock('fast-glob').default as jest.Mock
|
|
213
|
+
fg.mockResolvedValueOnce([]) // First scan for existing files
|
|
214
|
+
.mockResolvedValueOnce(['new-file.ts']) // Second scan for package files
|
|
215
|
+
;(fs.readFile as jest.Mock).mockImplementation((filePath: string) => {
|
|
216
|
+
if (filePath === path.join('packages/package1/copy', 'new-file.ts')) {
|
|
217
|
+
return Promise.resolve(Buffer.from('content'))
|
|
218
|
+
}
|
|
219
|
+
if (filePath === path.join(mockCwd, '.gitignore')) {
|
|
220
|
+
return Promise.resolve('existing\ngitignore\ncontent')
|
|
221
|
+
}
|
|
222
|
+
if (filePath === path.join(mockCwd, 'new-file.ts')) {
|
|
223
|
+
return Promise.reject(new Error('ENOENT: no such file or directory'))
|
|
224
|
+
}
|
|
225
|
+
return Promise.reject(new Error(`ENOENT: no such file or directory, open '${filePath}'`))
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
await copyFiles()
|
|
229
|
+
|
|
230
|
+
expect(consoleLog).toHaveBeenCalledWith(
|
|
231
|
+
'Creating new file: new-file.ts\nSource: packages/package1/copy/new-file.ts',
|
|
232
|
+
)
|
|
233
|
+
expect(fs.writeFile).toHaveBeenCalledWith(path.join(mockCwd, 'new-file.ts'), expect.any(Buffer))
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('should handle locally managed files', async () => {
|
|
237
|
+
const fg = jest.requireMock('fast-glob').default as jest.Mock
|
|
238
|
+
fg.mockResolvedValueOnce([]) // First scan for existing files
|
|
239
|
+
.mockResolvedValueOnce(['file.ts']) // Second scan for package files
|
|
240
|
+
;(fs.readFile as jest.Mock).mockImplementation((filePath: string) => {
|
|
241
|
+
if (filePath === path.join('packages/package1/copy', 'file.ts')) {
|
|
242
|
+
return Promise.resolve(Buffer.from('content'))
|
|
243
|
+
}
|
|
244
|
+
if (filePath === path.join(mockCwd, 'file.ts')) {
|
|
245
|
+
return Promise.resolve(Buffer.from('// managed by: local\ncontent'))
|
|
246
|
+
}
|
|
247
|
+
if (filePath === path.join(mockCwd, '.gitignore')) {
|
|
248
|
+
return Promise.resolve('existing\ngitignore\ncontent')
|
|
249
|
+
}
|
|
250
|
+
return Promise.reject(new Error(`ENOENT: no such file or directory, open '${filePath}'`))
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
await copyFiles()
|
|
254
|
+
|
|
255
|
+
// Should not overwrite locally managed files
|
|
256
|
+
expect(fs.writeFile).toHaveBeenCalledTimes(1) // Only .gitignore should be written
|
|
257
|
+
expect(fs.writeFile).toHaveBeenCalledWith(path.join(mockCwd, '.gitignore'), expect.any(String))
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('should create destination directory if it does not exist', async () => {
|
|
261
|
+
const fg = jest.requireMock('fast-glob').default as jest.Mock
|
|
262
|
+
fg.mockResolvedValueOnce([]) // First scan for existing files
|
|
263
|
+
.mockResolvedValueOnce(['nested/file.ts']) // Second scan for package files
|
|
264
|
+
;(fs.readFile as jest.Mock).mockImplementation((filePath: string) => {
|
|
265
|
+
if (filePath === path.join('packages/package1/copy', 'nested/file.ts')) {
|
|
266
|
+
return Promise.resolve(Buffer.from('content'))
|
|
267
|
+
}
|
|
268
|
+
if (filePath === path.join(mockCwd, '.gitignore')) {
|
|
269
|
+
return Promise.resolve('existing\ngitignore\ncontent')
|
|
270
|
+
}
|
|
271
|
+
return Promise.reject(new Error(`ENOENT: no such file or directory, open '${filePath}'`))
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
const mockMkdir = fs.mkdir as jest.Mock
|
|
275
|
+
mockMkdir.mockResolvedValue(undefined)
|
|
276
|
+
|
|
277
|
+
await copyFiles()
|
|
278
|
+
|
|
279
|
+
expect(mockMkdir).toHaveBeenCalledWith(path.join(mockCwd, 'nested'), { recursive: true })
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('should handle errors gracefully', async () => {
|
|
283
|
+
const fg = jest.requireMock('fast-glob').default as jest.Mock
|
|
284
|
+
fg.mockResolvedValueOnce([]) // First scan for existing files
|
|
285
|
+
.mockResolvedValueOnce(['file.ts']) // Second scan for package files
|
|
286
|
+
;(fs.readFile as jest.Mock).mockImplementation((filePath: string) => {
|
|
287
|
+
if (filePath === path.join('packages/package1/copy', 'file.ts')) {
|
|
288
|
+
return Promise.reject(new Error('Read error'))
|
|
289
|
+
}
|
|
290
|
+
if (filePath === path.join(mockCwd, '.gitignore')) {
|
|
291
|
+
return Promise.resolve('existing\ngitignore\ncontent')
|
|
292
|
+
}
|
|
293
|
+
return Promise.reject(new Error(`ENOENT: no such file or directory, open '${filePath}'`))
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
await copyFiles()
|
|
297
|
+
|
|
298
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
299
|
+
expect.stringContaining(
|
|
300
|
+
'Error copying file file.ts: Read error\nSource: packages/package1/copy/file.ts',
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
expect(processExit).toHaveBeenCalledWith(1)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('should detect file conflicts between packages', async () => {
|
|
307
|
+
const fg = jest.requireMock('fast-glob').default as jest.Mock
|
|
308
|
+
fg.mockResolvedValueOnce([]) // First scan for existing files
|
|
309
|
+
.mockResolvedValueOnce(['conflict.ts']) // Package 1 files
|
|
310
|
+
.mockResolvedValueOnce(['conflict.ts']) // Package 2 files
|
|
311
|
+
|
|
312
|
+
await copyFiles()
|
|
313
|
+
|
|
314
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
315
|
+
expect.stringContaining("Error: File conflict detected for 'conflict.ts'"),
|
|
316
|
+
)
|
|
317
|
+
expect(processExit).toHaveBeenCalledWith(1)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('should remove files that are no longer provided', async () => {
|
|
321
|
+
const fg = jest.requireMock('fast-glob').default as jest.Mock
|
|
322
|
+
fg.mockResolvedValueOnce(['old-file.ts']) // First scan finds existing managed file
|
|
323
|
+
.mockResolvedValueOnce([]) // Second scan finds no files in packages
|
|
324
|
+
|
|
325
|
+
// Mock existing managed file
|
|
326
|
+
;(fs.readFile as jest.Mock).mockImplementation((filePath: string) => {
|
|
327
|
+
if (filePath === path.join(mockCwd, 'old-file.ts')) {
|
|
328
|
+
return Promise.resolve(Buffer.from('// managed by: graphcommerce\ncontent'))
|
|
329
|
+
}
|
|
330
|
+
if (filePath === path.join(mockCwd, '.gitignore')) {
|
|
331
|
+
return Promise.resolve('existing\ngitignore\ncontent')
|
|
332
|
+
}
|
|
333
|
+
return Promise.reject(new Error(`ENOENT: no such file or directory, open '${filePath}'`))
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
// Mock directory checks
|
|
337
|
+
const mockReaddir = fs.readdir as jest.Mock
|
|
338
|
+
mockReaddir.mockImplementation((dirPath: string) => {
|
|
339
|
+
if (dirPath === mockCwd) {
|
|
340
|
+
return Promise.resolve(['old-file.ts'])
|
|
341
|
+
}
|
|
342
|
+
return Promise.resolve([])
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
await copyFiles()
|
|
346
|
+
|
|
347
|
+
// Verify file was removed
|
|
348
|
+
expect(fs.unlink).toHaveBeenCalledWith(path.join(mockCwd, 'old-file.ts'))
|
|
349
|
+
|
|
350
|
+
// Verify directory was checked
|
|
351
|
+
expect(mockReaddir).toHaveBeenCalledWith(mockCwd)
|
|
352
|
+
|
|
353
|
+
// Verify .gitignore was updated
|
|
354
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
355
|
+
path.join(mockCwd, '.gitignore'),
|
|
356
|
+
expect.stringContaining('existing\ngitignore\ncontent'),
|
|
357
|
+
)
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('should handle debug mode', async () => {
|
|
361
|
+
process.env.DEBUG = 'true'
|
|
362
|
+
const fg = jest.requireMock('fast-glob').default as jest.Mock
|
|
363
|
+
fg.mockResolvedValueOnce([]) // First scan for existing files
|
|
364
|
+
.mockResolvedValueOnce(['file.ts']) // Second scan for package files
|
|
365
|
+
;(fs.readFile as jest.Mock).mockImplementation((filePath: string) => {
|
|
366
|
+
if (filePath === path.join('packages/package1/copy', 'file.ts')) {
|
|
367
|
+
return Promise.resolve(Buffer.from('content'))
|
|
368
|
+
}
|
|
369
|
+
if (filePath === path.join(mockCwd, '.gitignore')) {
|
|
370
|
+
return Promise.resolve('existing\ngitignore\ncontent')
|
|
371
|
+
}
|
|
372
|
+
return Promise.reject(new Error(`ENOENT: no such file or directory, open '${filePath}'`))
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
await copyFiles()
|
|
376
|
+
|
|
377
|
+
expect(consoleLog).toHaveBeenCalledWith('[copy-files]', 'Starting copyFiles')
|
|
378
|
+
expect(consoleLog).toHaveBeenCalledWith('[copy-files]', expect.stringContaining('Found'))
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it('should handle unmanaged files', async () => {
|
|
382
|
+
const fg = jest.requireMock('fast-glob').default as jest.Mock
|
|
383
|
+
fg.mockResolvedValueOnce([]) // First scan for existing files
|
|
384
|
+
.mockResolvedValueOnce(['file.ts']) // Second scan for package files
|
|
385
|
+
;(fs.readFile as jest.Mock).mockImplementation((filePath: string) => {
|
|
386
|
+
if (filePath === path.join('packages/package1/copy', 'file.ts')) {
|
|
387
|
+
return Promise.resolve(Buffer.from('content'))
|
|
388
|
+
}
|
|
389
|
+
if (filePath === path.join(mockCwd, 'file.ts')) {
|
|
390
|
+
return Promise.resolve(Buffer.from('unmanaged content'))
|
|
391
|
+
}
|
|
392
|
+
if (filePath === path.join(mockCwd, '.gitignore')) {
|
|
393
|
+
return Promise.resolve('existing\ngitignore\ncontent')
|
|
394
|
+
}
|
|
395
|
+
return Promise.reject(new Error(`ENOENT: no such file or directory, open '${filePath}'`))
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
await copyFiles()
|
|
399
|
+
|
|
400
|
+
expect(consoleLog).toHaveBeenCalledWith(
|
|
401
|
+
expect.stringContaining('Note: File file.ts has been modified'),
|
|
402
|
+
)
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('should cleanup nested empty directories', async () => {
|
|
406
|
+
const fg = jest.requireMock('fast-glob').default as jest.Mock
|
|
407
|
+
fg.mockResolvedValueOnce(['nested/deeply/file.ts']) // First scan finds existing managed file
|
|
408
|
+
.mockResolvedValueOnce([]) // Second scan finds no files in packages
|
|
409
|
+
|
|
410
|
+
// Mock existing managed file
|
|
411
|
+
;(fs.readFile as jest.Mock).mockImplementation((filePath: string) => {
|
|
412
|
+
if (filePath === path.join(mockCwd, 'nested/deeply/file.ts')) {
|
|
413
|
+
return Promise.resolve(Buffer.from('// managed by: graphcommerce\ncontent'))
|
|
414
|
+
}
|
|
415
|
+
if (filePath === path.join(mockCwd, '.gitignore')) {
|
|
416
|
+
return Promise.resolve('existing\ngitignore\ncontent')
|
|
417
|
+
}
|
|
418
|
+
return Promise.reject(new Error(`ENOENT: no such file or directory, open '${filePath}'`))
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
// Mock directory checks to simulate empty directories
|
|
422
|
+
;(fs.readdir as jest.Mock).mockReturnValue(Promise.resolve([]))
|
|
423
|
+
|
|
424
|
+
await copyFiles()
|
|
425
|
+
|
|
426
|
+
// Verify file was removed
|
|
427
|
+
expect(fs.unlink).toHaveBeenCalledWith(path.join(mockCwd, 'nested/deeply/file.ts'))
|
|
428
|
+
|
|
429
|
+
// Verify directories were checked and removed in the correct order
|
|
430
|
+
const readdirCalls = (fs.readdir as jest.Mock).mock.calls.map((call) => call[0])
|
|
431
|
+
const rmdirCalls = (fs.rmdir as jest.Mock).mock.calls.map((call) => call[0])
|
|
432
|
+
|
|
433
|
+
// Both directories should have been checked
|
|
434
|
+
expect(readdirCalls).toContain(path.join(mockCwd, 'nested/deeply'))
|
|
435
|
+
expect(readdirCalls).toContain(path.join(mockCwd, 'nested'))
|
|
436
|
+
|
|
437
|
+
// Both directories should have been removed
|
|
438
|
+
expect(rmdirCalls).toContain(path.join(mockCwd, 'nested/deeply'))
|
|
439
|
+
expect(rmdirCalls).toContain(path.join(mockCwd, 'nested'))
|
|
440
|
+
|
|
441
|
+
// Verify the order: deeply should be processed before nested
|
|
442
|
+
const deeplyReadIndex = readdirCalls.indexOf(path.join(mockCwd, 'nested/deeply'))
|
|
443
|
+
const nestedReadIndex = readdirCalls.indexOf(path.join(mockCwd, 'nested'))
|
|
444
|
+
expect(deeplyReadIndex).toBeLessThan(nestedReadIndex)
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it('should handle partial directory cleanup', async () => {
|
|
448
|
+
const fg = jest.requireMock('fast-glob').default as jest.Mock
|
|
449
|
+
fg.mockResolvedValueOnce(['nested/remove.ts']) // First scan finds existing managed file
|
|
450
|
+
.mockResolvedValueOnce([]) // Second scan finds no files in packages
|
|
451
|
+
|
|
452
|
+
// Mock existing managed file
|
|
453
|
+
;(fs.readFile as jest.Mock).mockImplementation((filePath: string) => {
|
|
454
|
+
if (filePath === path.join(mockCwd, 'nested/remove.ts')) {
|
|
455
|
+
return Promise.resolve(Buffer.from('// managed by: graphcommerce\nremove content'))
|
|
456
|
+
}
|
|
457
|
+
if (filePath === path.join(mockCwd, '.gitignore')) {
|
|
458
|
+
return Promise.resolve('existing\ngitignore\ncontent')
|
|
459
|
+
}
|
|
460
|
+
return Promise.reject(new Error(`ENOENT: no such file or directory, open '${filePath}'`))
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
// Mock directory check to show directory is not empty
|
|
464
|
+
;(fs.readdir as jest.Mock).mockReturnValue(Promise.resolve(['other-file.ts']))
|
|
465
|
+
|
|
466
|
+
await copyFiles()
|
|
467
|
+
|
|
468
|
+
// Verify file removal
|
|
469
|
+
expect(fs.unlink).toHaveBeenCalledTimes(1)
|
|
470
|
+
expect(fs.unlink).toHaveBeenCalledWith(path.join(mockCwd, 'nested/remove.ts'))
|
|
471
|
+
|
|
472
|
+
// Verify directory was checked but not removed (since it's not empty)
|
|
473
|
+
expect(fs.readdir).toHaveBeenCalledWith(path.join(mockCwd, 'nested'))
|
|
474
|
+
expect(fs.rmdir).not.toHaveBeenCalled()
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
it('should handle directory removal errors gracefully', async () => {
|
|
478
|
+
const fg = jest.requireMock('fast-glob').default as jest.Mock
|
|
479
|
+
fg.mockResolvedValueOnce(['nested/file.ts']) // First scan finds existing managed file
|
|
480
|
+
.mockResolvedValueOnce([]) // Second scan finds no files in packages
|
|
481
|
+
|
|
482
|
+
// Mock existing managed file
|
|
483
|
+
;(fs.readFile as jest.Mock).mockImplementation((filePath: string) => {
|
|
484
|
+
if (filePath === path.join(mockCwd, 'nested/file.ts')) {
|
|
485
|
+
return Promise.resolve(Buffer.from('// managed by: graphcommerce\ncontent'))
|
|
486
|
+
}
|
|
487
|
+
if (filePath === path.join(mockCwd, '.gitignore')) {
|
|
488
|
+
return Promise.resolve('existing\ngitignore\ncontent')
|
|
489
|
+
}
|
|
490
|
+
return Promise.reject(new Error(`ENOENT: no such file or directory, open '${filePath}'`))
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
// Mock directory is empty but removal fails with EACCES
|
|
494
|
+
;(fs.readdir as jest.Mock).mockReturnValue(Promise.resolve([]))
|
|
495
|
+
;(fs.rmdir as jest.Mock).mockImplementation(() => {
|
|
496
|
+
const error = new Error('EACCES: permission denied') as NodeJS.ErrnoException
|
|
497
|
+
error.code = 'EACCES'
|
|
498
|
+
return Promise.reject(error)
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
await copyFiles()
|
|
502
|
+
|
|
503
|
+
// Verify file removal still succeeded
|
|
504
|
+
expect(fs.unlink).toHaveBeenCalledWith(path.join(mockCwd, 'nested/file.ts'))
|
|
505
|
+
|
|
506
|
+
// Verify error was logged but didn't crash the process
|
|
507
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
508
|
+
expect.stringContaining('Error cleaning up directory'),
|
|
509
|
+
)
|
|
510
|
+
expect(processExit).toHaveBeenCalledWith(1)
|
|
511
|
+
})
|
|
512
|
+
})
|
|
@@ -35,6 +35,8 @@ exports[`traverses a schema and returns a list of env variables that match 1`] =
|
|
|
35
35
|
"GC_CONFIGURABLE_VARIANT_VALUES_CONTENT",
|
|
36
36
|
"GC_CONFIGURABLE_VARIANT_VALUES_GALLERY",
|
|
37
37
|
"GC_CONFIGURABLE_VARIANT_VALUES_URL",
|
|
38
|
+
"GC_CONTAINER_SIZING_CONTENT",
|
|
39
|
+
"GC_CONTAINER_SIZING_SHELL",
|
|
38
40
|
"GC_CROSS_SELLS_HIDE_CART_ITEMS",
|
|
39
41
|
"GC_CROSS_SELLS_REDIRECT_ITEMS",
|
|
40
42
|
"GC_CUSTOMER_ADDRESS_NOTE_ENABLE",
|
|
@@ -44,6 +46,7 @@ exports[`traverses a schema and returns a list of env variables that match 1`] =
|
|
|
44
46
|
"GC_DATA_LAYER",
|
|
45
47
|
"GC_DATA_LAYER_CORE_WEB_VITALS",
|
|
46
48
|
"GC_DEBUG",
|
|
49
|
+
"GC_DEBUG_CART",
|
|
47
50
|
"GC_DEBUG_PLUGIN_STATUS",
|
|
48
51
|
"GC_DEBUG_SESSIONS",
|
|
49
52
|
"GC_DEBUG_WEBPACK_CIRCULAR_DEPENDENCY_PLUGIN",
|
|
@@ -51,6 +54,9 @@ exports[`traverses a schema and returns a list of env variables that match 1`] =
|
|
|
51
54
|
"GC_DEMO_MODE",
|
|
52
55
|
"GC_ENABLE_GUEST_CHECKOUT_LOGIN",
|
|
53
56
|
"GC_GOOGLE_ANALYTICS_ID",
|
|
57
|
+
"GC_GOOGLE_PLAYSTORE",
|
|
58
|
+
"GC_GOOGLE_PLAYSTORE_PACKAGE_NAME",
|
|
59
|
+
"GC_GOOGLE_PLAYSTORE_SHA256CERTIFICATE_FINGERPRINT",
|
|
54
60
|
"GC_GOOGLE_RECAPTCHA_KEY",
|
|
55
61
|
"GC_GOOGLE_TAGMANAGER_ID",
|
|
56
62
|
"GC_HYGRAPH_ENDPOINT",
|
|
@@ -3,9 +3,9 @@ import {
|
|
|
3
3
|
formatAppliedEnv,
|
|
4
4
|
mergeEnvIntoConfig,
|
|
5
5
|
} from '../../../src/config/utils/mergeEnvIntoConfig'
|
|
6
|
-
import { GraphCommerceConfig
|
|
6
|
+
import type { GraphCommerceConfig } from '../../../src/generated/config'
|
|
7
|
+
import { GraphCommerceConfigSchema } from '../../../src/generated/config'
|
|
7
8
|
import { removeColor } from './rewriteLegancyEnv'
|
|
8
|
-
|
|
9
9
|
const env = {
|
|
10
10
|
GC_ADVANCED_FILTERS: '0',
|
|
11
11
|
GC_DEMO_MODE: '1',
|
|
@@ -17,24 +17,21 @@ const env = {
|
|
|
17
17
|
GC_STOREFRONT_1_LOCALE: 'de',
|
|
18
18
|
GC_STOREFRONT_1_HYGRAPH_LOCALES_0: 'de',
|
|
19
19
|
GC_STOREFRONT_1_MAGENTO_STORE_CODE: 'de_de',
|
|
20
|
-
GC_STOREFRONT:
|
|
20
|
+
GC_STOREFRONT:
|
|
21
|
+
'[{"locale": "en", "defaultLocale": true, "hygraphLocales": ["en"], "magentoStoreCode": "en_us"}]',
|
|
21
22
|
}
|
|
22
|
-
|
|
23
23
|
it('traverses a schema and returns a list of env variables that match', () => {
|
|
24
24
|
const [envSchema] = configToEnvSchema(GraphCommerceConfigSchema())
|
|
25
25
|
expect(Object.keys(envSchema.shape)).toMatchSnapshot()
|
|
26
26
|
})
|
|
27
|
-
|
|
28
27
|
it('parses an env config object', () => {
|
|
29
28
|
const [envSchema] = configToEnvSchema(GraphCommerceConfigSchema())
|
|
30
29
|
const result = envSchema.parse(env)
|
|
31
30
|
expect(result).toMatchSnapshot()
|
|
32
31
|
})
|
|
33
|
-
|
|
34
32
|
it('parses an env string value to a number', () => {
|
|
35
33
|
const [envSchema] = configToEnvSchema(GraphCommerceConfigSchema())
|
|
36
34
|
const result = envSchema.safeParse({ GC_MAGENTO_VERSION: '247' })
|
|
37
|
-
|
|
38
35
|
expect(result.success).toBe(true)
|
|
39
36
|
if (result.success) {
|
|
40
37
|
expect(result.data).toMatchInlineSnapshot(`
|
|
@@ -44,7 +41,6 @@ it('parses an env string value to a number', () => {
|
|
|
44
41
|
`)
|
|
45
42
|
}
|
|
46
43
|
})
|
|
47
|
-
|
|
48
44
|
it('correctly validates if a value is JSON', () => {
|
|
49
45
|
const [envSchema] = configToEnvSchema(GraphCommerceConfigSchema())
|
|
50
46
|
const result = envSchema.safeParse({
|
|
@@ -74,7 +70,6 @@ it('correctly validates if a value is JSON', () => {
|
|
|
74
70
|
`)
|
|
75
71
|
}
|
|
76
72
|
})
|
|
77
|
-
|
|
78
73
|
it('converts an env schema to a config schema', () => {
|
|
79
74
|
const configFile: GraphCommerceConfig = {
|
|
80
75
|
storefront: [{ locale: 'en', hygraphLocales: ['en'], magentoStoreCode: 'en_us' }],
|
|
@@ -90,31 +85,25 @@ it('converts an env schema to a config schema', () => {
|
|
|
90
85
|
searchOnlyApiKey: 'a',
|
|
91
86
|
},
|
|
92
87
|
}
|
|
93
|
-
|
|
94
88
|
const environmentVariables = {
|
|
95
89
|
GC_PRODUCT_FILTERS_PRO: '1',
|
|
96
|
-
GC_STOREFRONT:
|
|
90
|
+
GC_STOREFRONT: '[{"defaultLocale": true }]',
|
|
97
91
|
GC_STOREFRONT_0_LOCALE: 'de',
|
|
98
92
|
}
|
|
99
|
-
|
|
100
93
|
const [mergedConfig, applied] = mergeEnvIntoConfig(
|
|
101
94
|
GraphCommerceConfigSchema(),
|
|
102
95
|
configFile,
|
|
103
96
|
environmentVariables,
|
|
104
97
|
)
|
|
105
|
-
|
|
106
98
|
expect(removeColor(formatAppliedEnv(applied))).toMatchInlineSnapshot(`
|
|
107
99
|
"info - Loaded GraphCommerce env variables
|
|
108
|
-
~ GC_PRODUCT_FILTERS_PRO
|
|
109
|
-
+ GC_STOREFRONT
|
|
110
|
-
~ GC_STOREFRONT_0_LOCALE
|
|
100
|
+
~ GC_PRODUCT_FILTERS_PRO => productFiltersPro
|
|
101
|
+
+ GC_STOREFRONT => storefront
|
|
102
|
+
~ GC_STOREFRONT_0_LOCALE => storefront.[0].locale"
|
|
111
103
|
`)
|
|
112
|
-
|
|
113
|
-
// Validate the resulting configuration
|
|
104
|
+
// Validate the resulting configura
|
|
114
105
|
const parsed = GraphCommerceConfigSchema().safeParse(mergedConfig)
|
|
115
|
-
|
|
116
106
|
expect(parsed.success).toBe(true)
|
|
117
|
-
|
|
118
107
|
if (parsed.success) {
|
|
119
108
|
expect(parsed.data.productFiltersPro).toBe(true)
|
|
120
109
|
expect(parsed.data.storefront[0].defaultLocale).toBe(true)
|