@friggframework/devtools 2.0.0-next.108 → 2.0.0-next.109

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.
@@ -285,6 +285,24 @@ const appDefinition = {
285
285
 
286
286
  > ℹ️ When `usePrismaLambdaLayer: false`, Prisma stays inside each bundle and the runtime automatically loads the correct binary. No extra configuration is required.
287
287
 
288
+ **Keeping nested node_modules in Lambda packages:**
289
+
290
+ By default every function package excludes `node_modules/**/node_modules/**`. npm only nests a package when the root copy cannot satisfy a dependant, so the exclusion makes a package resolve whatever version is hoisted, and it removes the package entirely when the root copy is a dev dependency. The first `require` of such a package then fails at Lambda init with `Runtime.ImportModuleError`.
291
+
292
+ To ship the dependency tree the way npm resolved it, opt in per app:
293
+
294
+ ```javascript
295
+ const appDefinition = {
296
+ name: 'my-app',
297
+ lambda: {
298
+ keepNestedNodeModules: true,
299
+ },
300
+ integrations: [{ Definition: { name: 'salesforce' } }],
301
+ };
302
+ ```
303
+
304
+ Nested copies of `@friggframework/*`, `aws-sdk`, `@aws-sdk/*` and, when the Prisma layer is enabled, `@prisma/*` and `.prisma` stay excluded: the app's single core, the Lambda runtime and the layer provide them. Expect each function package to grow by the size of the nested directories that remain (a few MB compressed in a typical app). The default stays unchanged for apps that do not set the flag.
305
+
288
306
  ## Usage Examples
289
307
 
290
308
  ### Basic Deployment
@@ -15,6 +15,7 @@
15
15
 
16
16
  const { InfrastructureBuilder, ValidationResult } = require('../shared/base-builder');
17
17
  const { isScopedEnvironmentActive } = require('../shared/function-environments');
18
+ const { nestedNodeModulesExcludes } = require('../shared/utilities/nested-node-modules');
18
19
 
19
20
  class AdminScriptBuilder extends InfrastructureBuilder {
20
21
  constructor() {
@@ -407,7 +408,7 @@ class AdminScriptBuilder extends InfrastructureBuilder {
407
408
  'node_modules/@friggframework/core/generated/**',
408
409
  ]
409
410
  : []),
410
- 'node_modules/**/node_modules/**',
411
+ ...nestedNodeModulesExcludes(appDefinition, usePrismaLayer),
411
412
  'node_modules/@friggframework/test/**',
412
413
  'node_modules/@friggframework/eslint-config/**',
413
414
  'node_modules/@friggframework/prettier-config/**',
@@ -984,3 +984,34 @@ describe('AdminScriptBuilder', () => {
984
984
  });
985
985
  });
986
986
  });
987
+
988
+ describe('AdminScriptBuilder nested node_modules (lambda.keepNestedNodeModules)', () => {
989
+ const appDefinition = (lambda) => ({
990
+ adminScripts: [{ Definition: { name: 'test-script' } }],
991
+ ...(lambda && { lambda }),
992
+ });
993
+ const executorAndRouter = (result) => [
994
+ result.functions.adminScriptExecutor,
995
+ result.functions.adminScriptRouter,
996
+ ];
997
+
998
+ it('excludes every nested node_modules by default', async () => {
999
+ const result = await new AdminScriptBuilder().build(appDefinition(), {});
1000
+
1001
+ for (const fn of executorAndRouter(result)) {
1002
+ expect(fn.package.exclude).toContain('node_modules/**/node_modules/**');
1003
+ }
1004
+ });
1005
+
1006
+ it('keeps nested node_modules when the app opts in, still excluding nested Frigg copies', async () => {
1007
+ const result = await new AdminScriptBuilder().build(
1008
+ appDefinition({ keepNestedNodeModules: true }),
1009
+ {}
1010
+ );
1011
+
1012
+ for (const fn of executorAndRouter(result)) {
1013
+ expect(fn.package.exclude).not.toContain('node_modules/**/node_modules/**');
1014
+ expect(fn.package.exclude).toContain('node_modules/**/node_modules/@friggframework/**');
1015
+ }
1016
+ });
1017
+ });
@@ -15,6 +15,7 @@
15
15
 
16
16
  const { InfrastructureBuilder, ValidationResult } = require('../shared/base-builder');
