@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 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 - 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().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,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.c063a2a.0",
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.c063a2a.0",
16
- "@friggframework/test": "2.0.0--canary.482.c063a2a.0",
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.c063a2a.0",
39
- "@friggframework/prettier-config": "2.0.0--canary.482.c063a2a.0",
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": "c063a2aaaaae0e4eaac6338f6adc250cf46b9103"
71
+ "gitHead": "411f36e107a200d437a085479c514d7bd9fd981d"
72
72
  }