@friggframework/devtools 2.0.0--canary.482.c063a2a.0 → 2.0.0--canary.482.411f36e.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,43 @@ 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 - cleaning...');
|
|
44
|
+
try {
|
|
45
|
+
fs.rmSync(layerPath, { recursive: true, force: true });
|
|
46
|
+
console.log('✓ Cleaned incomplete build');
|
|
47
|
+
} catch (cleanupError) {
|
|
48
|
+
// EBUSY error means another process might be building
|
|
49
|
+
if (cleanupError.code === 'EBUSY' || cleanupError.message.includes('EBUSY')) {
|
|
50
|
+
console.warn('⏳ Could not clean (EBUSY) - waiting for concurrent build...', cleanupError.message);
|
|
51
|
+
|
|
52
|
+
// Wait 1 second and check if concurrent process completed
|
|
53
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
54
|
+
|
|
55
|
+
// Check if concurrent build completed
|
|
56
|
+
if (fs.existsSync(completionMarkerPath)) {
|
|
57
|
+
console.log('✓ Concurrent build completed');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Concurrent build didn't complete, proceed with our build
|
|
62
|
+
console.log('⚠ Concurrent build incomplete, proceeding with rebuild');
|
|
63
|
+
} else {
|
|
64
|
+
throw cleanupError;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Build layer
|
|
36
70
|
console.log('📦 Prisma Lambda Layer not found - building automatically...');
|
|
37
71
|
console.log(' Building MINIMAL layer (runtime only, NO CLI)');
|
|
38
72
|
console.log(' CLI is packaged separately in dbMigrate function');
|
|
@@ -41,8 +75,24 @@ async function ensurePrismaLayerExists(databaseConfig = {}) {
|
|
|
41
75
|
try {
|
|
42
76
|
// Build layer WITHOUT CLI (runtime only for minimal size)
|
|
43
77
|
await buildPrismaLayer(databaseConfig);
|
|
78
|
+
|
|
79
|
+
// Create completion marker
|
|
80
|
+
fs.writeFileSync(
|
|
81
|
+
completionMarkerPath,
|
|
82
|
+
`Build completed: ${new Date().toISOString()}\nNode: ${process.version}\nPlatform: ${process.platform}\n`
|
|
83
|
+
);
|
|
84
|
+
|
|
44
85
|
console.log('✓ Prisma Lambda Layer built successfully (~10-15MB)\n');
|
|
45
86
|
} catch (error) {
|
|
87
|
+
// Clean up partial build on failure
|
|
88
|
+
if (fs.existsSync(layerPath)) {
|
|
89
|
+
try {
|
|
90
|
+
fs.rmSync(layerPath, { recursive: true, force: true });
|
|
91
|
+
} catch (cleanupError) {
|
|
92
|
+
console.warn('⚠ Could not clean failed build:', cleanupError.message);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
46
96
|
console.error('✗ Failed to build Prisma Lambda Layer:', error.message);
|
|
47
97
|
console.error(' You may need to run: npm install @friggframework/core\n');
|
|
48
98
|
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,121 @@ 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 clean and rebuild if directory exists without completion marker (TDD)', async () => {
|
|
183
|
+
mockFs.incompleteBuild();
|
|
184
|
+
buildPrismaLayer.mockResolvedValue();
|
|
185
|
+
|
|
186
|
+
await ensurePrismaLayerExists();
|
|
187
|
+
|
|
188
|
+
// Should clean incomplete build
|
|
189
|
+
expect(fs.rmSync).toHaveBeenCalledWith(
|
|
190
|
+
'/project/layers/prisma',
|
|
191
|
+
{ recursive: true, force: true }
|
|
192
|
+
);
|
|
193
|
+
// Should rebuild
|
|
194
|
+
expect(buildPrismaLayer).toHaveBeenCalled();
|
|
195
|
+
// Should create completion marker
|
|
196
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
197
|
+
'/project/layers/prisma/.build-complete',
|
|
198
|
+
expect.any(String)
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should create completion marker after successful build (TDD)', async () => {
|
|
203
|
+
mockFs.noBuild();
|
|
204
|
+
buildPrismaLayer.mockResolvedValue();
|
|
205
|
+
|
|
206
|
+
await ensurePrismaLayerExists();
|
|
207
|
+
|
|
208
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
209
|
+
'/project/layers/prisma/.build-complete',
|
|
210
|
+
expect.any(String)
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should clean up partial build on failure (TDD)', async () => {
|
|
215
|
+
mockFs.noBuild();
|
|
216
|
+
buildPrismaLayer.mockRejectedValue(new Error('Build failed'));
|
|
217
|
+
fs.existsSync = jest.fn((path) => {
|
|
218
|
+
// After build starts, directory exists
|
|
219
|
+
if (path.endsWith('layers/prisma')) return true;
|
|
220
|
+
return false;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await expect(ensurePrismaLayerExists()).rejects.toThrow('Build failed');
|
|
224
|
+
|
|
225
|
+
expect(fs.rmSync).toHaveBeenCalledWith(
|
|
226
|
+
'/project/layers/prisma',
|
|
227
|
+
{ recursive: true, force: true }
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should handle cleanup errors gracefully and attempt rebuild (TDD)', async () => {
|
|
232
|
+
mockFs.incompleteBuild();
|
|
233
|
+
fs.rmSync = jest.fn().mockImplementation(() => {
|
|
234
|
+
throw new Error('EBUSY: resource busy');
|
|
235
|
+
});
|
|
236
|
+
buildPrismaLayer.mockResolvedValue();
|
|
237
|
+
|
|
238
|
+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
239
|
+
|
|
240
|
+
await ensurePrismaLayerExists();
|
|
241
|
+
|
|
242
|
+
// Should log warning about cleanup failure
|
|
243
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
244
|
+
expect.stringContaining('Could not clean'),
|
|
245
|
+
expect.any(String)
|
|
246
|
+
);
|
|
247
|
+
// Should still attempt to build
|
|
248
|
+
expect(buildPrismaLayer).toHaveBeenCalled();
|
|
249
|
+
|
|
250
|
+
consoleWarnSpy.mockRestore();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should wait and check for concurrent process completion (TDD)', async () => {
|
|
254
|
+
jest.useFakeTimers();
|
|
255
|
+
mockFs.incompleteBuild();
|
|
256
|
+
|
|
257
|
+
// First cleanup fails
|
|
258
|
+
fs.rmSync = jest.fn().mockImplementation(() => {
|
|
259
|
+
throw new Error('EBUSY: resource busy');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// After 1 second, completion marker appears (concurrent process finished)
|
|
263
|
+
let callCount = 0;
|
|
264
|
+
fs.existsSync = jest.fn((path) => {
|
|
265
|
+
callCount++;
|
|
266
|
+
if (path.endsWith('.build-complete')) {
|
|
267
|
+
return callCount > 2; // Appears after setTimeout
|
|
268
|
+
}
|
|
269
|
+
if (path.endsWith('layers/prisma')) return true;
|
|
270
|
+
return false;
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const promise = ensurePrismaLayerExists();
|
|
274
|
+
|
|
275
|
+
// Fast-forward through the wait
|
|
276
|
+
jest.advanceTimersByTime(1000);
|
|
277
|
+
|
|
278
|
+
await promise;
|
|
279
|
+
|
|
280
|
+
// Should not rebuild (concurrent process completed)
|
|
281
|
+
expect(buildPrismaLayer).not.toHaveBeenCalled();
|
|
282
|
+
|
|
283
|
+
jest.useRealTimers();
|
|
284
|
+
});
|
|
285
|
+
});
|
|
136
286
|
});
|
|
137
287
|
});
|
|
138
288
|
|
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.411f36e.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.411f36e.0",
|
|
16
|
+
"@friggframework/test": "2.0.0--canary.482.411f36e.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.411f36e.0",
|
|
39
|
+
"@friggframework/prettier-config": "2.0.0--canary.482.411f36e.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": "411f36e107a200d437a085479c514d7bd9fd981d"
|
|
72
72
|
}
|