@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 layer already exists
30
- if (fs.existsSync(layerPath)) {
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
- // Layer doesn't exist - build it automatically
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().mockReturnValue(true);
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().mockReturnValue(true);
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.c063a2a.0",
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.c063a2a.0",
16
- "@friggframework/test": "2.0.0--canary.482.c063a2a.0",
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.c063a2a.0",
39
- "@friggframework/prettier-config": "2.0.0--canary.482.c063a2a.0",
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": "c063a2aaaaae0e4eaac6338f6adc250cf46b9103"
71
+ "gitHead": "7be62db49f55502252f0139a12562832210f8a74"
72
72
  }