@emarketeer/ts-microservice-commons 10.0.0 → 10.2.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.
@@ -9,6 +9,8 @@ var awsLogs = require('aws-cdk-lib/aws-logs');
9
9
  var constructs = require('constructs');
10
10
  var awsSsm = require('aws-cdk-lib/aws-ssm');
11
11
  var path = require('path');
12
+ var fs = require('fs');
13
+ var child_process = require('child_process');
12
14
  var awsDynamodb = require('aws-cdk-lib/aws-dynamodb');
13
15
  var awsApigateway = require('aws-cdk-lib/aws-apigateway');
14
16
  var awsApigatewayv2 = require('aws-cdk-lib/aws-apigatewayv2');
@@ -46,6 +48,7 @@ function _interopNamespace(e) {
46
48
 
47
49
  var cdk__namespace = /*#__PURE__*/_interopNamespace(cdk);
48
50
  var path__namespace = /*#__PURE__*/_interopNamespace(path);
51
+ var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
49
52
  var ec2__namespace = /*#__PURE__*/_interopNamespace(ec2);
50
53
 
51
54
  /**
@@ -458,29 +461,30 @@ const resolveRecapDevEndpoint = (scope) => {
458
461
  const DEFAULT_LAMBDA_RUNTIME = awsLambda.Runtime.NODEJS_24_X;
459
462
 
460
463
  const DEFAULT_HANDLERS_DIR = 'src/handlers';
461
- const DEFAULT_OUT_DIR = 'dist/handlers';
462
464
  /**
463
- * Resolve `handlerPath` into `codePath`, `handler`, and optionally `functionName`.
465
+ * Resolve `handlerPath` into `entryFile`, `handler`, and optionally `functionName`.
464
466
  *
465
467
  * Given `handlerPath: 'src/handlers/capture-screenshot/capture-screenshot-from-url'`:
466
- * - `codePath` → `'dist/handlers/capture-screenshot/capture-screenshot-from-url'`
468
+ * - `entryFile` → `'src/handlers/capture-screenshot/capture-screenshot-from-url.ts'`
467
469
  * - `handler` → `'index.handler'`
468
470
  * - `functionName` → `'capture-screenshot-from-url'` (only when not explicitly provided)
469
471
  *
472
+ * When `codePath` is provided, `entryFile` is left unset — the construct will
473
+ * package `codePath` directly (no bundling).
474
+ *
470
475
  * When `handlerPath` is not provided, `functionName` is required.
471
- * `handler` and `codePath` may remain undefined if the caller has its own defaults.
472
476
  */
473
477
  function resolveHandlerPath(config) {
474
478
  const { handlerPath } = config;
475
479
  if (handlerPath) {
476
480
  const normalised = handlerPath.replace(/\.ts$/, '');
477
- const startsWithHandlersDir = normalised.startsWith(DEFAULT_HANDLERS_DIR + '/');
481
+ const startsWithHandlersDir = normalised.startsWith(`${DEFAULT_HANDLERS_DIR}/`);
478
482
  const containsSeparator = normalised.includes('/') || normalised.includes(path__namespace.sep);
479
483
  if (!startsWithHandlersDir && (path__namespace.isAbsolute(normalised) || containsSeparator)) {
480
484
  // A bare basename like 'get-data' is fine (treated as relative to
481
485
  // src/handlers). Anything with directory components must be rooted at
482
486
  // DEFAULT_HANDLERS_DIR — otherwise we'd silently produce e.g.
483
- // 'dist/handlers/src/lambdas/foo' and fail at synth with an opaque
487
+ // 'src/lambdas/foo.ts' and fail at synth with an opaque
484
488
  // Code.fromAsset error far from the call site.
485
489
  throw new Error(`resolveHandlerPath: handlerPath "${handlerPath}" must either be a bare basename or start with "${DEFAULT_HANDLERS_DIR}/".`);
486
490
  }
@@ -490,7 +494,8 @@ function resolveHandlerPath(config) {
490
494
  return {
491
495
  functionName: config.functionName ?? path__namespace.basename(relative),
492
496
  handler: config.handler ?? 'index.handler',
493
- codePath: config.codePath ?? path__namespace.join(DEFAULT_OUT_DIR, relative)
497
+ entryFile: config.codePath ? undefined : path__namespace.join(DEFAULT_HANDLERS_DIR, `${relative}.ts`),
498
+ codePath: config.codePath
494
499
  };
495
500
  }
496
501
  if (!config.functionName) {
@@ -503,6 +508,70 @@ function resolveHandlerPath(config) {
503
508
  };
504
509
  }
505
510
 
511
+ const HANDLER_BUNDLER_RELATIVE_PATH = path__namespace.join('dist', 'handler-bundler.js');
512
+ const PACKAGE_NAME = '@emarketeer/ts-microservice-commons';
513
+ let cachedBundlerPath;
514
+ function getHandlerBundlerPath() {
515
+ if (cachedBundlerPath) {
516
+ return cachedBundlerPath;
517
+ }
518
+ const cwd = process.cwd();
519
+ const candidates = [
520
+ path__namespace.join(cwd, 'node_modules', PACKAGE_NAME, HANDLER_BUNDLER_RELATIVE_PATH),
521
+ path__namespace.join(cwd, HANDLER_BUNDLER_RELATIVE_PATH)
522
+ ];
523
+ for (const candidate of candidates) {
524
+ if (fs__namespace.existsSync(candidate)) {
525
+ cachedBundlerPath = candidate;
526
+ return candidate;
527
+ }
528
+ }
529
+ throw new Error(`Could not locate the handler bundler. Searched:\n${candidates.map((c) => ` - ${c}`).join('\n')}\n`
530
+ + `Ensure ${PACKAGE_NAME} is installed.`);
531
+ }
532
+ /**
533
+ * Build the `Code` asset for a Lambda function.
534
+ *
535
+ * - When `codePath` is set, packages that directory directly (no bundling).
536
+ * - Otherwise bundles `entryFile` via the project handler bundler at synth
537
+ * time, with overrides applied on top of the defaults.
538
+ *
539
+ * Bundling uses `assetHashType: OUTPUT` so the asset hash is computed from
540
+ * the bundled output rather than from the (transitive) source tree.
541
+ */
542
+ function resolveLambdaCode(options) {
543
+ if (options.codePath) {
544
+ return awsLambda.Code.fromAsset(options.codePath);
545
+ }
546
+ if (!options.entryFile) {
547
+ throw new Error('resolveLambdaCode: either `entryFile` or `codePath` is required.');
548
+ }
549
+ const entry = path__namespace.resolve(options.entryFile);
550
+ if (!fs__namespace.existsSync(entry)) {
551
+ throw new Error(`resolveLambdaCode: entry file not found: ${entry}`);
552
+ }
553
+ const overrides = options.bundling;
554
+ const bundlerPath = getHandlerBundlerPath();
555
+ return awsLambda.Code.fromAsset(path__namespace.dirname(entry), {
556
+ assetHashType: cdk.AssetHashType.OUTPUT,
557
+ bundling: {
558
+ // CDK requires `image` even when local bundling succeeds. We never use
559
+ // the Docker fallback — `tryBundle` always returns true.
560
+ image: cdk.DockerImage.fromRegistry('node:24'),
561
+ local: {
562
+ tryBundle(outputDir) {
563
+ const stdin = JSON.stringify({ entry, outDir: outputDir, overrides });
564
+ child_process.execFileSync('node', [bundlerPath], {
565
+ input: stdin,
566
+ stdio: ['pipe', 'inherit', 'inherit']
567
+ });
568
+ return true;
569
+ }
570
+ }
571
+ }
572
+ });
573
+ }
574
+
506
575
  class EmLambdaFunction extends constructs.Construct {
507
576
  constructor(scope, id, config) {
508
577
  super(scope, id);
@@ -539,14 +608,22 @@ class EmLambdaFunction extends constructs.Construct {
539
608
  });
540
609
  const handler = resolved.handler ?? config.handler;
541
610
  const codePath = resolved.codePath ?? config.codePath;
542
- if (!handler || !codePath) {
543
- throw new Error(`EmLambdaFunction requires either \`handlerPath\` or both \`handler\` and \`codePath\` for "${resolved.functionName}".`);
611
+ if (!handler) {
612
+ throw new Error(`EmLambdaFunction requires either \`handlerPath\` or \`handler\` for "${resolved.functionName}".`);
613
+ }
614
+ if (!resolved.entryFile && !codePath) {
615
+ throw new Error(`EmLambdaFunction requires either \`handlerPath\` or \`codePath\` for "${resolved.functionName}".`);
544
616
  }
617
+ const code = resolveLambdaCode({
618
+ entryFile: resolved.entryFile,
619
+ codePath,
620
+ bundling: config.bundling
621
+ });
545
622
  this.function = new awsLambda.Function(this, 'Function', {
546
623
  functionName,
547
624
  runtime: config.runtime ?? DEFAULT_LAMBDA_RUNTIME,
548
625
  handler,
549
- code: awsLambda.Code.fromAsset(codePath),
626
+ code,
550
627
  memorySize: config.memorySize ?? 1024,
551
628
  timeout: config.timeout ?? cdk.Duration.seconds(15),
552
629
  environment: {
@@ -1827,7 +1904,15 @@ class LambdaWithQueue extends constructs.Construct {
1827
1904
  const resourceName = props.resourceName ?? shortName;
1828
1905
  const functionName = props.physicalName ?? generateLambdaName(props.stage, props.serviceName, shortName);
1829
1906
  const handler = resolved.handler ?? props.handler ?? 'index.handler';
1830
- const codePath = resolved.codePath ?? props.codePath ?? `./dist/handlers/${resourceName}`;
1907
+ const codePath = resolved.codePath ?? props.codePath;
1908
+ if (!resolved.entryFile && !codePath) {
1909
+ throw new Error(`LambdaWithQueue requires either \`handlerPath\` or \`codePath\` for "${shortName}".`);
1910
+ }
1911
+ const code = resolveLambdaCode({
1912
+ entryFile: resolved.entryFile,
1913
+ codePath,
1914
+ bundling: props.bundling
1915
+ });
1831
1916
  if (props.reservedConcurrency === 0) {
1832
1917
  throw new Error(`reservedConcurrency:0 disables the Lambda entirely for ${shortName}. Omit the prop to use account-level concurrency.`);
1833
1918
  }
@@ -1860,7 +1945,7 @@ class LambdaWithQueue extends constructs.Construct {
1860
1945
  functionName,
1861
1946
  runtime: props.runtime ?? DEFAULT_LAMBDA_RUNTIME,
1862
1947
  handler,
1863
- code: awsLambda.Code.fromAsset(codePath),
1948
+ code,
1864
1949
  architecture: props.architecture ?? awsLambda.Architecture.ARM_64,
1865
1950
  memorySize,
1866
1951
  timeout,
@@ -2165,16 +2250,9 @@ class EmStack extends cdk__namespace.Stack {
2165
2250
  const merged = this.mergeConfig(config);
2166
2251
  const resolved = resolveHandlerPath(merged);
2167
2252
  const functionName = resolved.functionName;
2168
- const handler = resolved.handler ?? merged.handler;
2169
- const codePath = resolved.codePath ?? merged.codePath;
2170
- if (!handler || !codePath) {
2171
- throw new Error(`createFunction() requires either \`handlerPath\` or all of \`functionName\`, \`handler\`, and \`codePath\` for "${functionName}".`);
2172
- }
2173
2253
  const fn = new EmLambdaFunction(this, id, {
2174
2254
  ...merged,
2175
2255
  functionName,
2176
- handler,
2177
- codePath,
2178
2256
  stage: merged.stage ?? this.stage,
2179
2257
  serviceName: merged.serviceName ?? this.serviceName,
2180
2258
  role: merged.role ?? this.sharedRole
@@ -2805,6 +2883,7 @@ exports.overrideFunctionLogicalIds = overrideFunctionLogicalIds;
2805
2883
  exports.overrideLayerLogicalId = overrideLayerLogicalId;
2806
2884
  exports.overrideRoleLogicalId = overrideRoleLogicalId;
2807
2885
  exports.resolveHandlerPath = resolveHandlerPath;
2886
+ exports.resolveLambdaCode = resolveLambdaCode;
2808
2887
  exports.resolveRecapDevEndpoint = resolveRecapDevEndpoint;
2809
2888
  exports.stageToUpperCase = stageToUpperCase;
2810
2889
  exports.toServerlessLogicalIdPrefix = toServerlessLogicalIdPrefix;