@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,534 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Build Prisma Lambda Layer
5
+ *
6
+ * Creates a Lambda Layer containing Prisma packages and rhel-openssl-3.0.x binaries.
7
+ * This reduces individual Lambda function sizes by ~60% (120MB → 45MB).
8
+ *
9
+ * The layer is configured based on AppDefinition database settings:
10
+ * - PostgreSQL: Includes PostgreSQL client + query engine
11
+ * - MongoDB: Includes MongoDB client + query engine (if needed)
12
+ * - Defaults to PostgreSQL only if not specified
13
+ *
14
+ * Usage:
15
+ * node scripts/build-prisma-layer.js [--mongodb] [--postgresql]
16
+ * npm run build:prisma-layer
17
+ *
18
+ * Output:
19
+ * layers/prisma/nodejs/node_modules/
20
+ * ├── @prisma/client
21
+ * ├── @prisma/engines
22
+ * ├── generated/prisma-postgresql (if PostgreSQL enabled)
23
+ * ├── generated/prisma-mongodb (if MongoDB enabled)
24
+ * └── prisma (CLI for migrations)
25
+ *
26
+ * See: LAMBDA-LAYER-PRISMA.md for complete documentation
27
+ */
28
+
29
+ const fs = require('fs-extra');
30
+ const path = require('path');
31
+ const { execSync } = require('child_process');
32
+
33
+ /**
34
+ * Find @friggframework/core package, handling workspace hoisting
35
+ * Searches up the directory tree to find node_modules/@friggframework/core
36
+ */
37
+ function findCorePackage(startDir) {
38
+ let currentDir = startDir;
39
+ const root = path.parse(currentDir).root;
40
+
41
+ while (currentDir !== root) {
42
+ const candidatePath = path.join(currentDir, 'node_modules/@friggframework/core');
43
+ if (fs.existsSync(candidatePath)) {
44
+ return candidatePath;
45
+ }
46
+ currentDir = path.dirname(currentDir);
47
+ }
48
+
49
+ throw new Error(
50
+ '@friggframework/core not found in node_modules.\n' +
51
+ 'Run "npm install" to install dependencies.'
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Determine which database clients to include based on configuration
57
+ * @param {Object} databaseConfig - Database configuration from AppDefinition
58
+ * @returns {Array} List of generated client packages to include
59
+ */
60
+ function getGeneratedClientPackages(databaseConfig = {}) {
61
+ const packages = [];
62
+
63
+ // Check if MongoDB is enabled
64
+ const mongoEnabled = databaseConfig?.mongodb?.enable === true;
65
+ if (mongoEnabled) {
66
+ packages.push('generated/prisma-mongodb');
67
+ log('Including MongoDB client (based on AppDefinition)', 'blue');
68
+ }
69
+
70
+ // Check if PostgreSQL is enabled (default to true if not specified)
71
+ const postgresEnabled = databaseConfig?.postgres?.enable !== false;
72
+ if (postgresEnabled) {
73
+ packages.push('generated/prisma-postgresql');
74
+ log('Including PostgreSQL client (based on AppDefinition)', 'blue');
75
+ }
76
+
77
+ // If neither specified, default to PostgreSQL only
78
+ if (packages.length === 0) {
79
+ packages.push('generated/prisma-postgresql');
80
+ log('No database specified - defaulting to PostgreSQL', 'yellow');
81
+ }
82
+
83
+ return packages;
84
+ }
85
+
86
+ // Configuration
87
+ // Script runs from integration project root (e.g., backend/)
88
+ // and reads Prisma packages from @friggframework/core
89
+ const PROJECT_ROOT = process.cwd();
90
+ const CORE_PACKAGE_PATH = findCorePackage(PROJECT_ROOT);
91
+ const LAYER_OUTPUT_PATH = path.join(PROJECT_ROOT, 'layers/prisma');
92
+ const LAYER_NODE_MODULES = path.join(LAYER_OUTPUT_PATH, 'nodejs/node_modules');
93
+
94
+ // Binary patterns to remove (non-rhel)
95
+ const BINARY_PATTERNS_TO_REMOVE = [
96
+ '*darwin*',
97
+ '*debian*',
98
+ '*linux-arm*',
99
+ '*linux-musl*',
100
+ '*windows*',
101
+ ];
102
+
103
+ // Files to remove for size optimization
104
+ const FILES_TO_REMOVE = [
105
+ '*.map', // Source maps (37MB savings)
106
+ '*.md', // Markdown files
107
+ 'LICENSE*', // License files
108
+ 'CHANGELOG*', // Changelog files
109
+ '*.test.js', // Test files
110
+ '*.spec.js', // Spec files
111
+ ];
112
+
113
+ // ANSI color codes for output
114
+ const colors = {
115
+ reset: '\x1b[0m',
116
+ bright: '\x1b[1m',
117
+ green: '\x1b[32m',
118
+ yellow: '\x1b[33m',
119
+ blue: '\x1b[34m',
120
+ red: '\x1b[31m',
121
+ };
122
+
123
+ function log(message, color = 'reset') {
124
+ console.log(`${colors[color]}${message}${colors.reset}`);
125
+ }
126
+
127
+ function logStep(step, message) {
128
+ log(`\n[${step}] ${message}`, 'blue');
129
+ }
130
+
131
+ function logSuccess(message) {
132
+ log(`✓ ${message}`, 'green');
133
+ }
134
+
135
+ function logWarning(message) {
136
+ log(`⚠ ${message}`, 'yellow');
137
+ }
138
+
139
+ function logError(message) {
140
+ log(`✗ ${message}`, 'red');
141
+ }
142
+
143
+ /**
144
+ * Get directory size in MB
145
+ */
146
+ function getDirectorySize(dirPath) {
147
+ try {
148
+ const output = execSync(`du -sm "${dirPath}"`, { encoding: 'utf8' });
149
+ const size = parseInt(output.split('\t')[0], 10);
150
+ return size;
151
+ } catch (error) {
152
+ logWarning(`Could not calculate directory size: ${error.message}`);
153
+ return 0;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Clean existing layer directory
159
+ */
160
+ async function cleanLayerDirectory() {
161
+ logStep(1, 'Cleaning existing layer directory');
162
+
163
+ if (await fs.pathExists(LAYER_OUTPUT_PATH)) {
164
+ await fs.remove(LAYER_OUTPUT_PATH);
165
+ logSuccess(`Removed existing layer at ${LAYER_OUTPUT_PATH}`);
166
+ } else {
167
+ log('No existing layer to clean');
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Create layer directory structure
173
+ */
174
+ async function createLayerStructure() {
175
+ logStep(2, 'Creating layer directory structure');
176
+
177
+ await fs.ensureDir(LAYER_NODE_MODULES);
178
+ logSuccess(`Created ${LAYER_NODE_MODULES}`);
179
+ }
180
+
181
+ /**
182
+ * Install Prisma CLI and client directly into layer
183
+ * @param {Boolean} includeCLI - Whether to include Prisma CLI (needed for migrations)
184
+ */
185
+ async function installPrismaPackages(includeCLI = false) {
186
+ logStep(3, `Installing Prisma packages (${includeCLI ? 'with CLI' : 'runtime only'})`);
187
+
188
+ // Create a minimal package.json in the layer
189
+ const dependencies = {
190
+ '@prisma/client': '^6.16.3',
191
+ '@prisma/engines': '^6.16.3',
192
+ };
193
+
194
+ // Only include CLI if needed (saves 50MB + 32MB effect dependency)
195
+ if (includeCLI) {
196
+ dependencies['prisma'] = '^6.16.3';
197
+ log(' Including Prisma CLI for migrations', 'yellow');
198
+ } else {
199
+ log(' Runtime only - CLI excluded (saves ~82MB)', 'green');
200
+ }
201
+
202
+ const layerPackageJson = {
203
+ name: 'prisma-lambda-layer',
204
+ version: '1.0.0',
205
+ private: true,
206
+ dependencies,
207
+ };
208
+
209
+ const packageJsonPath = path.join(LAYER_OUTPUT_PATH, 'nodejs/package.json');
210
+ await fs.writeJson(packageJsonPath, layerPackageJson, { spaces: 2 });
211
+ logSuccess('Created layer package.json');
212
+
213
+ // Install Prisma packages
214
+ log('Installing prisma and @prisma/client...');
215
+ try {
216
+ execSync('npm install --production --no-package-lock', {
217
+ cwd: path.join(LAYER_OUTPUT_PATH, 'nodejs'),
218
+ stdio: 'inherit'
219
+ });
220
+ logSuccess('Prisma packages installed');
221
+ } catch (error) {
222
+ throw new Error(`Failed to install Prisma packages: ${error.message}`);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Copy generated Prisma clients from @friggframework/core to layer
228
+ * @param {Array} clientPackages - List of generated client packages to copy
229
+ */
230
+ async function copyPrismaPackages(clientPackages) {
231
+ logStep(4, 'Copying generated Prisma clients from @friggframework/core');
232
+
233
+ // Copy the generated clients from core based on database config
234
+ // Packages can be in:
235
+ // 1. Core's own node_modules (if not hoisted)
236
+ // 2. Project root node_modules (if hoisted from project)
237
+ // 3. Workspace root node_modules (where core is located - handles hoisting)
238
+ // 4. Core package itself (for generated/ directories)
239
+ const workspaceNodeModules = path.join(path.dirname(CORE_PACKAGE_PATH), '..');
240
+ const searchPaths = [
241
+ path.join(CORE_PACKAGE_PATH, 'node_modules'), // Core's own node_modules
242
+ path.join(PROJECT_ROOT, 'node_modules'), // Project node_modules
243
+ workspaceNodeModules, // Workspace root node_modules
244
+ CORE_PACKAGE_PATH, // Core package itself (for generated/)
245
+ ];
246
+
247
+ let copiedCount = 0;
248
+ let missingPackages = [];
249
+
250
+ for (const pkg of clientPackages) {
251
+ let sourcePath = null;
252
+
253
+ // Try to find package in search paths
254
+ for (const searchPath of searchPaths) {
255
+ const candidatePath = path.join(searchPath, pkg);
256
+ if (await fs.pathExists(candidatePath)) {
257
+ sourcePath = candidatePath;
258
+ break;
259
+ }
260
+ }
261
+
262
+ if (sourcePath) {
263
+ const destPath = path.join(LAYER_NODE_MODULES, pkg);
264
+ await fs.copy(sourcePath, destPath, {
265
+ dereference: true, // Follow symlinks
266
+ filter: (src) => {
267
+ // Skip node_modules within Prisma packages
268
+ return !src.includes('/node_modules/node_modules/');
269
+ }
270
+ });
271
+ const fromLocation = sourcePath.includes('@friggframework/core/generated')
272
+ ? 'core package (generated)'
273
+ : sourcePath.includes('@friggframework/core/node_modules')
274
+ ? 'core node_modules'
275
+ : 'workspace';
276
+ logSuccess(`Copied ${pkg} (from ${fromLocation})`);
277
+ copiedCount++;
278
+ } else {
279
+ missingPackages.push(pkg);
280
+ logWarning(`Package not found: ${pkg}`);
281
+ }
282
+ }
283
+
284
+ if (missingPackages.length > 0) {
285
+ throw new Error(
286
+ `Missing generated Prisma clients: ${missingPackages.join(', ')}.\n` +
287
+ 'Ensure @friggframework/core has generated Prisma clients (run "npm run prisma:generate" in core package).'
288
+ );
289
+ }
290
+
291
+ logSuccess(`Copied ${copiedCount} generated client packages from @friggframework/core`);
292
+ }
293
+
294
+ /**
295
+ * Remove unnecessary files to reduce layer size
296
+ */
297
+ async function removeUnnecessaryFiles() {
298
+ logStep(5, 'Removing unnecessary files (source maps, docs, tests)');
299
+
300
+ let removedCount = 0;
301
+ let totalSize = 0;
302
+
303
+ for (const pattern of FILES_TO_REMOVE) {
304
+ try {
305
+ // Find files matching the pattern
306
+ const findCmd = `find "${LAYER_NODE_MODULES}" -name "${pattern}" -type f 2>/dev/null || true`;
307
+ const files = execSync(findCmd, { encoding: 'utf8' })
308
+ .split('\n')
309
+ .filter(f => f.trim());
310
+
311
+ for (const file of files) {
312
+ if (await fs.pathExists(file)) {
313
+ const stats = await fs.stat(file);
314
+ totalSize += stats.size;
315
+ await fs.remove(file);
316
+ removedCount++;
317
+ }
318
+ }
319
+ } catch (error) {
320
+ logWarning(`Error removing ${pattern}: ${error.message}`);
321
+ }
322
+ }
323
+
324
+ if (removedCount > 0) {
325
+ const sizeMB = (totalSize / (1024 * 1024)).toFixed(2);
326
+ logSuccess(`Removed ${removedCount} unnecessary files (saved ${sizeMB} MB)`);
327
+ } else {
328
+ log('No unnecessary files found to remove');
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Remove non-rhel engine binaries to reduce layer size
334
+ */
335
+ async function removeNonRhelBinaries() {
336
+ logStep(6, 'Removing non-rhel engine binaries');
337
+
338
+ let removedCount = 0;
339
+ let totalSize = 0;
340
+
341
+ for (const pattern of BINARY_PATTERNS_TO_REMOVE) {
342
+ try {
343
+ // Find files matching the pattern
344
+ const findCmd = `find "${LAYER_NODE_MODULES}" -name "${pattern}" 2>/dev/null || true`;
345
+ const files = execSync(findCmd, { encoding: 'utf8' })
346
+ .split('\n')
347
+ .filter(f => f.trim());
348
+
349
+ for (const file of files) {
350
+ if (await fs.pathExists(file)) {
351
+ const stats = await fs.stat(file);
352
+ totalSize += stats.size;
353
+ await fs.remove(file);
354
+ removedCount++;
355
+ }
356
+ }
357
+ } catch (error) {
358
+ logWarning(`Error removing ${pattern}: ${error.message}`);
359
+ }
360
+ }
361
+
362
+ if (removedCount > 0) {
363
+ const sizeMB = (totalSize / (1024 * 1024)).toFixed(2);
364
+ logSuccess(`Removed ${removedCount} non-rhel binaries (saved ${sizeMB} MB)`);
365
+ } else {
366
+ log('No non-rhel binaries found to remove');
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Verify rhel binaries are present
372
+ * @param {Array} expectedClients - List of client packages that should have binaries
373
+ */
374
+ async function verifyRhelBinaries(expectedClients) {
375
+ logStep(7, 'Verifying rhel-openssl-3.0.x binaries are present');
376
+
377
+ try {
378
+ const findCmd = `find "${LAYER_NODE_MODULES}" -name "*rhel-openssl-3.0.x*" 2>/dev/null || true`;
379
+ const rhelFiles = execSync(findCmd, { encoding: 'utf8' })
380
+ .split('\n')
381
+ .filter(f => f.trim());
382
+
383
+ if (rhelFiles.length === 0) {
384
+ throw new Error(
385
+ 'No rhel-openssl-3.0.x binaries found!\n' +
386
+ 'Check that Prisma schemas have binaryTargets = ["native", "rhel-openssl-3.0.x"]'
387
+ );
388
+ }
389
+
390
+ const expectedCount = expectedClients.length;
391
+ logSuccess(`Found ${rhelFiles.length} rhel-openssl-3.0.x ${rhelFiles.length === 1 ? 'binary' : 'binaries'} (expected ${expectedCount})`);
392
+
393
+ // Show the binaries found
394
+ rhelFiles.forEach(file => {
395
+ const relativePath = path.relative(LAYER_NODE_MODULES, file);
396
+ log(` - ${relativePath}`, 'reset');
397
+ });
398
+ } catch (error) {
399
+ throw new Error(`Binary verification failed: ${error.message}`);
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Verify required files exist
405
+ * @param {Boolean} includeCLI - Whether CLI files should be present
406
+ */
407
+ async function verifyLayerStructure(includeCLI) {
408
+ logStep(8, 'Verifying layer structure');
409
+
410
+ const requiredPaths = [
411
+ '@prisma/client/runtime',
412
+ '@prisma/client/index.d.ts',
413
+ '@prisma/engines',
414
+ 'generated/prisma-postgresql/schema.prisma', // PostgreSQL
415
+ ];
416
+
417
+ // Only check for CLI files if CLI was included
418
+ if (includeCLI) {
419
+ requiredPaths.push('prisma/build');
420
+ requiredPaths.push('.bin/prisma');
421
+ }
422
+
423
+ let allPresent = true;
424
+
425
+ for (const requiredPath of requiredPaths) {
426
+ const fullPath = path.join(LAYER_NODE_MODULES, requiredPath);
427
+ if (await fs.pathExists(fullPath)) {
428
+ log(` ✓ ${requiredPath}`, 'green');
429
+ } else {
430
+ log(` ✗ ${requiredPath} (missing)`, 'red');
431
+ allPresent = false;
432
+ }
433
+ }
434
+
435
+ if (!allPresent) {
436
+ throw new Error('Layer structure verification failed - missing required files');
437
+ }
438
+
439
+ logSuccess('All required files present');
440
+ }
441
+
442
+ /**
443
+ * Calculate and display final layer size
444
+ */
445
+ async function displayLayerSummary() {
446
+ logStep(9, 'Layer build summary');
447
+
448
+ const layerSizeMB = getDirectorySize(LAYER_OUTPUT_PATH);
449
+
450
+ log('\n' + '='.repeat(60), 'bright');
451
+ log(' Prisma Lambda Layer Built Successfully!', 'bright');
452
+ log('='.repeat(60), 'bright');
453
+
454
+ log(`\nLayer location: ${LAYER_OUTPUT_PATH}`, 'blue');
455
+ log(`Layer size: ~${layerSizeMB} MB`, 'green');
456
+
457
+ log('\nPackages included:', 'bright');
458
+ log(' - prisma (CLI - installed)', 'reset');
459
+ log(' - @prisma/client (runtime - installed)', 'reset');
460
+ log(' - @prisma/engines (binaries - installed)', 'reset');
461
+ log(' - generated/prisma-postgresql (PostgreSQL only)', 'reset');
462
+ log('\nNote: MongoDB support excluded (not used in this project)', 'yellow');
463
+
464
+ log('\nNext steps:', 'bright');
465
+ log(' 1. Verify layer structure: ls -lah layers/prisma/nodejs/node_modules/', 'reset');
466
+ log(' 2. Deploy to AWS: frigg deploy --stage dev', 'reset');
467
+ log(' 3. Check function sizes in Lambda console (should be ~45-55MB)', 'reset');
468
+
469
+ log('\nSee LAMBDA-LAYER-PRISMA.md for complete documentation.', 'yellow');
470
+ log('='.repeat(60) + '\n', 'bright');
471
+ }
472
+
473
+ /**
474
+ * Main build function
475
+ * @param {Object} databaseConfig - Database configuration from AppDefinition.database
476
+ * @param {Boolean} includeCLI - Whether to include Prisma CLI (false = runtime only)
477
+ */
478
+ async function buildPrismaLayer(databaseConfig = {}, includeCLI = false) {
479
+ const startTime = Date.now();
480
+
481
+ log('\n' + '='.repeat(60), 'bright');
482
+ log(` Building Prisma Lambda Layer ${includeCLI ? '(with CLI)' : '(runtime only)'}`, 'bright');
483
+ log('='.repeat(60) + '\n', 'bright');
484
+
485
+ // Log paths
486
+ log(`Project root: ${PROJECT_ROOT}`, 'reset');
487
+ log(`Core package: ${CORE_PACKAGE_PATH}`, 'reset');
488
+ log(`Layer output: ${LAYER_OUTPUT_PATH}\n`, 'reset');
489
+
490
+ // Determine which database clients to include
491
+ const clientPackages = getGeneratedClientPackages(databaseConfig);
492
+
493
+ try {
494
+ await cleanLayerDirectory();
495
+ await createLayerStructure();
496
+ await installPrismaPackages(includeCLI); // Install Prisma packages (CLI optional)
497
+ await copyPrismaPackages(clientPackages); // Copy generated clients from core
498
+ await removeUnnecessaryFiles(); // Remove source maps, docs, tests (37MB+)
499
+ await removeNonRhelBinaries(); // Remove non-Linux binaries
500
+ await verifyRhelBinaries(clientPackages); // Verify query engines present
501
+ await verifyLayerStructure(includeCLI); // Verify structure (conditional on CLI)
502
+ await displayLayerSummary();
503
+
504
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
505
+ log(`Build completed in ${duration}s\n`, 'green');
506
+
507
+ return { success: true, duration };
508
+ } catch (error) {
509
+ logError(`\nBuild failed: ${error.message}`);
510
+
511
+ if (error.stack) {
512
+ log('\nStack trace:', 'red');
513
+ console.error(error.stack);
514
+ }
515
+
516
+ log('\nTroubleshooting:', 'yellow');
517
+ log(' 1. Ensure @friggframework/core has dependencies installed', 'reset');
518
+ log(' 2. Run "npm run prisma:generate" in core package', 'reset');
519
+ log(' 3. Check that Prisma schemas have correct binaryTargets', 'reset');
520
+ log(' 4. See LAMBDA-LAYER-PRISMA.md for details\n', 'reset');
521
+
522
+ // Throw error instead of exit when used as module
523
+ throw error;
524
+ }
525
+ }
526
+
527
+ // Run the build when executed directly
528
+ if (require.main === module) {
529
+ buildPrismaLayer()
530
+ .then(() => process.exit(0))
531
+ .catch(() => process.exit(1));
532
+ }
533
+
534
+ module.exports = { buildPrismaLayer };