@friggframework/devtools 2.0.0--canary.482.7be62db.0 → 2.0.0--canary.482.caa9000.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/infrastructure/domains/shared/utilities/base-definition-factory.js +1 -1
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +79 -15
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +163 -8
- package/layers/prisma/.build-complete +3 -0
- package/package.json +6 -6
|
@@ -224,7 +224,7 @@ function createBaseDefinition(
|
|
|
224
224
|
],
|
|
225
225
|
packager: 'npm',
|
|
226
226
|
keepNames: true,
|
|
227
|
-
keepOutputDirectory:
|
|
227
|
+
keepOutputDirectory: true, // Keep .esbuild directory to prevent ENOENT errors during packaging
|
|
228
228
|
exclude: [
|
|
229
229
|
'aws-sdk',
|
|
230
230
|
'@aws-sdk/*',
|
|
@@ -11,6 +11,21 @@ const path = require('path');
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const { buildPrismaLayer } = require('../../../scripts/build-prisma-layer');
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Check if a process is still running
|
|
16
|
+
* @param {number} pid - Process ID to check
|
|
17
|
+
* @returns {boolean} True if process is running
|
|
18
|
+
*/
|
|
19
|
+
function isProcessRunning(pid) {
|
|
20
|
+
try {
|
|
21
|
+
// Signal 0 checks if process exists without killing it
|
|
22
|
+
process.kill(pid, 0);
|
|
23
|
+
return true;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
/**
|
|
15
30
|
* Ensure Prisma Lambda Layer exists
|
|
16
31
|
*
|
|
@@ -18,10 +33,11 @@ const { buildPrismaLayer } = require('../../../scripts/build-prisma-layer');
|
|
|
18
33
|
* The layer contains ONLY the Prisma runtime client (minimal, ~10-15MB).
|
|
19
34
|
* Prisma CLI is bundled separately in the dbMigrate function.
|
|
20
35
|
*
|
|
21
|
-
* Domain Concept: Build Completion State
|
|
36
|
+
* Domain Concept: Build Completion State & Process Locking
|
|
22
37
|
* - Uses .build-complete marker file to track successful builds
|
|
23
|
-
* -
|
|
24
|
-
* -
|
|
38
|
+
* - Uses .build-lock PID file to prevent concurrent builds
|
|
39
|
+
* - Waits for active builds to complete before starting new one
|
|
40
|
+
* - Cleans stale locks and incomplete builds before retry
|
|
25
41
|
*
|
|
26
42
|
* @param {Object} databaseConfig - Database configuration from app definition
|
|
27
43
|
* @returns {Promise<void>}
|
|
@@ -31,6 +47,7 @@ async function ensurePrismaLayerExists(databaseConfig = {}) {
|
|
|
31
47
|
const projectRoot = process.cwd();
|
|
32
48
|
const layerPath = path.join(projectRoot, 'layers/prisma');
|
|
33
49
|
const completionMarkerPath = path.join(layerPath, '.build-complete');
|
|
50
|
+
const lockFilePath = path.join(layerPath, '.build-lock');
|
|
34
51
|
|
|
35
52
|
// Check if build is complete (marker exists)
|
|
36
53
|
if (fs.existsSync(completionMarkerPath)) {
|
|
@@ -38,22 +55,50 @@ async function ensurePrismaLayerExists(databaseConfig = {}) {
|
|
|
38
55
|
return;
|
|
39
56
|
}
|
|
40
57
|
|
|
41
|
-
// Check
|
|
42
|
-
if (fs.existsSync(
|
|
43
|
-
|
|
58
|
+
// Check for active build process
|
|
59
|
+
if (fs.existsSync(lockFilePath)) {
|
|
60
|
+
const lockPid = parseInt(fs.readFileSync(lockFilePath, 'utf-8').trim(), 10);
|
|
61
|
+
|
|
62
|
+
if (isProcessRunning(lockPid)) {
|
|
63
|
+
console.log(`⏳ Another build process (PID ${lockPid}) is active - waiting...`);
|
|
64
|
+
|
|
65
|
+
// Wait up to 60 seconds for the other process to complete
|
|
66
|
+
for (let i = 0; i < 60; i++) {
|
|
67
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
68
|
+
|
|
69
|
+
// Check if build completed
|
|
70
|
+
if (fs.existsSync(completionMarkerPath)) {
|
|
71
|
+
console.log('✓ Concurrent build completed successfully');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check if process died
|
|
76
|
+
if (!isProcessRunning(lockPid)) {
|
|
77
|
+
console.log(`⚠ Build process ${lockPid} terminated - cleaning up stale lock`);
|
|
78
|
+
fs.rmSync(lockFilePath, { force: true });
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
44
82
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
83
|
+
// Timeout - check one final time
|
|
84
|
+
if (fs.existsSync(completionMarkerPath)) {
|
|
85
|
+
console.log('✓ Concurrent build completed');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
48
88
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
89
|
+
// Still locked after 60s - remove stale lock
|
|
90
|
+
console.log('⚠ Build timeout - removing stale lock and rebuilding');
|
|
91
|
+
fs.rmSync(lockFilePath, { force: true });
|
|
92
|
+
} else {
|
|
93
|
+
// Stale lock file (process not running)
|
|
94
|
+
console.log(`⚠ Stale lock file detected (PID ${lockPid} not running) - cleaning up`);
|
|
95
|
+
fs.rmSync(lockFilePath, { force: true });
|
|
53
96
|
}
|
|
97
|
+
}
|
|
54
98
|
|
|
55
|
-
|
|
56
|
-
|
|
99
|
+
// Check if incomplete build exists (directory without marker)
|
|
100
|
+
if (fs.existsSync(layerPath) && !fs.existsSync(completionMarkerPath)) {
|
|
101
|
+
console.log('⚠ Incomplete Prisma layer detected - will be cleaned by buildPrismaLayer()');
|
|
57
102
|
}
|
|
58
103
|
|
|
59
104
|
// Build layer
|
|
@@ -62,6 +107,16 @@ async function ensurePrismaLayerExists(databaseConfig = {}) {
|
|
|
62
107
|
console.log(' CLI is packaged separately in dbMigrate function');
|
|
63
108
|
console.log(' This may take a minute on first deployment.\n');
|
|
64
109
|
|
|
110
|
+
// Create lock file with current process PID
|
|
111
|
+
try {
|
|
112
|
+
if (!fs.existsSync(layerPath)) {
|
|
113
|
+
fs.mkdirSync(layerPath, { recursive: true });
|
|
114
|
+
}
|
|
115
|
+
fs.writeFileSync(lockFilePath, process.pid.toString());
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.warn('⚠ Could not create lock file:', error.message);
|
|
118
|
+
}
|
|
119
|
+
|
|
65
120
|
try {
|
|
66
121
|
// Build layer WITHOUT CLI (runtime only for minimal size)
|
|
67
122
|
await buildPrismaLayer(databaseConfig);
|
|
@@ -86,6 +141,15 @@ async function ensurePrismaLayerExists(databaseConfig = {}) {
|
|
|
86
141
|
console.error('✗ Failed to build Prisma Lambda Layer:', error.message);
|
|
87
142
|
console.error(' You may need to run: npm install @friggframework/core\n');
|
|
88
143
|
throw error;
|
|
144
|
+
} finally {
|
|
145
|
+
// Always remove lock file when done (success or failure)
|
|
146
|
+
if (fs.existsSync(lockFilePath)) {
|
|
147
|
+
try {
|
|
148
|
+
fs.rmSync(lockFilePath, { force: true });
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.warn('⚠ Could not remove lock file:', error.message);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
89
153
|
}
|
|
90
154
|
}
|
|
91
155
|
|
|
@@ -179,6 +179,144 @@ describe('Prisma Layer Manager', () => {
|
|
|
179
179
|
expect(buildPrismaLayer).not.toHaveBeenCalled();
|
|
180
180
|
});
|
|
181
181
|
|
|
182
|
+
it('should wait for active build process to complete (TDD)', async () => {
|
|
183
|
+
jest.useFakeTimers();
|
|
184
|
+
|
|
185
|
+
// Mock active lock file with running process
|
|
186
|
+
const activePid = 12345;
|
|
187
|
+
let completionMarkerExists = false;
|
|
188
|
+
fs.existsSync = jest.fn((path) => {
|
|
189
|
+
if (path.endsWith('.build-complete')) return completionMarkerExists;
|
|
190
|
+
if (path.endsWith('.build-lock')) return true;
|
|
191
|
+
if (path.endsWith('layers/prisma')) return true;
|
|
192
|
+
return false;
|
|
193
|
+
});
|
|
194
|
+
fs.readFileSync = jest.fn().mockReturnValue(activePid.toString());
|
|
195
|
+
fs.writeFileSync = jest.fn();
|
|
196
|
+
fs.rmSync = jest.fn();
|
|
197
|
+
|
|
198
|
+
// Mock process.kill to simulate running process, then completion
|
|
199
|
+
let killCallCount = 0;
|
|
200
|
+
const originalKill = process.kill;
|
|
201
|
+
process.kill = jest.fn((pid, signal) => {
|
|
202
|
+
killCallCount++;
|
|
203
|
+
if (killCallCount >= 3) {
|
|
204
|
+
// After 3 seconds, build completes
|
|
205
|
+
completionMarkerExists = true;
|
|
206
|
+
}
|
|
207
|
+
return true; // Process is running
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const promise = ensurePrismaLayerExists();
|
|
211
|
+
|
|
212
|
+
// Fast-forward through the wait loop
|
|
213
|
+
for (let i = 0; i < 5; i++) {
|
|
214
|
+
jest.advanceTimersByTime(1000);
|
|
215
|
+
await Promise.resolve();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await promise;
|
|
219
|
+
|
|
220
|
+
// Should not rebuild (waited for concurrent build)
|
|
221
|
+
expect(buildPrismaLayer).not.toHaveBeenCalled();
|
|
222
|
+
|
|
223
|
+
process.kill = originalKill;
|
|
224
|
+
jest.useRealTimers();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should clean stale lock file if process not running (TDD)', async () => {
|
|
228
|
+
// Mock stale lock file (process not running)
|
|
229
|
+
fs.existsSync = jest.fn((path) => {
|
|
230
|
+
if (path.endsWith('.build-complete')) return false;
|
|
231
|
+
if (path.endsWith('.build-lock')) return true;
|
|
232
|
+
if (path.endsWith('layers/prisma')) return false;
|
|
233
|
+
return false;
|
|
234
|
+
});
|
|
235
|
+
fs.readFileSync = jest.fn().mockReturnValue('99999');
|
|
236
|
+
fs.writeFileSync = jest.fn();
|
|
237
|
+
fs.mkdirSync = jest.fn();
|
|
238
|
+
fs.rmSync = jest.fn();
|
|
239
|
+
|
|
240
|
+
// Mock process.kill to throw (process not running)
|
|
241
|
+
const originalKill = process.kill;
|
|
242
|
+
process.kill = jest.fn(() => {
|
|
243
|
+
throw new Error('ESRCH');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
buildPrismaLayer.mockResolvedValue();
|
|
247
|
+
|
|
248
|
+
await ensurePrismaLayerExists();
|
|
249
|
+
|
|
250
|
+
// Should remove stale lock
|
|
251
|
+
expect(fs.rmSync).toHaveBeenCalledWith('/project/layers/prisma/.build-lock', { force: true });
|
|
252
|
+
// Should proceed with build
|
|
253
|
+
expect(buildPrismaLayer).toHaveBeenCalled();
|
|
254
|
+
|
|
255
|
+
process.kill = originalKill;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should create and remove lock file during build (TDD)', async () => {
|
|
259
|
+
// Mock to simulate successful build flow
|
|
260
|
+
fs.existsSync = jest.fn((path) => {
|
|
261
|
+
// Completion marker doesn't exist initially
|
|
262
|
+
if (path.endsWith('.build-complete')) return false;
|
|
263
|
+
// Lock file exists in finally block (after writeFileSync)
|
|
264
|
+
if (path.endsWith('.build-lock')) return true;
|
|
265
|
+
// Directory doesn't exist initially
|
|
266
|
+
if (path.endsWith('layers/prisma')) return false;
|
|
267
|
+
return false;
|
|
268
|
+
});
|
|
269
|
+
fs.writeFileSync = jest.fn();
|
|
270
|
+
fs.mkdirSync = jest.fn();
|
|
271
|
+
fs.rmSync = jest.fn();
|
|
272
|
+
buildPrismaLayer.mockResolvedValue();
|
|
273
|
+
|
|
274
|
+
await ensurePrismaLayerExists();
|
|
275
|
+
|
|
276
|
+
// Should create directory for lock file
|
|
277
|
+
expect(fs.mkdirSync).toHaveBeenCalledWith('/project/layers/prisma', { recursive: true });
|
|
278
|
+
|
|
279
|
+
// Should create lock file with PID
|
|
280
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
281
|
+
'/project/layers/prisma/.build-lock',
|
|
282
|
+
expect.any(String)
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Should create completion marker
|
|
286
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
287
|
+
'/project/layers/prisma/.build-complete',
|
|
288
|
+
expect.any(String)
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Should remove lock file in finally block
|
|
292
|
+
expect(fs.rmSync).toHaveBeenCalledWith(
|
|
293
|
+
'/project/layers/prisma/.build-lock',
|
|
294
|
+
{ force: true }
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should remove lock file even if build fails (TDD)', async () => {
|
|
299
|
+
mockFs.noBuild();
|
|
300
|
+
fs.mkdirSync = jest.fn();
|
|
301
|
+
buildPrismaLayer.mockRejectedValue(new Error('Build failed'));
|
|
302
|
+
|
|
303
|
+
// After failure, directory exists
|
|
304
|
+
fs.existsSync = jest.fn((path) => {
|
|
305
|
+
if (path.endsWith('.build-complete')) return false;
|
|
306
|
+
if (path.endsWith('.build-lock')) return true;
|
|
307
|
+
if (path.endsWith('layers/prisma')) return true;
|
|
308
|
+
return false;
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
await expect(ensurePrismaLayerExists()).rejects.toThrow('Build failed');
|
|
312
|
+
|
|
313
|
+
// Should remove lock file in finally block
|
|
314
|
+
expect(fs.rmSync).toHaveBeenCalledWith(
|
|
315
|
+
'/project/layers/prisma/.build-lock',
|
|
316
|
+
{ force: true }
|
|
317
|
+
);
|
|
318
|
+
});
|
|
319
|
+
|
|
182
320
|
it('should wait and rebuild if directory exists without completion marker (TDD)', async () => {
|
|
183
321
|
jest.useFakeTimers();
|
|
184
322
|
mockFs.incompleteBuild();
|
|
@@ -258,29 +396,46 @@ describe('Prisma Layer Manager', () => {
|
|
|
258
396
|
it('should detect when concurrent process completes during wait (TDD)', async () => {
|
|
259
397
|
jest.useFakeTimers();
|
|
260
398
|
|
|
261
|
-
//
|
|
262
|
-
let
|
|
399
|
+
// Simulate lock file existing with completion happening during wait
|
|
400
|
+
let completionMarkerExists = false;
|
|
401
|
+
let lockFileExists = true;
|
|
263
402
|
fs.existsSync = jest.fn((path) => {
|
|
264
|
-
|
|
265
|
-
if (path.endsWith('.build-
|
|
266
|
-
return callCount > 2; // Appears after setTimeout
|
|
267
|
-
}
|
|
403
|
+
if (path.endsWith('.build-complete')) return completionMarkerExists;
|
|
404
|
+
if (path.endsWith('.build-lock')) return lockFileExists;
|
|
268
405
|
if (path.endsWith('layers/prisma')) return true;
|
|
269
406
|
return false;
|
|
270
407
|
});
|
|
408
|
+
fs.readFileSync = jest.fn().mockReturnValue('12345');
|
|
271
409
|
fs.writeFileSync = jest.fn();
|
|
272
410
|
fs.rmSync = jest.fn();
|
|
273
411
|
|
|
412
|
+
// Mock process.kill to simulate active process
|
|
413
|
+
let killCallCount = 0;
|
|
414
|
+
const originalKill = process.kill;
|
|
415
|
+
process.kill = jest.fn((pid, signal) => {
|
|
416
|
+
killCallCount++;
|
|
417
|
+
if (killCallCount >= 3) {
|
|
418
|
+
// After 3 checks, build completes
|
|
419
|
+
completionMarkerExists = true;
|
|
420
|
+
lockFileExists = false;
|
|
421
|
+
}
|
|
422
|
+
return true; // Process is running
|
|
423
|
+
});
|
|
424
|
+
|
|
274
425
|
const promise = ensurePrismaLayerExists();
|
|
275
426
|
|
|
276
|
-
// Fast-forward through the wait
|
|
277
|
-
|
|
427
|
+
// Fast-forward through the wait loop
|
|
428
|
+
for (let i = 0; i < 5; i++) {
|
|
429
|
+
jest.advanceTimersByTime(1000);
|
|
430
|
+
await Promise.resolve();
|
|
431
|
+
}
|
|
278
432
|
|
|
279
433
|
await promise;
|
|
280
434
|
|
|
281
435
|
// Should not rebuild (concurrent process completed)
|
|
282
436
|
expect(buildPrismaLayer).not.toHaveBeenCalled();
|
|
283
437
|
|
|
438
|
+
process.kill = originalKill;
|
|
284
439
|
jest.useRealTimers();
|
|
285
440
|
});
|
|
286
441
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/devtools",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.482.
|
|
4
|
+
"version": "2.0.0--canary.482.caa9000.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-ec2": "^3.835.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.835.0",
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"@babel/eslint-parser": "^7.18.9",
|
|
13
13
|
"@babel/parser": "^7.25.3",
|
|
14
14
|
"@babel/traverse": "^7.25.3",
|
|
15
|
-
"@friggframework/schemas": "2.0.0--canary.482.
|
|
16
|
-
"@friggframework/test": "2.0.0--canary.482.
|
|
15
|
+
"@friggframework/schemas": "2.0.0--canary.482.caa9000.0",
|
|
16
|
+
"@friggframework/test": "2.0.0--canary.482.caa9000.0",
|
|
17
17
|
"@hapi/boom": "^10.0.1",
|
|
18
18
|
"@inquirer/prompts": "^5.3.8",
|
|
19
19
|
"axios": "^1.7.2",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"serverless-http": "^2.7.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@friggframework/eslint-config": "2.0.0--canary.482.
|
|
39
|
-
"@friggframework/prettier-config": "2.0.0--canary.482.
|
|
38
|
+
"@friggframework/eslint-config": "2.0.0--canary.482.caa9000.0",
|
|
39
|
+
"@friggframework/prettier-config": "2.0.0--canary.482.caa9000.0",
|
|
40
40
|
"aws-sdk-client-mock": "^4.1.0",
|
|
41
41
|
"aws-sdk-client-mock-jest": "^4.1.0",
|
|
42
42
|
"jest": "^30.1.3",
|
|
@@ -68,5 +68,5 @@
|
|
|
68
68
|
"publishConfig": {
|
|
69
69
|
"access": "public"
|
|
70
70
|
},
|
|
71
|
-
"gitHead": "
|
|
71
|
+
"gitHead": "caa90009460831fdb4685c012c2cdf11b1df78de"
|
|
72
72
|
}
|