Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions src/UserFunction.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -146,29 +150,37 @@ 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) {
return extensionless;
}

// 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) {
return loaded;
}
}

// 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;
}
Expand Down
3 changes: 3 additions & 0 deletions test/handlers/typescript/basicCts.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
exports.handler = async (): Promise<string> => {
return 'basic-cts';
};
3 changes: 3 additions & 0 deletions test/handlers/typescript/basicMts.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function handler(): Promise<string> {
return 'basic-mts';
}
3 changes: 3 additions & 0 deletions test/handlers/typescript/basicTs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
exports.handler = async function handler(): Promise<string> {
return 'basic-ts';
};
3 changes: 3 additions & 0 deletions test/handlers/typescript/esm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function handler(): Promise<string> {
return 'basic-esm';
}
3 changes: 3 additions & 0 deletions test/handlers/typescript/esm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "module"
}
3 changes: 3 additions & 0 deletions test/handlers/typescript/precedenceMtsVsCts.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
exports.handler = async function handler(): Promise<string> {
return 'precedence-cts';
};
3 changes: 3 additions & 0 deletions test/handlers/typescript/precedenceMtsVsCts.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function handler(): Promise<string> {
return 'precedence-mts';
}
3 changes: 3 additions & 0 deletions test/handlers/typescript/precedenceTsVsMts.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function handler(): Promise<string> {
return 'precedence-mts';
}
3 changes: 3 additions & 0 deletions test/handlers/typescript/precedenceTsVsMts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
exports.handler = async function handler(): Promise<string> {
return 'precedence-ts';
};
119 changes: 119 additions & 0 deletions test/unit/UserFunctionTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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'),
Expand Down