@friggframework/devtools 2.0.0--canary.463.62579dd.0 → 2.0.0--canary.461.ec909cf.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.
@@ -0,0 +1,385 @@
1
+ /**
2
+ * Base Serverless Definition Factory
3
+ *
4
+ * Utility Layer - Hexagonal Architecture
5
+ *
6
+ * Creates the base serverless.yml configuration with core functions,
7
+ * resources, plugins, and provider settings.
8
+ */
9
+
10
+ const { buildEnvironment } = require('../environment-builder');
11
+
12
+ /**
13
+ * Create base serverless definition with core functions and resources
14
+ *
15
+ * This creates the foundation serverless configuration that all
16
+ * Frigg applications need, including:
17
+ * - Core Lambda functions (auth, user, health, dbMigrate)
18
+ * - Error handling infrastructure (SQS, SNS, CloudWatch)
19
+ * - Prisma Lambda Layer
20
+ * - Base plugins and esbuild configuration
21
+ *
22
+ * @param {Object} AppDefinition - Application definition
23
+ * @param {Object} appEnvironmentVars - Environment variables from app definition
24
+ * @param {Object} discoveredResources - AWS resources discovered during build
25
+ * @returns {Object} Base serverless definition
26
+ */
27
+ function createBaseDefinition(
28
+ AppDefinition,
29
+ appEnvironmentVars,
30
+ discoveredResources
31
+ ) {
32
+ const region = process.env.AWS_REGION || 'us-east-1';
33
+
34
+ // Package config for handlers that skip esbuild (need node_modules dependencies)
35
+ // Since Express and other deps are now in backend/node_modules, exclude only what's not needed
36
+ const skipEsbuildPackageConfig = {
37
+ exclude: [
38
+ // Exclude Prisma (provided via Lambda Layer)
39
+ 'node_modules/@prisma/**',
40
+ 'node_modules/.prisma/**',
41
+ 'node_modules/prisma/**',
42
+ 'node_modules/@friggframework/core/generated/**',
43
+
44
+ // Exclude AWS SDK (provided by Lambda runtime)
45
+ 'node_modules/aws-sdk/**',
46
+ 'node_modules/@aws-sdk/**',
47
+
48
+ // Exclude dev/test dependencies
49
+ 'node_modules/@friggframework/test/**',
50
+ 'node_modules/@friggframework/eslint-config/**',
51
+ 'node_modules/@friggframework/prettier-config/**',
52
+ 'node_modules/jest/**',
53
+ 'node_modules/prettier/**',
54
+ 'node_modules/eslint/**',
55
+
56
+ // Exclude backend source and layers
57
+ 'src/**',
58
+ 'test/**',
59
+ 'layers/**',
60
+ 'coverage/**',
61
+ '**/*.test.js',
62
+ '**/*.spec.js',
63
+ '**/.claude-flow/**',
64
+ '**/.swarm/**',
65
+ ],
66
+ };
67
+
68
+ // Function-level package config to exclude Prisma and AWS SDK
69
+ const functionPackageConfig = {
70
+ exclude: [
71
+ // Exclude AWS SDK (already in Lambda runtime or externalized by esbuild)
72
+ 'node_modules/aws-sdk/**',
73
+ 'node_modules/@aws-sdk/**',
74
+
75
+ // Exclude Prisma (provided via Lambda Layer)
76
+ 'node_modules/@prisma/**',
77
+ 'node_modules/.prisma/**',
78
+ 'node_modules/prisma/**',
79
+ 'node_modules/@friggframework/core/generated/**',
80
+
81
+ // Exclude nested node_modules from symlinked frigg packages (for npm link development)
82
+ 'node_modules/@friggframework/core/node_modules/**',
83
+ 'node_modules/@friggframework/devtools/node_modules/**',
84
+
85
+ // Exclude development/test files from backend project
86
+ 'coverage/**',
87
+ 'test/**',
88
+ 'src/**',
89
+ 'layers/**',
90
+ '**/*.test.js',
91
+ '**/*.spec.js',
92
+ '.git/**',
93
+ '.github/**',
94
+
95
+ // Exclude AI assistant and development artifacts
96
+ '**/.claude-flow/**',
97
+ '**/.swarm/**',
98
+ '**/CLAUDE.md',
99
+ '**/README.md',
100
+ '**/*.md',
101
+
102
+ // Exclude config and meta files from core
103
+ 'node_modules/@friggframework/core/.eslintrc.json',
104
+ 'node_modules/@friggframework/core/.gitignore',
105
+ 'node_modules/@friggframework/core/jest.config.js',
106
+ 'node_modules/@friggframework/core/CHANGELOG.md',
107
+ ],
108
+ };
109
+
110
+ return {
111
+ frameworkVersion: '>=3.17.0',
112
+ service: AppDefinition.name || 'create-frigg-app',
113
+ package: {
114
+ individually: true,
115
+ },
116
+ useDotenv: true,
117
+ provider: {
118
+ name: AppDefinition.provider || 'aws',
119
+ ...(process.env.AWS_PROFILE && { profile: process.env.AWS_PROFILE }),
120
+ runtime: 'nodejs22.x', // Node.js 22.x (latest Lambda runtime with AWS SDK v3)
121
+ timeout: 29, // Set to 29s to give buffer before API Gateway's 30s timeout
122
+ region,
123
+ stage: '${opt:stage}',
124
+ environment: buildEnvironment(appEnvironmentVars, discoveredResources),
125
+ iamRoleStatements: [
126
+ {
127
+ Effect: 'Allow',
128
+ Action: ['sns:Publish'],
129
+ Resource: { Ref: 'InternalErrorBridgeTopic' },
130
+ },
131
+ {
132
+ Effect: 'Allow',
133
+ Action: [
134
+ 'sqs:SendMessage',
135
+ 'sqs:SendMessageBatch',
136
+ 'sqs:GetQueueUrl',
137
+ 'sqs:GetQueueAttributes',
138
+ ],
139
+ Resource: [
140
+ { 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
141
+ {
142
+ 'Fn::Join': [
143
+ ':',
144
+ [
145
+ 'arn:aws:sqs:${self:provider.region}:*:${self:service}--${self:provider.stage}-*Queue',
146
+ ],
147
+ ],
148
+ },
149
+ ],
150
+ },
151
+ ],
152
+ httpApi: {
153
+ payload: '2.0',
154
+ cors: {
155
+ allowedOrigins: ['*'],
156
+ allowedHeaders: ['*'],
157
+ allowedMethods: ['*'],
158
+ allowCredentials: false,
159
+ },
160
+ name: '${opt:stage, "dev"}-${self:service}',
161
+ disableDefaultEndpoint: false,
162
+ },
163
+ },
164
+ plugins: [
165
+ 'serverless-esbuild',
166
+ 'serverless-dotenv-plugin',
167
+ 'serverless-offline-sqs',
168
+ 'serverless-offline',
169
+ '@friggframework/serverless-plugin',
170
+ ],
171
+ custom: {
172
+ esbuild: {
173
+ bundle: true,
174
+ minify: true,
175
+ sourcemap: true,
176
+ target: 'node22',
177
+ platform: 'node',
178
+ format: 'cjs',
179
+ external: [
180
+ '@aws-sdk/*',
181
+ 'aws-sdk',
182
+ '@prisma/client',
183
+ 'prisma',
184
+ '.prisma/*',
185
+ ],
186
+ packager: 'npm',
187
+ keepNames: true,
188
+ keepOutputDirectory: false, // Clean up .esbuild directory after packaging
189
+ exclude: [
190
+ 'aws-sdk',
191
+ '@aws-sdk/*',
192
+ '@prisma/client',
193
+ 'prisma',
194
+ ],
195
+ },
196
+ 'serverless-offline': {
197
+ httpPort: 3001,
198
+ lambdaPort: 4001,
199
+ websocketPort: 3002,
200
+ location: '.', // Set base directory for handler resolution to current directory
201
+ skipCacheInvalidation: false,
202
+ },
203
+ 'serverless-offline-sqs': {
204
+ autoCreate: false,
205
+ apiVersion: '2012-11-05',
206
+ endpoint: 'http://localhost:4566',
207
+ region,
208
+ accessKeyId: 'root',
209
+ secretAccessKey: 'root',
210
+ skipCacheInvalidation: false,
211
+ },
212
+ },
213
+ functions: {
214
+ auth: {
215
+ handler: 'node_modules/@friggframework/core/handlers/routers/auth.handler',
216
+ layers: [{ Ref: 'PrismaLambdaLayer' }],
217
+ skipEsbuild: true, // Handlers in node_modules don't need bundling
218
+ package: skipEsbuildPackageConfig,
219
+ events: [
220
+ { httpApi: { path: '/api/integrations', method: 'ANY' } },
221
+ {
222
+ httpApi: {
223
+ path: '/api/integrations/{proxy+}',
224
+ method: 'ANY',
225
+ },
226
+ },
227
+ { httpApi: { path: '/api/authorize', method: 'ANY' } },
228
+ ],
229
+ },
230
+ user: {
231
+ handler: 'node_modules/@friggframework/core/handlers/routers/user.handler',
232
+ layers: [{ Ref: 'PrismaLambdaLayer' }],
233
+ skipEsbuild: true, // Handlers in node_modules don't need bundling
234
+ package: skipEsbuildPackageConfig,
235
+ events: [{ httpApi: { path: '/user/{proxy+}', method: 'ANY' } }],
236
+ },
237
+ health: {
238
+ handler: 'node_modules/@friggframework/core/handlers/routers/health.handler',
239
+ layers: [{ Ref: 'PrismaLambdaLayer' }],
240
+ skipEsbuild: true, // Handlers in node_modules don't need bundling
241
+ package: skipEsbuildPackageConfig,
242
+ events: [
243
+ { httpApi: { path: '/health', method: 'GET' } },
244
+ { httpApi: { path: '/health/{proxy+}', method: 'GET' } },
245
+ ],
246
+ },
247
+ dbMigrate: {
248
+ handler: 'node_modules/@friggframework/core/handlers/database-migration-handler.handler',
249
+ // DO NOT use Prisma layer - this function includes Prisma CLI separately
250
+ skipEsbuild: true, // Handlers in node_modules don't need bundling
251
+ timeout: 300, // 5 minutes for long-running migrations
252
+ memorySize: 1024, // Extra memory for Prisma CLI and migration operations
253
+ reservedConcurrency: 1, // Prevent concurrent migrations (CRITICAL for data safety)
254
+ description: 'Runs database migrations via Prisma CLI (invoke manually from CI/CD or triggers). Prisma CLI bundled separately.',
255
+ package: {
256
+ individually: true,
257
+ patterns: [
258
+ // Include handler
259
+ 'node_modules/@friggframework/core/handlers/database-migration-handler.js',
260
+
261
+ // Include ONLY PostgreSQL Prisma client (exclude MongoDB)
262
+ 'node_modules/@friggframework/core/generated/prisma-postgresql/**',
263
+ '!node_modules/@friggframework/core/generated/prisma-mongodb/**', // Exclude MongoDB client entirely
264
+
265
+ // Include Prisma runtime
266
+ 'node_modules/@prisma/client/**',
267
+ 'node_modules/.prisma/**',
268
+ 'node_modules/prisma/**', // Prisma CLI
269
+
270
+ // Exclude unnecessary engines and files
271
+ '!node_modules/prisma/node_modules/**',
272
+ '!**/query-engine-darwin*', // Exclude macOS binaries (keep rhel for Lambda)
273
+ '!**/runtime/*.wasm', // WASM engines
274
+ '!**/*.md',
275
+ '!**/*.map',
276
+ '!**/LICENSE*',
277
+ '!**/*.d.ts',
278
+ '!**/*.d.mts',
279
+ ],
280
+ },
281
+ maximumEventAge: 60,
282
+ maximumRetryAttempts: 0,
283
+ tags: {
284
+ Purpose: 'DatabaseMigration',
285
+ ManagedBy: 'Frigg',
286
+ },
287
+ environment: {
288
+ CI: '1',
289
+ PRISMA_HIDE_UPDATE_MESSAGE: '1',
290
+ PRISMA_MIGRATE_SKIP_SEED: '1',
291
+ },
292
+ },
293
+ },
294
+ layers: {
295
+ prisma: {
296
+ path: 'layers/prisma',
297
+ name: '${self:service}-prisma-${sls:stage}',
298
+ description: 'Prisma runtime client only (NO CLI) with rhel-openssl-3.0.x binaries (~10-15MB). CLI packaged separately in dbMigrate function.',
299
+ compatibleRuntimes: ['nodejs20.x', 'nodejs22.x'],
300
+ retain: false,
301
+ },
302
+ },
303
+ resources: {
304
+ Resources: {
305
+ InternalErrorQueue: {
306
+ Type: 'AWS::SQS::Queue',
307
+ Properties: {
308
+ QueueName:
309
+ '${self:service}-internal-error-queue-${self:provider.stage}',
310
+ MessageRetentionPeriod: 300,
311
+ },
312
+ },
313
+ InternalErrorBridgeTopic: {
314
+ Type: 'AWS::SNS::Topic',
315
+ Properties: {
316
+ Subscription: [
317
+ {
318
+ Protocol: 'sqs',
319
+ Endpoint: {
320
+ 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
321
+ },
322
+ },
323
+ ],
324
+ },
325
+ },
326
+ InternalErrorBridgePolicy: {
327
+ Type: 'AWS::SQS::QueuePolicy',
328
+ Properties: {
329
+ Queues: [{ Ref: 'InternalErrorQueue' }],
330
+ PolicyDocument: {
331
+ Version: '2012-10-17',
332
+ Statement: [
333
+ {
334
+ Sid: 'Allow Dead Letter SNS to publish to SQS',
335
+ Effect: 'Allow',
336
+ Principal: { Service: 'sns.amazonaws.com' },
337
+ Resource: {
338
+ 'Fn::GetAtt': [
339
+ 'InternalErrorQueue',
340
+ 'Arn',
341
+ ],
342
+ },
343
+ Action: [
344
+ 'SQS:SendMessage',
345
+ 'SQS:SendMessageBatch',
346
+ ],
347
+ Condition: {
348
+ ArnEquals: {
349
+ 'aws:SourceArn': {
350
+ Ref: 'InternalErrorBridgeTopic',
351
+ },
352
+ },
353
+ },
354
+ },
355
+ ],
356
+ },
357
+ },
358
+ },
359
+ ApiGatewayAlarm5xx: {
360
+ Type: 'AWS::CloudWatch::Alarm',
361
+ Properties: {
362
+ AlarmDescription: 'API Gateway 5xx Errors',
363
+ Namespace: 'AWS/ApiGateway',
364
+ MetricName: '5XXError',
365
+ Statistic: 'Sum',
366
+ Threshold: 0,
367
+ ComparisonOperator: 'GreaterThanThreshold',
368
+ EvaluationPeriods: 1,
369
+ Period: 60,
370
+ AlarmActions: [{ Ref: 'InternalErrorBridgeTopic' }],
371
+ Dimensions: [
372
+ { Name: 'ApiId', Value: { Ref: 'HttpApi' } },
373
+ { Name: 'Stage', Value: '${self:provider.stage}' },
374
+ ],
375
+ },
376
+ },
377
+ },
378
+ },
379
+ };
380
+ }
381
+
382
+ module.exports = {
383
+ createBaseDefinition,
384
+ };
385
+
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Handler Path Resolver Utility
3
+ *
4
+ * Utility Layer - Hexagonal Architecture
5
+ *
6
+ * Handles Lambda handler path resolution for offline mode compatibility.
7
+ * In offline mode, handler paths need to be relative to the working directory
8
+ * rather than absolute paths to node_modules.
9
+ */
10
+
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+
14
+ /**
15
+ * Find node_modules path for offline mode handler path resolution
16
+ *
17
+ * Searches upward from current directory to locate node_modules directory
18
+ * using multiple fallback strategies.
19
+ *
20
+ * @returns {string} Path to node_modules directory
21
+ */
22
+ function findNodeModulesPath() {
23
+ try {
24
+ let currentDir = process.cwd();
25
+ let nodeModulesPath = null;
26
+
27
+ // Strategy 1: Search upward through directory tree
28
+ for (let i = 0; i < 5; i++) {
29
+ const potentialPath = path.join(currentDir, 'node_modules');
30
+ if (fs.existsSync(potentialPath)) {
31
+ nodeModulesPath = potentialPath;
32
+ console.log(
33
+ `Found node_modules at: ${nodeModulesPath} (method 1)`
34
+ );
35
+ break;
36
+ }
37
+ const parentDir = path.dirname(currentDir);
38
+ if (parentDir === currentDir) break;
39
+ currentDir = parentDir;
40
+ }
41
+
42
+ // Strategy 2: Use npm root command
43
+ if (!nodeModulesPath) {
44
+ try {
45
+ const { execSync } = require('node:child_process');
46
+ const npmRoot = execSync('npm root', {
47
+ encoding: 'utf8',
48
+ }).trim();
49
+ if (fs.existsSync(npmRoot)) {
50
+ nodeModulesPath = npmRoot;
51
+ console.log(
52
+ `Found node_modules at: ${nodeModulesPath} (method 2)`
53
+ );
54
+ }
55
+ } catch (npmError) {
56
+ console.error('Error executing npm root:', npmError);
57
+ }
58
+ }
59
+
60
+ // Strategy 3: Search from package.json locations
61
+ if (!nodeModulesPath) {
62
+ currentDir = process.cwd();
63
+ for (let i = 0; i < 5; i++) {
64
+ const packageJsonPath = path.join(currentDir, 'package.json');
65
+ if (fs.existsSync(packageJsonPath)) {
66
+ const potentialNodeModules = path.join(
67
+ currentDir,
68
+ 'node_modules'
69
+ );
70
+ if (fs.existsSync(potentialNodeModules)) {
71
+ nodeModulesPath = potentialNodeModules;
72
+ console.log(
73
+ `Found node_modules at: ${nodeModulesPath} (method 3)`
74
+ );
75
+ break;
76
+ }
77
+ }
78
+ const parentDir = path.dirname(currentDir);
79
+ if (parentDir === currentDir) break;
80
+ currentDir = parentDir;
81
+ }
82
+ }
83
+
84
+ if (nodeModulesPath) {
85
+ return nodeModulesPath;
86
+ }
87
+
88
+ // Fallback: Assume parent directory
89
+ console.warn(
90
+ 'Could not find node_modules path, falling back to default'
91
+ );
92
+ return path.resolve(process.cwd(), '../node_modules');
93
+ } catch (error) {
94
+ console.error('Error finding node_modules path:', error);
95
+ return path.resolve(process.cwd(), '../node_modules');
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Modify handler paths for offline mode
101
+ *
102
+ * In serverless-offline mode, handler paths need to be relative
103
+ * to the current working directory rather than using absolute
104
+ * node_modules paths.
105
+ *
106
+ * @param {Object} functions - Serverless functions configuration
107
+ * @returns {Object} Functions with modified handler paths
108
+ */
109
+ function modifyHandlerPaths(functions) {
110
+ const isOffline = process.argv.includes('offline');
111
+ console.log('isOffline', isOffline);
112
+
113
+ if (!isOffline) {
114
+ console.log('Not in offline mode, skipping handler path modification');
115
+ return functions;
116
+ }
117
+
118
+ // In offline mode, don't modify the handler paths at all
119
+ // serverless-offline will resolve node_modules paths from the working directory
120
+ console.log('Offline mode detected - keeping original handler paths for serverless-offline');
121
+
122
+ return functions;
123
+ }
124
+
125
+ module.exports = {
126
+ findNodeModulesPath,
127
+ modifyHandlerPaths,
128
+ };
129
+