From 17230b828e9a3d90d43e83dd9e5f35366e1c07b2 Mon Sep 17 00:00:00 2001 From: White Autumn Date: Tue, 11 Nov 2025 14:32:26 +0100 Subject: [PATCH] Add support for typescript handlers --- src/UserFunction.js | 30 +++-- test/handlers/typescript/basicCts.cts | 3 + test/handlers/typescript/basicMts.mts | 3 + test/handlers/typescript/basicTs.ts | 3 + test/handlers/typescript/esm/index.ts | 3 + test/handlers/typescript/esm/package.json | 3 + .../typescript/precedenceMtsVsCts.cts | 3 + .../typescript/precedenceMtsVsCts.mts | 3 + .../handlers/typescript/precedenceTsVsMts.mts | 3 + test/handlers/typescript/precedenceTsVsMts.ts | 3 + test/unit/UserFunctionTest.js | 119 ++++++++++++++++++ 11 files changed, 167 insertions(+), 9 deletions(-) create mode 100644 test/handlers/typescript/basicCts.cts create mode 100644 test/handlers/typescript/basicMts.mts create mode 100644 test/handlers/typescript/basicTs.ts create mode 100644 test/handlers/typescript/esm/index.ts create mode 100644 test/handlers/typescript/esm/package.json create mode 100644 test/handlers/typescript/precedenceMtsVsCts.cts create mode 100644 test/handlers/typescript/precedenceMtsVsCts.mts create mode 100644 test/handlers/typescript/precedenceTsVsMts.mts create mode 100644 test/handlers/typescript/precedenceTsVsMts.ts diff --git a/src/UserFunction.js b/src/UserFunction.js index a6e1631..cfa7dd4 100644 --- a/src/UserFunction.js +++ b/src/UserFunction.js @@ -122,12 +122,16 @@ function _hasFolderPackageJsonTypeModule(folder) { return _hasFolderPackageJsonTypeModule(path.resolve(folder, '..')); } -function _hasPackageJsonTypeModule(file) { - // File must have a .js extension - const jsPath = file + '.js'; - return fs.existsSync(jsPath) - ? _hasFolderPackageJsonTypeModule(path.resolve(path.dirname(jsPath))) - : false; +function _runtimeSupportsTypeScript() { + const minimumNodeVersion = 24; + + const version = process.versions?.node; + if (!version) { + return false; + } + + const major = parseInt(version.split('.')[0], 10); + return !Number.isNaN(major) && major >= minimumNodeVersion; } /** @@ -146,6 +150,8 @@ async function _tryRequire(appRoot, moduleRoot, module) { const lambdaStylePath = path.resolve(appRoot, moduleRoot, module); + const supportsTs = _runtimeSupportsTypeScript(); + // Extensionless files are loaded via require. const extensionless = _tryRequireFile(lambdaStylePath); if (extensionless) { @@ -153,7 +159,9 @@ async function _tryRequire(appRoot, moduleRoot, module) { } // If package.json type != module, .js files are loaded via require. - const pjHasModule = _hasPackageJsonTypeModule(lambdaStylePath); + const pjHasModule = _hasFolderPackageJsonTypeModule( + path.resolve(path.dirname(lambdaStylePath)), + ); if (!pjHasModule) { const loaded = _tryRequireFile(lambdaStylePath, '.js'); if (loaded) { @@ -161,14 +169,18 @@ async function _tryRequire(appRoot, moduleRoot, module) { } } - // If still not loaded, try .js, .mjs, and .cjs in that order. + // If still not loaded, try .js, .mjs, .cjs, .ts, .mts, and .cts in that order. // Files ending with .js are loaded as ES modules when the nearest parent package.json // file contains a top-level field "type" with a value of "module". // https://nodejs.org/api/packages.html#packages_type const loaded = (pjHasModule && (await _tryAwaitImport(lambdaStylePath, '.js'))) || (await _tryAwaitImport(lambdaStylePath, '.mjs')) || - _tryRequireFile(lambdaStylePath, '.cjs'); + _tryRequireFile(lambdaStylePath, '.cjs') || + (supportsTs && !pjHasModule && _tryRequireFile(lambdaStylePath, '.ts')) || + (supportsTs && pjHasModule && (await _tryAwaitImport(lambdaStylePath, '.ts'))) || + (supportsTs && (await _tryAwaitImport(lambdaStylePath, '.mts'))) || + (supportsTs && _tryRequireFile(lambdaStylePath, '.cts')); if (loaded) { return loaded; } diff --git a/test/handlers/typescript/basicCts.cts b/test/handlers/typescript/basicCts.cts new file mode 100644 index 0000000..c92a89c --- /dev/null +++ b/test/handlers/typescript/basicCts.cts @@ -0,0 +1,3 @@ +exports.handler = async (): Promise => { + return 'basic-cts'; +}; diff --git a/test/handlers/typescript/basicMts.mts b/test/handlers/typescript/basicMts.mts new file mode 100644 index 0000000..1b49bd0 --- /dev/null +++ b/test/handlers/typescript/basicMts.mts @@ -0,0 +1,3 @@ +export async function handler(): Promise { + return 'basic-mts'; +} diff --git a/test/handlers/typescript/basicTs.ts b/test/handlers/typescript/basicTs.ts new file mode 100644 index 0000000..0850190 --- /dev/null +++ b/test/handlers/typescript/basicTs.ts @@ -0,0 +1,3 @@ +exports.handler = async function handler(): Promise { + return 'basic-ts'; +}; diff --git a/test/handlers/typescript/esm/index.ts b/test/handlers/typescript/esm/index.ts new file mode 100644 index 0000000..56f43f4 --- /dev/null +++ b/test/handlers/typescript/esm/index.ts @@ -0,0 +1,3 @@ +export async function handler(): Promise { + return 'basic-esm'; +} diff --git a/test/handlers/typescript/esm/package.json b/test/handlers/typescript/esm/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/test/handlers/typescript/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/test/handlers/typescript/precedenceMtsVsCts.cts b/test/handlers/typescript/precedenceMtsVsCts.cts new file mode 100644 index 0000000..6b6d495 --- /dev/null +++ b/test/handlers/typescript/precedenceMtsVsCts.cts @@ -0,0 +1,3 @@ +exports.handler = async function handler(): Promise { + return 'precedence-cts'; +}; diff --git a/test/handlers/typescript/precedenceMtsVsCts.mts b/test/handlers/typescript/precedenceMtsVsCts.mts new file mode 100644 index 0000000..8e90a8e --- /dev/null +++ b/test/handlers/typescript/precedenceMtsVsCts.mts @@ -0,0 +1,3 @@ +export async function handler(): Promise { + return 'precedence-mts'; +} diff --git a/test/handlers/typescript/precedenceTsVsMts.mts b/test/handlers/typescript/precedenceTsVsMts.mts new file mode 100644 index 0000000..8e90a8e --- /dev/null +++ b/test/handlers/typescript/precedenceTsVsMts.mts @@ -0,0 +1,3 @@ +export async function handler(): Promise { + return 'precedence-mts'; +} diff --git a/test/handlers/typescript/precedenceTsVsMts.ts b/test/handlers/typescript/precedenceTsVsMts.ts new file mode 100644 index 0000000..6269dd2 --- /dev/null +++ b/test/handlers/typescript/precedenceTsVsMts.ts @@ -0,0 +1,3 @@ +exports.handler = async function handler(): Promise { + return 'precedence-ts'; +}; diff --git a/test/unit/UserFunctionTest.js b/test/unit/UserFunctionTest.js index 1916866..8500f07 100644 --- a/test/unit/UserFunctionTest.js +++ b/test/unit/UserFunctionTest.js @@ -19,6 +19,17 @@ const HANDLERS_ROOT = path.join(TEST_ROOT, 'handlers'); describe('UserFunction.load method', () => { const echoTestMessage = 'This is a echo test'; + + const runtimeSupportsTypeScript = () => { + const version = process.versions?.node; + if (!version) { + return false; + } + + const major = parseInt(version.split('.')[0], 10); + return !Number.isNaN(major) && major >= 24; + }; + it('should successfully load a user function', async () => { const handler = await UserFunction.load(HANDLERS_ROOT, 'core.echo'); const response = await handler(echoTestMessage); @@ -402,6 +413,114 @@ describe('UserFunction.load method', () => { response.should.equal('Hello from CJS!'); }); + describe('TypeScript handlers', function () { + before(function () { + if (!runtimeSupportsTypeScript()) { + this.skip(); + } + }); + + it('should successfully load a .ts handler without module type', async () => { + const handler = await UserFunction.load( + HANDLERS_ROOT, + 'typescript/basicTs.handler', + ); + + const response = await handler(); + response.should.equal('basic-ts'); + }); + + it('should successfully load a .ts handler inside a module package', async () => { + const handler = await UserFunction.load( + path.join(HANDLERS_ROOT, 'typescript', 'esm'), + 'index.handler', + ); + + const response = await handler(); + response.should.equal('basic-esm'); + }); + + it('should successfully load a .mts handler', async () => { + const handler = await UserFunction.load( + HANDLERS_ROOT, + 'typescript/basicMts.handler', + ); + + const response = await handler(); + response.should.equal('basic-mts'); + }); + + it('should successfully load a .cts handler', async () => { + const handler = await UserFunction.load( + HANDLERS_ROOT, + 'typescript/basicCts.handler', + ); + + const response = await handler(); + response.should.equal('basic-cts'); + }); + + it('should default to load the .ts file over the .mts module', async () => { + const handler = await UserFunction.load( + HANDLERS_ROOT, + 'typescript/precedenceTsVsMts.handler', + ); + + const response = await handler(); + response.should.equal('precedence-ts'); + }); + + it('should default to load the .mts file over the .cts module', async () => { + const handler = await UserFunction.load( + HANDLERS_ROOT, + 'typescript/precedenceMtsVsCts.handler', + ); + + const response = await handler(); + response.should.equal('precedence-mts'); + }); + }); + + describe('TypeScript handlers on unsupported runtimes', () => { + let originalNodeDescriptor; + + beforeEach(() => { + originalNodeDescriptor = Object.getOwnPropertyDescriptor( + process.versions, + 'node', + ); + }); + + afterEach(() => { + if (originalNodeDescriptor) { + Object.defineProperty(process.versions, 'node', originalNodeDescriptor); + } else { + delete process.versions.node; + } + }); + + it('should throw ImportModuleError for TypeScript handlers', async () => { + const versions = process.versions; + if (runtimeSupportsTypeScript()) { + Object.defineProperty(versions, 'node', { + value: '20.0.0', + configurable: true, + enumerable: originalNodeDescriptor + ? originalNodeDescriptor.enumerable + : true, + writable: originalNodeDescriptor + ? originalNodeDescriptor.writable + : false, + }); + } + + await UserFunction.load( + HANDLERS_ROOT, + 'typescript/basicTs.handler', + ).should.be.rejectedWith(ImportModuleError); + }); + }); + it('should fail when using require in .mjs', async () => { await UserFunction.load( path.join(HANDLERS_ROOT, 'pkg-less'),