@friggframework/devtools 2.0.0--canary.474.edb48ba.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.
@@ -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
 
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Plugin Validator
3
+ *
4
+ * Validation Layer - Hexagonal Architecture
5
+ *
6
+ * Validates serverless plugin configuration to detect conflicts and provide
7
+ * migration guidance for packaging plugins (serverless-esbuild vs serverless-jetpack).
8
+ */
9
+
10
+ /**
11
+ * Detect conflicting packaging plugins in serverless configuration
12
+ *
13
+ * @param {Array<string>} plugins - List of serverless plugins
14
+ * @returns {Object} Validation result with conflict detection
15
+ */
16
+ function detectConflictingPlugins(plugins = []) {
17
+ const hasEsbuild = plugins.includes('serverless-esbuild');
18
+ const hasJetpack = plugins.includes('serverless-jetpack');
19
+
20
+ const result = {
21
+ hasConflict: false,
22
+ hasEsbuild,
23
+ hasJetpack,
24
+ warnings: [],
25
+ recommendations: [],
26
+ };
27
+
28
+ // Check for explicit conflict - both plugins present
29
+ if (hasEsbuild && hasJetpack) {
30
+ result.hasConflict = true;
31
+ result.warnings.push(
32
+ 'Both serverless-esbuild and serverless-jetpack are configured. ' +
33
+ 'These plugins have overlapping functionality for Lambda packaging.'
34
+ );
35
+ result.recommendations.push(
36
+ 'Remove serverless-jetpack from your serverless.yml plugins array.',
37
+ 'The Frigg framework now uses serverless-esbuild as the standard bundling solution.',
38
+ 'See docs/reference/aws-sdk-v3-osls-migration.md for migration guidance.'
39
+ );
40
+ }
41
+
42
+ // Check for legacy jetpack usage (jetpack without esbuild)
43
+ if (hasJetpack && !hasEsbuild) {
44
+ result.warnings.push(
45
+ 'serverless-jetpack is a legacy packaging plugin. ' +
46
+ 'Frigg framework now recommends serverless-esbuild for better performance and compatibility.'
47
+ );
48
+ result.recommendations.push(
49
+ 'Consider migrating to serverless-esbuild for improved build times and tree-shaking.',
50
+ 'Update your serverless.yml to use serverless-esbuild instead of serverless-jetpack.',
51
+ 'See docs/reference/aws-sdk-v3-osls-migration.md for migration guidance.'
52
+ );
53
+ }
54
+
55
+ // Validate esbuild is present (standard for Frigg)
56
+ if (!hasEsbuild && !hasJetpack) {
57
+ result.warnings.push(
58
+ 'No packaging plugin detected. Serverless will use default packaging which may be slow.'
59
+ );
60
+ result.recommendations.push(
61
+ 'Add serverless-esbuild to your serverless.yml plugins array for optimized Lambda bundling.'
62
+ );
63
+ }
64
+
65
+ return result;
66
+ }
67
+
68
+ /**
69
+ * Validate and clean plugin list by removing conflicts
70
+ *
71
+ * Automatically removes serverless-jetpack if serverless-esbuild is present.
72
+ * This provides automatic migration for users with legacy configurations.
73
+ *
74
+ * @param {Array<string>} plugins - List of serverless plugins
75
+ * @param {Object} options - Validation options
76
+ * @param {boolean} options.autoFix - Automatically fix conflicts by removing jetpack (default: true)
77
+ * @param {boolean} options.silent - Suppress console warnings (default: false)
78
+ * @returns {Object} Result with cleaned plugins and validation info
79
+ */
80
+ function validateAndCleanPlugins(plugins = [], options = {}) {
81
+ const { autoFix = true, silent = false } = options;
82
+ const validation = detectConflictingPlugins(plugins);
83
+
84
+ let cleanedPlugins = [...plugins];
85
+ let modified = false;
86
+
87
+ // Auto-fix: Remove jetpack if esbuild is present
88
+ if (validation.hasConflict && autoFix) {
89
+ cleanedPlugins = plugins.filter(p => p !== 'serverless-jetpack');
90
+ modified = true;
91
+
92
+ if (!silent) {
93
+ console.warn('\n⚠️ Plugin Conflict Detected and Auto-Fixed:');
94
+ console.warn(' Removed serverless-jetpack (using serverless-esbuild instead)');
95
+ console.warn(' The Frigg framework uses serverless-esbuild as the standard bundling solution.\n');
96
+ }
97
+ }
98
+
99
+ // Show warnings for other cases
100
+ if (!silent && validation.warnings.length > 0 && !modified) {
101
+ console.warn('\n⚠️ Plugin Configuration Warning:');
102
+ validation.warnings.forEach(warning => {
103
+ console.warn(` ${warning}`);
104
+ });
105
+
106
+ if (validation.recommendations.length > 0) {
107
+ console.warn('\n💡 Recommendations:');
108
+ validation.recommendations.forEach(rec => {
109
+ console.warn(` • ${rec}`);
110
+ });
111
+ console.warn('');
112
+ }
113
+ }
114
+
115
+ return {
116
+ plugins: cleanedPlugins,
117
+ modified,
118
+ validation,
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Check if a serverless definition has proper packaging configuration
124
+ *
125
+ * @param {Object} serverlessDefinition - Serverless framework definition
126
+ * @returns {Object} Validation result
127
+ */
128
+ function validatePackagingConfiguration(serverlessDefinition) {
129
+ const plugins = serverlessDefinition?.plugins || [];
130
+ const custom = serverlessDefinition?.custom || {};
131
+
132
+ const result = {
133
+ valid: true,
134
+ errors: [],
135
+ warnings: [],
136
+ };
137
+
138
+ const hasEsbuild = plugins.includes('serverless-esbuild');
139
+ const hasEsbuildConfig = custom.esbuild !== undefined;
140
+
141
+ // If esbuild plugin is present, ensure it's configured
142
+ if (hasEsbuild && !hasEsbuildConfig) {
143
+ result.warnings.push(
144
+ 'serverless-esbuild plugin is present but custom.esbuild configuration is missing. ' +
145
+ 'Using default esbuild settings.'
146
+ );
147
+ }
148
+
149
+ // Check for external dependencies in esbuild config
150
+ if (hasEsbuildConfig && hasEsbuild) {
151
+ const external = custom.esbuild.external || [];
152
+
153
+ // Validate that AWS SDK and Prisma are externalized
154
+ const hasAwsSdkExternal = external.some(e =>
155
+ e === '@aws-sdk/*' || e === 'aws-sdk' || e.startsWith('@aws-sdk/')
156
+ );
157
+ const hasPrismaExternal = external.some(e =>
158
+ e === '@prisma/client' || e === 'prisma' || e.startsWith('.prisma')
159
+ );
160
+
161
+ if (!hasAwsSdkExternal) {
162
+ result.warnings.push(
163
+ 'AWS SDK is not externalized in esbuild config. ' +
164
+ 'Consider adding "@aws-sdk/*" to external array to reduce bundle size.'
165
+ );
166
+ }
167
+
168
+ if (!hasPrismaExternal) {
169
+ result.warnings.push(
170
+ 'Prisma is not externalized in esbuild config. ' +
171
+ 'Consider adding "@prisma/client" to external array since it\'s provided via Lambda Layer.'
172
+ );
173
+ }
174
+ }
175
+
176
+ if (result.errors.length > 0) {
177
+ result.valid = false;
178
+ }
179
+
180
+ return result;
181
+ }
182
+
183
+ module.exports = {
184
+ detectConflictingPlugins,
185
+ validateAndCleanPlugins,
186
+ validatePackagingConfiguration,
187
+ };
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Plugin Validator Tests
3
+ *
4
+ * Test suite following TDD best practices for plugin conflict detection
5
+ */
6
+
7
+ const {
8
+ detectConflictingPlugins,
9
+ validateAndCleanPlugins,
10
+ validatePackagingConfiguration,
11
+ } = require('./plugin-validator');
12
+
13
+ describe('Plugin Validator', () => {
14
+ describe('detectConflictingPlugins', () => {
15
+ it('should detect conflict when both esbuild and jetpack are present', () => {
16
+ const plugins = ['serverless-esbuild', 'serverless-jetpack'];
17
+ const result = detectConflictingPlugins(plugins);
18
+
19
+ expect(result.hasConflict).toBe(true);
20
+ expect(result.hasEsbuild).toBe(true);
21
+ expect(result.hasJetpack).toBe(true);
22
+ expect(result.warnings).toHaveLength(1);
23
+ expect(result.warnings[0]).toContain('overlapping functionality');
24
+ expect(result.recommendations).toHaveLength(3);
25
+ });
26
+
27
+ it('should not detect conflict when only esbuild is present', () => {
28
+ const plugins = ['serverless-esbuild'];
29
+ const result = detectConflictingPlugins(plugins);
30
+
31
+ expect(result.hasConflict).toBe(false);
32
+ expect(result.hasEsbuild).toBe(true);
33
+ expect(result.hasJetpack).toBe(false);
34
+ expect(result.warnings).toHaveLength(0);
35
+ });
36
+
37
+ it('should warn about legacy jetpack usage', () => {
38
+ const plugins = ['serverless-jetpack'];
39
+ const result = detectConflictingPlugins(plugins);
40
+
41
+ expect(result.hasConflict).toBe(false);
42
+ expect(result.hasEsbuild).toBe(false);
43
+ expect(result.hasJetpack).toBe(true);
44
+ expect(result.warnings).toHaveLength(1);
45
+ expect(result.warnings[0]).toContain('legacy packaging plugin');
46
+ expect(result.recommendations.length).toBeGreaterThan(0);
47
+ });
48
+
49
+ it('should warn when no packaging plugin is present', () => {
50
+ const plugins = ['serverless-offline'];
51
+ const result = detectConflictingPlugins(plugins);
52
+
53
+ expect(result.hasConflict).toBe(false);
54
+ expect(result.hasEsbuild).toBe(false);
55
+ expect(result.hasJetpack).toBe(false);
56
+ expect(result.warnings).toHaveLength(1);
57
+ expect(result.warnings[0]).toContain('No packaging plugin detected');
58
+ });
59
+
60
+ it('should handle empty plugin array', () => {
61
+ const result = detectConflictingPlugins([]);
62
+
63
+ expect(result.hasConflict).toBe(false);
64
+ expect(result.hasEsbuild).toBe(false);
65
+ expect(result.hasJetpack).toBe(false);
66
+ });
67
+
68
+ it('should handle undefined plugin array', () => {
69
+ const result = detectConflictingPlugins();
70
+
71
+ expect(result.hasConflict).toBe(false);
72
+ expect(result.hasEsbuild).toBe(false);
73
+ expect(result.hasJetpack).toBe(false);
74
+ });
75
+ });
76
+
77
+ describe('validateAndCleanPlugins', () => {
78
+ it('should auto-fix conflict by removing jetpack when autoFix is true', () => {
79
+ const plugins = ['serverless-esbuild', 'serverless-jetpack', 'serverless-offline'];
80
+ const result = validateAndCleanPlugins(plugins, { autoFix: true, silent: true });
81
+
82
+ expect(result.modified).toBe(true);
83
+ expect(result.plugins).toEqual(['serverless-esbuild', 'serverless-offline']);
84
+ expect(result.plugins).not.toContain('serverless-jetpack');
85
+ expect(result.validation.hasConflict).toBe(true);
86
+ });
87
+
88
+ it('should not modify plugins when autoFix is false', () => {
89
+ const plugins = ['serverless-esbuild', 'serverless-jetpack'];
90
+ const result = validateAndCleanPlugins(plugins, { autoFix: false, silent: true });
91
+
92
+ expect(result.modified).toBe(false);
93
+ expect(result.plugins).toEqual(plugins);
94
+ expect(result.validation.hasConflict).toBe(true);
95
+ });
96
+
97
+ it('should not modify plugins when no conflict exists', () => {
98
+ const plugins = ['serverless-esbuild', 'serverless-offline'];
99
+ const result = validateAndCleanPlugins(plugins, { autoFix: true, silent: true });
100
+
101
+ expect(result.modified).toBe(false);
102
+ expect(result.plugins).toEqual(plugins);
103
+ expect(result.validation.hasConflict).toBe(false);
104
+ });
105
+
106
+ it('should preserve order of non-conflicting plugins', () => {
107
+ const plugins = [
108
+ 'serverless-offline',
109
+ 'serverless-esbuild',
110
+ 'serverless-jetpack',
111
+ '@friggframework/serverless-plugin',
112
+ ];
113
+ const result = validateAndCleanPlugins(plugins, { autoFix: true, silent: true });
114
+
115
+ expect(result.plugins).toEqual([
116
+ 'serverless-offline',
117
+ 'serverless-esbuild',
118
+ '@friggframework/serverless-plugin',
119
+ ]);
120
+ });
121
+
122
+ it('should handle empty plugin array', () => {
123
+ const result = validateAndCleanPlugins([], { autoFix: true, silent: true });
124
+
125
+ expect(result.modified).toBe(false);
126
+ expect(result.plugins).toEqual([]);
127
+ });
128
+
129
+ it('should use default options when none provided', () => {
130
+ const plugins = ['serverless-esbuild', 'serverless-jetpack'];
131
+
132
+ // Suppress console output for test
133
+ const originalWarn = console.warn;
134
+ console.warn = jest.fn();
135
+
136
+ const result = validateAndCleanPlugins(plugins);
137
+
138
+ expect(result.modified).toBe(true);
139
+ expect(result.plugins).not.toContain('serverless-jetpack');
140
+ expect(console.warn).toHaveBeenCalled();
141
+
142
+ console.warn = originalWarn;
143
+ });
144
+
145
+ it('should not log warnings when silent is true', () => {
146
+ const plugins = ['serverless-esbuild', 'serverless-jetpack'];
147
+ const originalWarn = console.warn;
148
+ console.warn = jest.fn();
149
+
150
+ validateAndCleanPlugins(plugins, { autoFix: true, silent: true });
151
+
152
+ expect(console.warn).not.toHaveBeenCalled();
153
+
154
+ console.warn = originalWarn;
155
+ });
156
+ });
157
+
158
+ describe('validatePackagingConfiguration', () => {
159
+ it('should validate a properly configured esbuild setup', () => {
160
+ const serverlessDefinition = {
161
+ plugins: ['serverless-esbuild'],
162
+ custom: {
163
+ esbuild: {
164
+ bundle: true,
165
+ external: ['@aws-sdk/*', '@prisma/client'],
166
+ },
167
+ },
168
+ };
169
+
170
+ const result = validatePackagingConfiguration(serverlessDefinition);
171
+
172
+ expect(result.valid).toBe(true);
173
+ expect(result.errors).toHaveLength(0);
174
+ expect(result.warnings).toHaveLength(0);
175
+ });
176
+
177
+ it('should warn when esbuild plugin is present but config is missing', () => {
178
+ const serverlessDefinition = {
179
+ plugins: ['serverless-esbuild'],
180
+ custom: {},
181
+ };
182
+
183
+ const result = validatePackagingConfiguration(serverlessDefinition);
184
+
185
+ expect(result.valid).toBe(true);
186
+ expect(result.warnings).toHaveLength(1);
187
+ expect(result.warnings[0]).toContain('custom.esbuild configuration is missing');
188
+ });
189
+
190
+ it('should warn when AWS SDK is not externalized', () => {
191
+ const serverlessDefinition = {
192
+ plugins: ['serverless-esbuild'],
193
+ custom: {
194
+ esbuild: {
195
+ bundle: true,
196
+ external: ['@prisma/client'],
197
+ },
198
+ },
199
+ };
200
+
201
+ const result = validatePackagingConfiguration(serverlessDefinition);
202
+
203
+ expect(result.valid).toBe(true);
204
+ expect(result.warnings.some(w => w.includes('AWS SDK'))).toBe(true);
205
+ });
206
+
207
+ it('should warn when Prisma is not externalized', () => {
208
+ const serverlessDefinition = {
209
+ plugins: ['serverless-esbuild'],
210
+ custom: {
211
+ esbuild: {
212
+ bundle: true,
213
+ external: ['@aws-sdk/*'],
214
+ },
215
+ },
216
+ };
217
+
218
+ const result = validatePackagingConfiguration(serverlessDefinition);
219
+
220
+ expect(result.valid).toBe(true);
221
+ expect(result.warnings.some(w => w.includes('Prisma'))).toBe(true);
222
+ });
223
+
224
+ it('should accept aws-sdk as valid AWS SDK externalization', () => {
225
+ const serverlessDefinition = {
226
+ plugins: ['serverless-esbuild'],
227
+ custom: {
228
+ esbuild: {
229
+ external: ['aws-sdk', '@prisma/client'],
230
+ },
231
+ },
232
+ };
233
+
234
+ const result = validatePackagingConfiguration(serverlessDefinition);
235
+
236
+ expect(result.warnings.some(w => w.includes('AWS SDK'))).toBe(false);
237
+ });
238
+
239
+ it('should accept .prisma/* as valid Prisma externalization', () => {
240
+ const serverlessDefinition = {
241
+ plugins: ['serverless-esbuild'],
242
+ custom: {
243
+ esbuild: {
244
+ external: ['@aws-sdk/*', '.prisma/*'],
245
+ },
246
+ },
247
+ };
248
+
249
+ const result = validatePackagingConfiguration(serverlessDefinition);
250
+
251
+ expect(result.warnings.some(w => w.includes('Prisma'))).toBe(false);
252
+ });
253
+
254
+ it('should handle serverless definition without plugins', () => {
255
+ const serverlessDefinition = {
256
+ custom: {},
257
+ };
258
+
259
+ const result = validatePackagingConfiguration(serverlessDefinition);
260
+
261
+ expect(result.valid).toBe(true);
262
+ });
263
+
264
+ it('should handle empty serverless definition', () => {
265
+ const result = validatePackagingConfiguration({});
266
+
267
+ expect(result.valid).toBe(true);
268
+ });
269
+ });
270
+
271
+ describe('Integration Tests', () => {
272
+ it('should handle complete migration scenario from jetpack to esbuild', () => {
273
+ const plugins = [
274
+ 'serverless-offline',
275
+ 'serverless-jetpack',
276
+ 'serverless-esbuild',
277
+ '@friggframework/serverless-plugin',
278
+ ];
279
+
280
+ const result = validateAndCleanPlugins(plugins, { autoFix: true, silent: true });
281
+
282
+ expect(result.modified).toBe(true);
283
+ expect(result.plugins).toEqual([
284
+ 'serverless-offline',
285
+ 'serverless-esbuild',
286
+ '@friggframework/serverless-plugin',
287
+ ]);
288
+ expect(result.validation.hasConflict).toBe(true);
289
+ expect(result.validation.hasEsbuild).toBe(true);
290
+ expect(result.validation.hasJetpack).toBe(true);
291
+ });
292
+
293
+ it('should provide helpful recommendations for legacy configurations', () => {
294
+ const plugins = ['serverless-jetpack'];
295
+ const result = validateAndCleanPlugins(plugins, { autoFix: false, silent: true });
296
+
297
+ expect(result.validation.recommendations).toContain(
298
+ 'Consider migrating to serverless-esbuild for improved build times and tree-shaking.'
299
+ );
300
+ expect(
301
+ result.validation.recommendations.some(r => r.includes('migration guidance'))
302
+ ).toBe(true);
303
+ });
304
+
305
+ it('should work with Frigg standard plugin configuration', () => {
306
+ const standardPlugins = [
307
+ 'serverless-esbuild',
308
+ 'serverless-offline-sqs',
309
+ 'serverless-offline',
310
+ '@friggframework/serverless-plugin',
311
+ ];
312
+
313
+ const result = validateAndCleanPlugins(standardPlugins, {
314
+ autoFix: true,
315
+ silent: true,
316
+ });
317
+
318
+ expect(result.modified).toBe(false);
319
+ expect(result.plugins).toEqual(standardPlugins);
320
+ expect(result.validation.hasConflict).toBe(false);
321
+ });
322
+ });
323
+ });