@graphcommerce/next-config 9.0.0-canary.99 → 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.
Files changed (69) hide show
  1. package/CHANGELOG.md +219 -1151
  2. package/__tests__/commands/copyFiles.ts +512 -0
  3. package/__tests__/config/utils/__snapshots__/mergeEnvIntoConfig.ts.snap +6 -0
  4. package/__tests__/config/utils/mergeEnvIntoConfig.ts +9 -20
  5. package/__tests__/config/utils/rewriteLegancyEnv.ts +32 -36
  6. package/__tests__/interceptors/findPlugins.ts +76 -78
  7. package/__tests__/interceptors/generateInterceptors.ts +78 -135
  8. package/__tests__/interceptors/parseStructure.ts +2 -2
  9. package/__tests__/utils/resolveDependenciesSync.ts +11 -10
  10. package/dist/commands/codegen.js +18 -0
  11. package/dist/commands/copyFiles.js +292 -0
  12. package/dist/commands/copyRoutes.js +20 -0
  13. package/dist/config/commands/exportConfig.js +1 -1
  14. package/dist/config/commands/generateConfig.js +2 -2
  15. package/dist/config/demoConfig.js +2 -2
  16. package/dist/config/utils/mergeEnvIntoConfig.js +18 -20
  17. package/dist/config/utils/rewriteLegacyEnv.js +2 -2
  18. package/dist/generated/config.js +13 -1
  19. package/dist/index.js +3 -1
  20. package/dist/interceptors/InterceptorPlugin.js +4 -3
  21. package/dist/interceptors/Visitor.js +5 -9
  22. package/dist/interceptors/commands/codegenInterceptors.js +2 -2
  23. package/dist/interceptors/extractExports.js +9 -54
  24. package/dist/interceptors/findOriginalSource.js +2 -1
  25. package/dist/interceptors/findPlugins.js +5 -8
  26. package/dist/interceptors/generateInterceptor.js +12 -10
  27. package/dist/interceptors/generateInterceptors.js +3 -2
  28. package/dist/interceptors/parseStructure.js +1 -1
  29. package/dist/interceptors/writeInterceptors.js +2 -2
  30. package/dist/utils/TopologicalSort.js +4 -0
  31. package/dist/utils/isMonorepo.js +40 -6
  32. package/dist/utils/resolveDependenciesSync.js +9 -2
  33. package/dist/utils/sig.js +34 -0
  34. package/dist/withGraphCommerce.js +3 -2
  35. package/package.json +17 -16
  36. package/src/commands/codegen.ts +18 -0
  37. package/src/commands/copyFiles.ts +328 -0
  38. package/src/config/commands/exportConfig.ts +1 -1
  39. package/src/config/commands/generateConfig.ts +3 -3
  40. package/src/config/demoConfig.ts +5 -5
  41. package/src/config/index.ts +1 -1
  42. package/src/config/utils/exportConfigToEnv.ts +1 -1
  43. package/src/config/utils/mergeEnvIntoConfig.ts +22 -25
  44. package/src/config/utils/replaceConfigInString.ts +1 -1
  45. package/src/config/utils/rewriteLegacyEnv.ts +5 -4
  46. package/src/generated/config.ts +36 -0
  47. package/src/index.ts +6 -5
  48. package/src/interceptors/InterceptorPlugin.ts +10 -7
  49. package/src/interceptors/RenameVisitor.ts +1 -1
  50. package/src/interceptors/Visitor.ts +10 -15
  51. package/src/interceptors/commands/codegenInterceptors.ts +2 -2
  52. package/src/interceptors/extractExports.ts +4 -46
  53. package/src/interceptors/findOriginalSource.ts +5 -4
  54. package/src/interceptors/findPlugins.ts +8 -9
  55. package/src/interceptors/generateInterceptor.ts +15 -12
  56. package/src/interceptors/generateInterceptors.ts +7 -13
  57. package/src/interceptors/parseStructure.ts +4 -4
  58. package/src/interceptors/swc.ts +2 -1
  59. package/src/interceptors/writeInterceptors.ts +3 -3
  60. package/src/utils/TopologicalSort.ts +4 -0
  61. package/src/utils/isMonorepo.ts +46 -5
  62. package/src/utils/packageRoots.ts +1 -1
  63. package/src/utils/resolveDependenciesSync.ts +14 -2
  64. package/src/utils/sig.ts +37 -0
  65. package/src/withGraphCommerce.ts +7 -5
  66. package/dist/config/commands/generateIntercetors.js +0 -9
  67. package/dist/interceptors/commands/generateIntercetors.js +0 -9
  68. package/dist/runtimeCachingOptimizations.js +0 -28
  69. 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, GraphCommerceConfigSchema } from '../../../src/generated/config'
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: `[{"locale": "en", "defaultLocale": true, "hygraphLocales": ["en"], "magentoStoreCode": "en_us"}]`,
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: `[{"defaultLocale": true }]`,
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='1' => productFiltersPro: false => true
109
- + GC_STOREFRONT='[{"defaultLocale": true }]' => storefront: [{"defaultLocale":true}]
110
- ~ GC_STOREFRONT_0_LOCALE='de' => storefront.[0].locale: "en" => "de""
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)