17
17
  const { isScopedEnvironmentActive } = require('../shared/function-environments');
18
+ const { nestedNodeModulesExcludes } = require('../shared/utilities/nested-node-modules');
18
19
  const { MigrationResourceResolver } = require('./migration-resolver');
19
20
  const { createEmptyDiscoveryResult, ResourceOwnership } = require('../shared/types');
20
21
 
@@ -235,7 +236,7 @@ class MigrationBuilder extends InfrastructureBuilder {
235
236
  * Create Lambda function definitions for database migrations
236
237
  * Based on refactor/add-better-support-for-commands branch implementation
237
238
  */
238
- async createFunctionDefinitions(result, usePrismaLayer = true) {
239
+ async createFunctionDefinitions(result, usePrismaLayer = true, appDefinition = {}) {
239
240
  console.log(' 🔍 DEBUG: createFunctionDefinitions called');
240
241
  console.log(' 🔍 DEBUG: result.functions is:', typeof result.functions, result.functions);
241
242
  // Migration WORKER package config (needs Prisma CLI WASM files)
@@ -250,8 +251,7 @@ class MigrationBuilder extends InfrastructureBuilder {
250
251
  ] : []),
251
252
  // But KEEP node_modules/prisma/** (the CLI with WASM)
252
253
 
253
- // Exclude ALL nested node_modules
254
- 'node_modules/**/node_modules/**',
254
+ ...nestedNodeModulesExcludes(appDefinition, usePrismaLayer),
255
255
 
256
256
  // Exclude AWS SDK (provided by Lambda runtime)
257
257
  'node_modules/aws-sdk/**',
@@ -343,8 +343,7 @@ class MigrationBuilder extends InfrastructureBuilder {
343
343
  // Router only skips Prisma CLI if Lambda Layer is enabled
344
344
  ...(usePrismaLayer ? ['node_modules/prisma/**'] : []),
345
345
 
346
- // Exclude ALL nested node_modules
347
- 'node_modules/**/node_modules/**',
346
+ ...nestedNodeModulesExcludes(appDefinition, usePrismaLayer),
348
347
 
349
348
  // Exclude AWS SDK (provided by Lambda runtime)
350
349
  'node_modules/aws-sdk/**',
@@ -516,7 +515,7 @@ class MigrationBuilder extends InfrastructureBuilder {
516
515
  console.log(' 🔍 DEBUG: result object before createFunctionDefinitions:', Object.keys(result));
517
516
 
518
517
  // Create Lambda function definitions first (they reference the queue)
519
- await this.createFunctionDefinitions(result, usePrismaLayer);
518
+ await this.createFunctionDefinitions(result, usePrismaLayer, appDefinition);
520
519
 
521
520
  console.log(' 🔍 DEBUG: result.functions after createFunctionDefinitions:', Object.keys(result.functions || {}));
522
521
 
@@ -424,3 +424,31 @@ describe('MigrationBuilder', () => {
424
424
  });
425
425
  });
426
426
 
427
+
428
+ describe('MigrationBuilder nested node_modules (lambda.keepNestedNodeModules)', () => {
429
+ const baseAppDefinition = { database: { postgres: { enable: true } } };
430
+ const workerAndRouter = (result) => [
431
+ result.functions.dbMigrationWorker,
432
+ result.functions.dbMigrationRouter,
433
+ ];
434
+
435
+ it('excludes every nested node_modules by default', async () => {
436
+ const result = await new MigrationBuilder().build(baseAppDefinition, {});
437
+
438
+ for (const fn of workerAndRouter(result)) {
439
+ expect(fn.package.exclude).toContain('node_modules/**/node_modules/**');
440
+ }
441
+ });
442
+
443
+ it('keeps nested node_modules when the app opts in, still excluding nested Frigg copies', async () => {
444
+ const result = await new MigrationBuilder().build(
445
+ { ...baseAppDefinition, lambda: { keepNestedNodeModules: true } },
446
+ {}
447
+ );
448
+
449
+ for (const fn of workerAndRouter(result)) {
450
+ expect(fn.package.exclude).not.toContain('node_modules/**/node_modules/**');
451
+ expect(fn.package.exclude).toContain('node_modules/**/node_modules/@friggframework/**');
452
+ }
453
+ });
454
+ });
@@ -29,6 +29,9 @@ const {
29
29
  getIntegrationFunctionNames,
30
30
  getAdminFunctionNames,
31
31
  } = require('../shared/function-environments');
32
+ const {
33
+ nestedNodeModulesExcludes,
34
+ } = require('../shared/utilities/nested-node-modules');
32
35
 
33
36
  class IntegrationBuilder extends InfrastructureBuilder {
34
37
  constructor() {
@@ -182,8 +185,10 @@ class IntegrationBuilder extends InfrastructureBuilder {
182
185
  usePrismaLayer = true
183
186
  ) {
184
187
  // Create package config first — needed by all Lambda functions including DLQ processor
185
- const functionPackageConfig =
186
- this.createFunctionPackageConfig(usePrismaLayer);
188
+ const functionPackageConfig = this.createFunctionPackageConfig(
189
+ usePrismaLayer,
190
+ appDefinition
191
+ );
187
192
 
188
193
  // Create InternalErrorQueue if ownership = STACK
189
194
  const shouldCreateInternalErrorQueue =
@@ -243,7 +248,7 @@ class IntegrationBuilder extends InfrastructureBuilder {
243
248
  /**
244
249
  * Create function package exclusion configuration
245
250
  */
246
- createFunctionPackageConfig(usePrismaLayer = true) {
251
+ createFunctionPackageConfig(usePrismaLayer = true, appDefinition = {}) {
247
252
  return {
248
253
  exclude: [
249
254
  // Exclude AWS SDK (provided by Lambda runtime)
@@ -260,8 +265,7 @@ class IntegrationBuilder extends InfrastructureBuilder {
260
265
  ]
261
266
  : []),
262
267
 
263
- // Exclude ALL nested node_modules
264
- 'node_modules/**/node_modules/**',
268
+ ...nestedNodeModulesExcludes(appDefinition, usePrismaLayer),
265
269
 
266
270
  // Exclude build tools (not needed at runtime)
267
271
  'node_modules/esbuild/**',
@@ -1032,3 +1032,33 @@ describe('IntegrationBuilder', () => {
1032
1032
  });
1033
1033
  });
1034
1034
 
1035
+
1036
+ describe('IntegrationBuilder nested node_modules (lambda.keepNestedNodeModules)', () => {
1037
+ const appDefinition = (lambda) => ({
1038
+ integrations: [{ Definition: { name: 'test', webhooks: true } }],
1039
+ ...(lambda && { lambda }),
1040
+ });
1041
+ const packagedFunctions = (result) =>
1042
+ Object.values(result.functions).filter((fn) => fn.package?.exclude);
1043
+
1044
+ it('excludes every nested node_modules by default', async () => {
1045
+ const result = await new IntegrationBuilder().build(appDefinition(), {});
1046
+
1047
+ expect(packagedFunctions(result).length).toBeGreaterThan(0);
1048
+ for (const fn of packagedFunctions(result)) {
1049
+ expect(fn.package.exclude).toContain('node_modules/**/node_modules/**');
1050
+ }
1051
+ });
1052
+
1053
+ it('keeps nested node_modules when the app opts in, still excluding nested Frigg copies', async () => {
1054
+ const result = await new IntegrationBuilder().build(
1055
+ appDefinition({ keepNestedNodeModules: true }),
1056
+ {}
1057
+ );
1058
+
1059
+ for (const fn of packagedFunctions(result)) {
1060
+ expect(fn.package.exclude).not.toContain('node_modules/**/node_modules/**');
1061
+ expect(fn.package.exclude).toContain('node_modules/**/node_modules/@friggframework/**');
1062
+ }
1063
+ });
1064
+ });
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  const { buildEnvironment } = require('../environment-builder');
11
+ const { nestedNodeModulesExcludes } = require('./nested-node-modules');
11
12
 
12
13
  /**
13
14
  * Create base serverless definition with core functions and resources
@@ -64,8 +65,7 @@ function createBaseDefinition(
64
65
  'node_modules/prettier/**',
65
66
  'node_modules/eslint/**',
66
67
 
67
- // Exclude ALL nested node_modules (catch any package with nested dependencies)
68
- 'node_modules/**/node_modules/**',
68
+ ...nestedNodeModulesExcludes(AppDefinition, usePrismaLayer),
69
69
 
70
70
  // Exclude build tools (not needed at runtime)
71
71
  'node_modules/esbuild/**',
@@ -289,3 +289,31 @@ describe('Base Definition Factory', () => {
289
289
  });
290
290
  });
291
291
 
292
+
293
+ describe('nested node_modules (lambda.keepNestedNodeModules)', () => {
294
+ const build = (appDefinition) => createBaseDefinition(appDefinition, {}, {}, true);
295
+
296
+ it('excludes every nested node_modules from the core function packages by default', () => {
297
+ const result = build({ name: 'test-app' });
298
+
299
+ for (const fn of ['auth', 'user', 'health']) {
300
+ expect(result.functions[fn].package.exclude).toContain('node_modules/**/node_modules/**');
301
+ }
302
+ });
303
+
304
+ it('keeps nested node_modules when the app opts in, still excluding nested Frigg, AWS SDK and Prisma copies', () => {
305
+ const result = build({ name: 'test-app', lambda: { keepNestedNodeModules: true } });
306
+
307
+ for (const fn of ['auth', 'user', 'health']) {
308
+ const { exclude } = result.functions[fn].package;
309
+ expect(exclude).not.toContain('node_modules/**/node_modules/**');
310
+ expect(exclude).toEqual(
311
+ expect.arrayContaining([
312
+ 'node_modules/**/node_modules/@friggframework/**',
313
+ 'node_modules/**/node_modules/@aws-sdk/**',
314
+ 'node_modules/**/node_modules/@prisma/**',
315
+ ])
316
+ );
317
+ }
318
+ });
319
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Nested node_modules packaging patterns
3
+ *
4
+ * Utility Layer - Hexagonal Architecture
5
+ *
6
+ * npm nests a package only when the root copy cannot satisfy the dependant's
7
+ * range. Excluding every nested node_modules from a Lambda package therefore
8
+ * rewires module resolution to whatever is hoisted, and drops a package
9
+ * outright when the root copy is dev-only. Apps opt in to shipping the tree
10
+ * the way npm resolved it with `lambda.keepNestedNodeModules: true`. Nested
11
+ * copies of Frigg packages, the AWS SDK and Prisma stay excluded because the
12
+ * app's single core, the Lambda runtime and the Prisma layer provide them.
13
+ */
14
+
15
+ const ALL_NESTED_NODE_MODULES = 'node_modules/**/node_modules/**';
16
+
17
+ /**
18
+ * @param {Object} appDefinition
19
+ * @returns {boolean}
20
+ */
21
+ function keepsNestedNodeModules(appDefinition = {}) {
22
+ return appDefinition.lambda?.keepNestedNodeModules === true;
23
+ }
24
+
25
+ /**
26
+ * Package exclude patterns for nested node_modules directories.
27
+ *
28
+ * @param {Object} appDefinition
29
+ * @param {boolean} usePrismaLayer - Whether Prisma ships via the Lambda Layer
30
+ * @returns {string[]}
31
+ */
32
+ function nestedNodeModulesExcludes(appDefinition, usePrismaLayer = true) {
33
+ if (!keepsNestedNodeModules(appDefinition)) {
34
+ return [ALL_NESTED_NODE_MODULES];
35
+ }
36
+ return [
37
+ 'node_modules/**/node_modules/@friggframework/**',
38
+ 'node_modules/**/node_modules/aws-sdk/**',
39
+ 'node_modules/**/node_modules/@aws-sdk/**',
40
+ ...(usePrismaLayer
41
+ ? [
42
+ 'node_modules/**/node_modules/@prisma/**',
43
+ 'node_modules/**/node_modules/.prisma/**',
44
+ ]
45
+ : []),
46
+ ];
47
+ }
48
+
49
+ module.exports = {
50
+ ALL_NESTED_NODE_MODULES,
51
+ keepsNestedNodeModules,
52
+ nestedNodeModulesExcludes,
53
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Tests for nested node_modules packaging patterns
3
+ */
4
+
5
+ const {
6
+ ALL_NESTED_NODE_MODULES,
7
+ keepsNestedNodeModules,
8
+ nestedNodeModulesExcludes,
9
+ } = require('./nested-node-modules');
10
+
11
+ describe('nestedNodeModulesExcludes', () => {
12
+ it('excludes every nested node_modules by default', () => {
13
+ expect(nestedNodeModulesExcludes({}, true)).toEqual([
14
+ 'node_modules/**/node_modules/**',
15
+ ]);
16
+ expect(ALL_NESTED_NODE_MODULES).toBe('node_modules/**/node_modules/**');
17
+ });
18
+
19
+ it('keeps nested node_modules when the app opts in, except Frigg, AWS SDK and Prisma copies', () => {
20
+ const appDefinition = { lambda: { keepNestedNodeModules: true } };
21
+
22
+ const excludes = nestedNodeModulesExcludes(appDefinition, true);
23
+
24
+ expect(excludes).not.toContain('node_modules/**/node_modules/**');
25
+ expect(excludes).toEqual([
26
+ 'node_modules/**/node_modules/@friggframework/**',
27
+ 'node_modules/**/node_modules/aws-sdk/**',
28
+ 'node_modules/**/node_modules/@aws-sdk/**',
29
+ 'node_modules/**/node_modules/@prisma/**',
30
+ 'node_modules/**/node_modules/.prisma/**',
31
+ ]);
32
+ });
33
+
34
+ it('keeps nested Prisma copies when the app bundles Prisma instead of using the layer', () => {
35
+ const appDefinition = { lambda: { keepNestedNodeModules: true } };
36
+
37
+ const excludes = nestedNodeModulesExcludes(appDefinition, false);
38
+
39
+ expect(excludes).toEqual([
40
+ 'node_modules/**/node_modules/@friggframework/**',
41
+ 'node_modules/**/node_modules/aws-sdk/**',
42
+ 'node_modules/**/node_modules/@aws-sdk/**',
43
+ ]);
44
+ });
45
+ });
46
+
47
+ describe('keepsNestedNodeModules', () => {
48
+ it('is only enabled by the boolean true', () => {
49
+ expect(
50
+ keepsNestedNodeModules({ lambda: { keepNestedNodeModules: true } })
51
+ ).toBe(true);
52
+ expect(
53
+ keepsNestedNodeModules({
54
+ lambda: { keepNestedNodeModules: 'true' },
55
+ })
56
+ ).toBe(false);
57
+ expect(keepsNestedNodeModules({ lambda: {} })).toBe(false);
58
+ expect(keepsNestedNodeModules({})).toBe(false);
59
+ expect(keepsNestedNodeModules()).toBe(false);
60
+ });
61
+ });
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-next.108",
4
+ "version": "2.0.0-next.109",
5
5
  "bin": {
6
6
  "frigg": "./frigg-cli/index.js"
7
7
  },
@@ -26,9 +26,9 @@
26
26
  "@babel/eslint-parser": "^7.18.9",
27
27
  "@babel/parser": "^7.25.3",
28
28
  "@babel/traverse": "^7.25.3",
29
- "@friggframework/core": "2.0.0-next.108",
30
- "@friggframework/schemas": "2.0.0-next.108",
31
- "@friggframework/test": "2.0.0-next.108",
29
+ "@friggframework/core": "2.0.0-next.109",
30
+ "@friggframework/schemas": "2.0.0-next.109",
31
+ "@friggframework/test": "2.0.0-next.109",
32
32
  "@hapi/boom": "^10.0.1",
33
33
  "@inquirer/prompts": "^5.3.8",
34
34
  "axios": "^1.18.0",
@@ -56,8 +56,8 @@
56
56
  "validate-npm-package-name": "^5.0.0"
57
57
  },
58
58
  "devDependencies": {
59
- "@friggframework/eslint-config": "2.0.0-next.108",
60
- "@friggframework/prettier-config": "2.0.0-next.108",
59
+ "@friggframework/eslint-config": "2.0.0-next.109",
60
+ "@friggframework/prettier-config": "2.0.0-next.109",
61
61
  "aws-sdk-client-mock": "^4.1.0",
62
62
  "aws-sdk-client-mock-jest": "^4.1.0",
63
63
  "jest": "^30.1.3",
@@ -89,5 +89,5 @@
89
89
  "publishConfig": {
90
90
  "access": "public"
91
91
  },
92
- "gitHead": "2b4b206dfda5366614042e9743162ccd07c50ce6"
92
+ "gitHead": "193f4e024490cd25256c0fa33fd05bc8f323a58e"
93
93
  }