Skip to content

Commit

Permalink
feat: add support for resolve extensions esbuild setting (#510)
Browse files Browse the repository at this point in the history
* feat(resolveExtensions): added support for custom resolve extensions esbuild setting

* feat(resolveExtensions): added stripResolveExtensions option to remove file extension prefixes

* feat(resolveExtensions): renamed to stripEntryResolveExtensions as esbuild setting, updated readme
  • Loading branch information
JohnGrisham committed Dec 5, 2023
1 parent 901dcfc commit 31dd88f
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 8 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ See [example folder](examples) for some example configurations.
| `watch` | Watch options for `serverless-offline`. | [Watch Options](#watch-options) |
| `skipBuild` | Avoid rebuilding lambda artifacts in favor of reusing previous build artifacts. | `false` |
| `skipBuildExcludeFns` | An array of lambda names that will always be rebuilt if `skipBuild` is set to `true` and bundling individually. This is helpful for dynamically generated functions like serverless-plugin-warmup. | `[]` |
| `stripEntryResolveExtensions` | A boolean that determines if entrypoints using custom file extensions provided in the `resolveExtensions` ESbuild setting should be stripped of their custom extension upon packing the final bundle for that file. Example: `myLambda.custom.ts` would result in `myLambda.js` instead of `myLambda.custom.js`.

#### Default Esbuild Options

Expand Down
1 change: 1 addition & 0 deletions src/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export async function bundle(this: EsbuildServerlessPlugin): Promise<void> {
'nodeExternals',
'skipBuild',
'skipBuildExcludeFns',
'stripEntryResolveExtensions',
].reduce<Record<string, any>>((options, optionName) => {
const { [optionName]: _, ...rest } = options;

Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export const SERVERLESS_FOLDER = '.serverless';
export const BUILD_FOLDER = '.build';
export const WORK_FOLDER = '.esbuild';
export const ONLY_PREFIX = '__only_';
export const DEFAULT_EXTENSIONS = ['.ts', '.js', '.jsx', '.tsx'];
23 changes: 20 additions & 3 deletions src/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import { uniq } from 'ramda';

import type Serverless from 'serverless';
import type ServerlessPlugin from 'serverless/classes/Plugin';
import type { Configuration, DependencyMap, FunctionEntry } from './types';
import type { Configuration, DependencyMap, FunctionEntry, IFile } from './types';
import type { EsbuildFunctionDefinitionHandler } from './types';
import { DEFAULT_EXTENSIONS } from './constants';

export function asArray<T>(data: T | T[]): T[] {
return Array.isArray(data) ? data : [data];
Expand All @@ -27,7 +28,8 @@ export function assertIsString(input: unknown, message = 'input is not a string'
export function extractFunctionEntries(
cwd: string,
provider: string,
functions: Record<string, Serverless.FunctionDefinitionHandler>
functions: Record<string, Serverless.FunctionDefinitionHandler>,
resolveExtensions?: string[]
): FunctionEntry[] {
// The Google provider will use the entrypoint not from the definition of the
// handler function, but instead from the package.json:main field, or via a
Expand Down Expand Up @@ -69,7 +71,7 @@ export function extractFunctionEntries(
// replace only last instance to allow the same name for file and handler
const fileName = handler.substring(0, fnNameLastAppearanceIndex);

const extensions = ['.ts', '.js', '.jsx', '.tsx'];
const extensions = resolveExtensions ?? DEFAULT_EXTENSIONS;

for (const extension of extensions) {
// Check if the .{extension} files exists. If so return that to watch
Expand Down Expand Up @@ -313,3 +315,18 @@ export const buildServerlessV3LoggerFromLegacyLogger = (
verbose: legacyLogger.log.bind(legacyLogger),
success: legacyLogger.log.bind(legacyLogger),
});

export const stripEntryResolveExtensions = (file: IFile, extensions: string[]): IFile => {
const resolveExtensionMatch = file.localPath.match(extensions.map((ext) => ext).join('|'));

if (resolveExtensionMatch?.length && !DEFAULT_EXTENSIONS.includes(resolveExtensionMatch[0])) {
const extensionParts = resolveExtensionMatch[0].split('.');

return {
...file,
localPath: file.localPath.replace(resolveExtensionMatch[0], `.${extensionParts[extensionParts.length - 1]}`),
};
}

return file;
};
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ class EsbuildServerlessPlugin implements ServerlessPlugin {
outputFileExtension: '.js',
skipBuild: false,
skipBuildExcludeFns: [],
stripEntryResolveExtensions: false,
};

const providerRuntime = this.serverless.service.provider.runtime;
Expand All @@ -345,7 +346,12 @@ class EsbuildServerlessPlugin implements ServerlessPlugin {
}

get functionEntries() {
return extractFunctionEntries(this.serviceDirPath, this.serverless.service.provider.name, this.functions);
return extractFunctionEntries(
this.serviceDirPath,
this.serverless.service.provider.name,
this.functions,
this.buildOptions?.resolveExtensions
);
}

watch(): void {
Expand Down
13 changes: 11 additions & 2 deletions src/pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import semver from 'semver';
import type Serverless from 'serverless';

import { ONLY_PREFIX, SERVERLESS_FOLDER } from './constants';
import { assertIsString, doSharePath, flatDep, getDepsFromBundle, isESM } from './helper';
import { assertIsString, doSharePath, flatDep, getDepsFromBundle, isESM, stripEntryResolveExtensions } from './helper';
import { getPackager } from './packagers';
import { humanSize, trimExtension, zip } from './utils';

Expand Down Expand Up @@ -114,7 +114,16 @@ export async function pack(this: EsbuildServerlessPlugin) {
onlyFiles: true,
})
.filter((file) => !excludedFiles.includes(file))
.map((localPath) => ({ localPath, rootPath: path.join(buildDirPath, localPath) }));
.map((localPath) => ({ localPath, rootPath: path.join(buildDirPath, localPath) }))
.map((file) => {
if (this.buildOptions?.resolveExtensions && this.buildOptions.resolveExtensions.length > 0) {
if (this.buildOptions.stripEntryResolveExtensions) {
return stripEntryResolveExtensions(file, this.buildOptions.resolveExtensions);
}
}

return file;
});

if (isEmpty(files)) {
this.log.verbose('Packaging: No files found. Skipping esbuild.');
Expand Down
45 changes: 43 additions & 2 deletions src/tests/helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import fs from 'fs-extra';
import os from 'os';
import path from 'path';

import { extractFunctionEntries, flatDep, getDepsFromBundle, isESM } from '../helper';
import { extractFunctionEntries, flatDep, getDepsFromBundle, isESM, stripEntryResolveExtensions } from '../helper';

import type { Configuration, DependencyMap } from '../types';
import type { Configuration, DependencyMap, IFile } from '../types';

jest.mock('fs-extra');

Expand Down Expand Up @@ -135,6 +135,35 @@ describe('extractFunctionEntries', () => {
]);
});

it('should allow resolve extensions custom Esbuild setting', () => {
jest.mocked(fs.existsSync).mockReturnValue(true);
const functionDefinitions = {
function1: {
events: [],
handler: './file1.handler',
},
function2: {
events: [],
handler: './file2.handler',
},
};

const fileNames = extractFunctionEntries(cwd, 'aws', functionDefinitions, ['.custom.ts']);

expect(fileNames).toStrictEqual([
{
entry: 'file1.custom.ts',
func: functionDefinitions.function1,
functionAlias: 'function1',
},
{
entry: 'file2.custom.ts',
func: functionDefinitions.function2,
functionAlias: 'function2',
},
]);
});

it('should not return entries for handlers which have skipEsbuild set to true', async () => {
jest.mocked(fs.existsSync).mockReturnValue(true);
const functionDefinitions = {
Expand Down Expand Up @@ -614,3 +643,15 @@ describe('flatDeps', () => {
});
});
});

describe('stripEntryResolveExtensions', () => {
it('should remove custom extension prefixes', () => {
const result = stripEntryResolveExtensions({ localPath: 'test.custom.js' } as IFile, ['.custom.js']);
expect(result.localPath).toEqual('test.js');
});

it('should ignore prefixes not inside the resolve extensions list', () => {
const result = stripEntryResolveExtensions({ localPath: 'test.other.js' } as IFile, ['.custom.js']);
expect(result.localPath).toEqual('test.other.js');
});
});
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface Configuration extends EsbuildOptions {
nodeExternals?: NodeExternalsOptions;
skipBuild?: boolean;
skipBuildExcludeFns: string[];
stripEntryResolveExtensions?: boolean;
}

export interface EsbuildFunctionDefinitionHandler extends Serverless.FunctionDefinitionHandler {
Expand Down

0 comments on commit 31dd88f

Please sign in to comment.