@friggframework/devtools 2.0.0--canary.482.c063a2a.0 → 2.0.0--canary.482.7be62db.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.
|
@@ -13,11 +13,16 @@ const { buildPrismaLayer } = require('../../../scripts/build-prisma-layer');
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Ensure Prisma Lambda Layer exists
|
|
16
|
-
*
|
|
16
|
+
*
|
|
17
17
|
* Automatically builds the layer if it doesn't exist.
|
|
18
18
|
* The layer contains ONLY the Prisma runtime client (minimal, ~10-15MB).
|
|
19
19
|
* Prisma CLI is bundled separately in the dbMigrate function.
|
|
20
|
-
*
|
|
20
|
+
*
|
|
21
|
+
* Domain Concept: Build Completion State
|
|
22
|
+
* - Uses .build-complete marker file to track successful builds
|
|
23
|
+
* - Prevents race conditions during concurrent infrastructure composition
|
|
24
|
+
* - Cleans incomplete builds before retry
|
|
25
|
+
*
|
|
21
26
|
* @param {Object} databaseConfig - Database configuration from app definition
|
|
22
27
|
* @returns {Promise<void>}
|
|
23
28
|
* @throws {Error} If layer build fails
|
|
@@ -25,14 +30,33 @@ const { buildPrismaLayer } = require('../../../scripts/build-prisma-layer');
|
|
|
25
30
|
async function ensurePrismaLayerExists(databaseConfig = {}) {
|
|
26
31
|
const projectRoot = process.cwd();
|
|
27
32
|
const layerPath = path.join(projectRoot, 'layers/prisma');
|
|
33
|
+
const completionMarkerPath = path.join(layerPath, '.build-complete');
|
|
28
34
|
|
|
29
|
-
// Check if
|
|
30
|
-
if (fs.existsSync(
|
|
35
|
+
// Check if build is complete (marker exists)
|
|
36
|
+
if (fs.existsSync(completionMarkerPath)) {
|
|
31
37
|
console.log('✓ Prisma Lambda Layer already exists at', layerPath);
|
|
32
38
|
return;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
//
|
|
41
|
+
// Check if incomplete build exists (directory without marker)
|
|
42
|
+
if (fs.existsSync(layerPath)) {
|
|
43
|
+
console.log('⚠ Incomplete Prisma layer detected');
|
|
44
|
+
|
|
45
|
+
// Wait briefly to see if another process completes the build
|
|
46
|
+
console.log('⏳ Checking if concurrent build is in progress...');
|
|
47
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
48
|
+
|
|
49
|
+
// Check if concurrent build completed while we waited
|
|
50
|
+
if (fs.existsSync(completionMarkerPath)) {
|
|
51
|
+
console.log('✓ Concurrent build completed');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Incomplete build still exists - will be cleaned by buildPrismaLayer()
|
|
56
|
+
console.log('⚠ Proceeding with rebuild (buildPrismaLayer will clean incomplete build)');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Build layer
|
|
36
60
|
console.log('📦 Prisma Lambda Layer not found - building automatically...');
|
|
37
61
|
console.log(' Building MINIMAL layer (runtime only, NO CLI)');
|
|
38
62
|
console.log(' CLI is packaged separately in dbMigrate function');
|
|
@@ -41,8 +65,24 @@ async function ensurePrismaLayerExists(databaseConfig = {}) {
|
|
|
41
65
|
try {
|
|
42
66
|
// Build layer WITHOUT CLI (runtime only for minimal size)
|
|
43
67
|
await buildPrismaLayer(databaseConfig);
|
|
68
|
+
|
|
69
|
+
// Create completion marker
|
|
70
|
+
fs.writeFileSync(
|
|
71
|
+
completionMarkerPath,
|
|
72
|
+
`Build completed: ${new Date().toISOString()}\nNode: ${process.version}\nPlatform: ${process.platform}\n`
|
|
73
|
+
);
|
|
74
|
+
|
|
44
75
|
console.log('✓ Prisma Lambda Layer built successfully (~10-15MB)\n');
|
|
45
76
|
} catch (error) {
|
|
77
|
+
// Clean up partial build on failure
|
|
78
|
+
if (fs.existsSync(layerPath)) {
|
|
79
|
+
try {
|
|
80
|
+
fs.rmSync(layerPath, { recursive: true, force: true });
|
|
81
|
+
} catch (cleanupError) {
|
|
82
|
+
console.warn('⚠ Could not clean failed build:', cleanupError.message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
46
86
|
console.error('✗ Failed to build Prisma Lambda Layer:', error.message);
|
|
47
87
|
console.error(' You may need to run: npm install @friggframework/core\n');
|
|
48
88
|
throw error;
|
|
@@ -16,6 +16,33 @@ jest.mock('../../../scripts/build-prisma-layer', () => ({
|
|
|
16
16
|
|
|
17
17
|
const { buildPrismaLayer } = require('../../../scripts/build-prisma-layer');
|
|
18
18
|
|
|
19
|
+
// Helper to mock fs methods for different scenarios
|
|
20
|
+
const mockFs = {
|
|
21
|
+
completedBuild: () => {
|
|
22
|
+
fs.existsSync = jest.fn((path) => {
|
|
23
|
+
if (path.endsWith('.build-complete')) return true;
|
|
24
|
+
if (path.endsWith('layers/prisma')) return true;
|
|
25
|
+
return false;
|
|
26
|
+
});
|
|
27
|
+
fs.writeFileSync = jest.fn();
|
|
28
|
+
fs.rmSync = jest.fn();
|
|
29
|
+
},
|
|
30
|
+
incompleteBuild: () => {
|
|
31
|
+
fs.existsSync = jest.fn((path) => {
|
|
32
|
+
if (path.endsWith('.build-complete')) return false;
|
|
33
|
+
if (path.endsWith('layers/prisma')) return true;
|
|
34
|
+
return false;
|
|
35
|
+
});
|
|
36
|
+
fs.writeFileSync = jest.fn();
|
|
37
|
+
fs.rmSync = jest.fn();
|
|
38
|
+
},
|
|
39
|
+
noBuild: () => {
|
|
40
|
+
fs.existsSync = jest.fn().mockReturnValue(false);
|
|
41
|
+
fs.writeFileSync = jest.fn();
|
|
42
|
+
fs.rmSync = jest.fn();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
19
46
|
describe('Prisma Layer Manager', () => {
|
|
20
47
|
let originalCwd;
|
|
21
48
|
|
|
@@ -31,11 +58,15 @@ describe('Prisma Layer Manager', () => {
|
|
|
31
58
|
|
|
32
59
|
describe('ensurePrismaLayerExists()', () => {
|
|
33
60
|
it('should skip build if layer already exists', async () => {
|
|
34
|
-
fs.existsSync = jest.fn(
|
|
61
|
+
fs.existsSync = jest.fn((path) => {
|
|
62
|
+
// Completion marker exists
|
|
63
|
+
if (path.endsWith('.build-complete')) return true;
|
|
64
|
+
return false;
|
|
65
|
+
});
|
|
35
66
|
|
|
36
67
|
await ensurePrismaLayerExists();
|
|
37
68
|
|
|
38
|
-
expect(fs.existsSync).toHaveBeenCalledWith('/project/layers/prisma');
|
|
69
|
+
expect(fs.existsSync).toHaveBeenCalledWith('/project/layers/prisma/.build-complete');
|
|
39
70
|
expect(buildPrismaLayer).not.toHaveBeenCalled();
|
|
40
71
|
});
|
|
41
72
|
|
|
@@ -79,11 +110,15 @@ describe('Prisma Layer Manager', () => {
|
|
|
79
110
|
|
|
80
111
|
it('should use correct layer path relative to project root', async () => {
|
|
81
112
|
process.cwd = jest.fn().mockReturnValue('/custom/project/path');
|
|
82
|
-
fs.existsSync = jest.fn(
|
|
113
|
+
fs.existsSync = jest.fn((path) => {
|
|
114
|
+
// Completion marker exists
|
|
115
|
+
if (path.endsWith('.build-complete')) return true;
|
|
116
|
+
return false;
|
|
117
|
+
});
|
|
83
118
|
|
|
84
119
|
await ensurePrismaLayerExists();
|
|
85
120
|
|
|
86
|
-
expect(fs.existsSync).toHaveBeenCalledWith('/custom/project/path/layers/prisma');
|
|
121
|
+
expect(fs.existsSync).toHaveBeenCalledWith('/custom/project/path/layers/prisma/.build-complete');
|
|
87
122
|
});
|
|
88
123
|
|
|
89
124
|
it('should log success when layer already exists', async () => {
|
|
@@ -133,6 +168,122 @@ describe('Prisma Layer Manager', () => {
|
|
|
133
168
|
|
|
134
169
|
consoleErrorSpy.mockRestore();
|
|
135
170
|
});
|
|
171
|
+
|
|
172
|
+
describe('Concurrent Build Protection', () => {
|
|
173
|
+
it('should skip build if completion marker exists (TDD)', async () => {
|
|
174
|
+
mockFs.completedBuild();
|
|
175
|
+
|
|
176
|
+
await ensurePrismaLayerExists();
|
|
177
|
+
|
|
178
|
+
expect(fs.existsSync).toHaveBeenCalledWith('/project/layers/prisma/.build-complete');
|
|
179
|
+
expect(buildPrismaLayer).not.toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should wait and rebuild if directory exists without completion marker (TDD)', async () => {
|
|
183
|
+
jest.useFakeTimers();
|
|
184
|
+
mockFs.incompleteBuild();
|
|
185
|
+
buildPrismaLayer.mockResolvedValue();
|
|
186
|
+
|
|
187
|
+
const promise = ensurePrismaLayerExists();
|
|
188
|
+
|
|
189
|
+
// Fast-forward through the wait
|
|
190
|
+
jest.advanceTimersByTime(1000);
|
|
191
|
+
|
|
192
|
+
await promise;
|
|
193
|
+
|
|
194
|
+
// Should NOT manually clean (buildPrismaLayer handles this)
|
|
195
|
+
expect(fs.rmSync).not.toHaveBeenCalled();
|
|
196
|
+
// Should rebuild
|
|
197
|
+
expect(buildPrismaLayer).toHaveBeenCalled();
|
|
198
|
+
// Should create completion marker
|
|
199
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
200
|
+
'/project/layers/prisma/.build-complete',
|
|
201
|
+
expect.any(String)
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
jest.useRealTimers();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should create completion marker after successful build (TDD)', async () => {
|
|
208
|
+
mockFs.noBuild();
|
|
209
|
+
buildPrismaLayer.mockResolvedValue();
|
|
210
|
+
|
|
211
|
+
await ensurePrismaLayerExists();
|
|
212
|
+
|
|
213
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
214
|
+
'/project/layers/prisma/.build-complete',
|
|
215
|
+
expect.any(String)
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should clean up partial build on failure (TDD)', async () => {
|
|
220
|
+
mockFs.noBuild();
|
|
221
|
+
buildPrismaLayer.mockRejectedValue(new Error('Build failed'));
|
|
222
|
+
|
|
223
|
+
// After build attempt fails, directory exists
|
|
224
|
+
fs.existsSync = jest.fn((path) => {
|
|
225
|
+
if (path.endsWith('.build-complete')) return false;
|
|
226
|
+
if (path.endsWith('layers/prisma')) return true;
|
|
227
|
+
return false;
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
await expect(ensurePrismaLayerExists()).rejects.toThrow('Build failed');
|
|
231
|
+
|
|
232
|
+
// Should clean up after failure
|
|
233
|
+
expect(fs.rmSync).toHaveBeenCalledWith(
|
|
234
|
+
'/project/layers/prisma',
|
|
235
|
+
{ recursive: true, force: true }
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should wait for potential concurrent build before rebuilding (TDD)', async () => {
|
|
240
|
+
jest.useFakeTimers();
|
|
241
|
+
mockFs.incompleteBuild();
|
|
242
|
+
buildPrismaLayer.mockResolvedValue();
|
|
243
|
+
|
|
244
|
+
const promise = ensurePrismaLayerExists();
|
|
245
|
+
|
|
246
|
+
// Fast-forward through the wait
|
|
247
|
+
jest.advanceTimersByTime(1000);
|
|
248
|
+
|
|
249
|
+
await promise;
|
|
250
|
+
|
|
251
|
+
// Should wait before rebuilding
|
|
252
|
+
// Should still attempt to build
|
|
253
|
+
expect(buildPrismaLayer).toHaveBeenCalled();
|
|
254
|
+
|
|
255
|
+
jest.useRealTimers();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should detect when concurrent process completes during wait (TDD)', async () => {
|
|
259
|
+
jest.useFakeTimers();
|
|
260
|
+
|
|
261
|
+
// After 1 second, completion marker appears (concurrent process finished)
|
|
262
|
+
let callCount = 0;
|
|
263
|
+
fs.existsSync = jest.fn((path) => {
|
|
264
|
+
callCount++;
|
|
265
|
+
if (path.endsWith('.build-complete')) {
|
|
266
|
+
return callCount > 2; // Appears after setTimeout
|
|
267
|
+
}
|
|
268
|
+
if (path.endsWith('layers/prisma')) return true;
|
|
269
|
+
return false;
|
|
270
|
+
});
|
|
271
|
+
fs.writeFileSync = jest.fn();
|
|
272
|
+
fs.rmSync = jest.fn();
|
|
273
|
+
|
|
274
|
+
const promise = ensurePrismaLayerExists();
|
|
275
|
+
|
|
276
|
+
// Fast-forward through the wait
|
|
277
|
+
jest.advanceTimersByTime(1000);
|
|
278
|
+
|
|
279
|
+
await promise;
|
|
280
|
+
|
|
281
|
+
// Should not rebuild (concurrent process completed)
|
|
282
|
+
expect(buildPrismaLayer).not.toHaveBeenCalled();
|
|
283
|
+
|
|
284
|
+
jest.useRealTimers();
|
|
285
|
+
});
|
|
286
|
+
});
|
|
136
287
|
});
|
|
137
288
|
});
|
|
138
289
|
|
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.7be62db.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.7be62db.0",
|
|
16
|
+
"@friggframework/test": "2.0.0--canary.482.7be62db.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.7be62db.0",
|
|
39
|
+
"@friggframework/prettier-config": "2.0.0--canary.482.7be62db.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": "7be62db49f55502252f0139a12562832210f8a74"
|
|
72
72
|
}
|