1- const { load } = require ( 'UserFunction.js' ) ; // https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/main/src/UserFunction.js
1+ const fs = require ( 'fs' ) ;
2+ const path = require ( 'path' ) ;
3+ const { pathToFileURL } = require ( 'url' ) ;
24
3- module . exports . load = load
5+ const DEBUG = process . env . CX_DEBUG_USER_FUNCTION_RESOLUTION === 'true' ;
6+
7+ function logDebug ( message ) {
8+ if ( DEBUG ) {
9+ // eslint-disable-next-line no-console
10+ console . debug ( `[cx-wrapper] ${ message } ` ) ;
11+ }
12+ }
13+
14+ // Break "module.sub.handler" into the module path and exported symbol chain.
15+ function splitHandlerString ( handler ) {
16+ const lastDot = handler . lastIndexOf ( '.' ) ;
17+ if ( lastDot === - 1 || lastDot === handler . length - 1 ) {
18+ throw new Error (
19+ `CX handler "${ handler } " must be in "module.submodule.handler" format.`
20+ ) ;
21+ }
22+ return [ handler . slice ( 0 , lastDot ) , handler . slice ( lastDot + 1 ) ] ;
23+ }
24+
25+ // Walk the export tree (foo.bar.baz) to grab the final handler reference.
26+ function resolveHandler ( userApp , handlerPath ) {
27+ return handlerPath . split ( '.' ) . reduce ( ( acc , key ) => {
28+ if ( acc == null ) {
29+ return undefined ;
30+ }
31+ return acc [ key ] ;
32+ } , userApp ) ;
33+ }
34+
35+ // Resolve the handler module even if Node's default lookup no longer includes it.
36+ function resolveModuleFile ( modulePath ) {
37+ try {
38+ const resolved = require . resolve ( modulePath ) ;
39+ logDebug ( `Resolved handler module via require.resolve: ${ resolved } ` ) ;
40+ return resolved ;
41+ } catch ( err ) {
42+ if ( err ?. code !== 'MODULE_NOT_FOUND' ) {
43+ throw err ;
44+ }
45+ }
46+
47+ const extensions = [ '' , '.js' , '.cjs' , '.mjs' ] ;
48+ for ( const ext of extensions ) {
49+ const candidate = modulePath + ext ;
50+ if ( fs . existsSync ( candidate ) ) {
51+ logDebug ( `Resolved handler module via fs lookup: ${ candidate } ` ) ;
52+ return candidate ;
53+ }
54+ }
55+
56+ logDebug ( `Falling back to unresolved handler module path: ${ modulePath } ` ) ;
57+ return modulePath ;
58+ }
59+
60+ // Require the user module, retrying via dynamic import when the file is ESM-only.
61+ async function loadUserModule ( resolvedPath ) {
62+ try {
63+ logDebug ( `Attempting to require handler module: ${ resolvedPath } ` ) ;
64+ // eslint-disable-next-line global-require, import/no-dynamic-require
65+ return require ( resolvedPath ) ;
66+ } catch ( err ) {
67+ if ( err ?. code === 'ERR_REQUIRE_ESM' ) {
68+ const url = pathToFileURL ( resolvedPath ) . href ;
69+ logDebug (
70+ `Handler module is ESM. Retrying with dynamic import: ${ url } (${ err . message } )`
71+ ) ;
72+ return import ( url ) ;
73+ }
74+ logDebug (
75+ `Failed to require handler module (${ resolvedPath } ): ${ err ?. message } `
76+ ) ;
77+ throw err ;
78+ }
79+ }
80+
81+ // Resolve and return the original Lambda handler function.
82+ async function load ( appRoot = process . env . LAMBDA_TASK_ROOT , handlerString ) {
83+ const finalHandler = handlerString ?? process . env . CX_ORIGINAL_HANDLER ;
84+ if ( ! appRoot ) {
85+ throw new Error ( 'LAMBDA_TASK_ROOT is not defined' ) ;
86+ }
87+ if ( ! finalHandler ) {
88+ throw new Error ( 'CX_ORIGINAL_HANDLER is not defined' ) ;
89+ }
90+
91+ const [ modulePath , handlerPath ] = splitHandlerString ( finalHandler ) ;
92+ const absoluteModulePath = path . resolve ( appRoot , modulePath ) ;
93+ const resolvedModulePath = resolveModuleFile ( absoluteModulePath ) ;
94+ const userApp = await loadUserModule ( resolvedModulePath ) ;
95+ const handler = resolveHandler ( userApp , handlerPath ) ;
96+
97+ if ( typeof handler !== 'function' ) {
98+ throw new Error (
99+ `Handler "${ finalHandler } " resolved to "${ handler } " instead of a function`
100+ ) ;
101+ }
102+
103+ logDebug ( `Successfully loaded handler ${ finalHandler } ` ) ;
104+ return handler ;
105+ }
106+
107+ module . exports = { load } ;
0 commit comments