From 6a97354977db600b6b6b23f1db0bbad33f111372 Mon Sep 17 00:00:00 2001 From: JohnGrisham Date: Fri, 1 Dec 2023 10:55:24 -0600 Subject: [PATCH 1/3] feat(resolveExtensions): added support for custom resolve extensions esbuild setting --- src/constants.ts | 1 + src/helper.ts | 6 ++++-- src/index.ts | 7 ++++++- src/tests/helper.test.ts | 29 +++++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index a1d29242..0e4b6413 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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']; diff --git a/src/helper.ts b/src/helper.ts index af3b66f7..c0fee898 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -11,6 +11,7 @@ import type Serverless from 'serverless'; import type ServerlessPlugin from 'serverless/classes/Plugin'; import type { Configuration, DependencyMap, FunctionEntry } from './types'; import type { EsbuildFunctionDefinitionHandler } from './types'; +import { DEFAULT_EXTENSIONS } from './constants'; export function asArray(data: T | T[]): T[] { return Array.isArray(data) ? data : [data]; @@ -27,7 +28,8 @@ export function assertIsString(input: unknown, message = 'input is not a string' export function extractFunctionEntries( cwd: string, provider: string, - functions: Record + functions: Record, + 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 @@ -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 diff --git a/src/index.ts b/src/index.ts index f8a7ba99..82ad92de 100644 --- a/src/index.ts +++ b/src/index.ts @@ -345,7 +345,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 { diff --git a/src/tests/helper.test.ts b/src/tests/helper.test.ts index cbec499d..d58fb6cf 100644 --- a/src/tests/helper.test.ts +++ b/src/tests/helper.test.ts @@ -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 = { From e42e7c808b59d6f8c676d6dfc0456294c5da77aa Mon Sep 17 00:00:00 2001 From: JohnGrisham Date: Fri, 1 Dec 2023 16:15:36 -0600 Subject: [PATCH 2/3] feat(resolveExtensions): added stripResolveExtensions option to remove file extension prefixes --- src/helper.ts | 17 ++++++++++++++++- src/pack.ts | 13 +++++++++++-- src/tests/helper.test.ts | 16 ++++++++++++++-- src/types.ts | 1 + 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/helper.ts b/src/helper.ts index c0fee898..624475d4 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -9,7 +9,7 @@ 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'; @@ -315,3 +315,18 @@ export const buildServerlessV3LoggerFromLegacyLogger = ( verbose: legacyLogger.log.bind(legacyLogger), success: legacyLogger.log.bind(legacyLogger), }); + +export const stripResolveExtensions = (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; +}; diff --git a/src/pack.ts b/src/pack.ts index 649eafc1..86d3fe77 100644 --- a/src/pack.ts +++ b/src/pack.ts @@ -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, stripResolveExtensions } from './helper'; import { getPackager } from './packagers'; import { humanSize, trimExtension, zip } from './utils'; @@ -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.options.stripResolveExtensions) { + return stripResolveExtensions(file, this.buildOptions.resolveExtensions); + } + } + + return file; + }); if (isEmpty(files)) { this.log.verbose('Packaging: No files found. Skipping esbuild.'); diff --git a/src/tests/helper.test.ts b/src/tests/helper.test.ts index d58fb6cf..54d960d4 100644 --- a/src/tests/helper.test.ts +++ b/src/tests/helper.test.ts @@ -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, stripResolveExtensions } from '../helper'; -import type { Configuration, DependencyMap } from '../types'; +import type { Configuration, DependencyMap, IFile } from '../types'; jest.mock('fs-extra'); @@ -643,3 +643,15 @@ describe('flatDeps', () => { }); }); }); + +describe('stripResolveExtensions', () => { + it('should remove custom extension prefixes', () => { + const result = stripResolveExtensions({ 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 = stripResolveExtensions({ localPath: 'test.other.js' } as IFile, ['.custom.js']); + expect(result.localPath).toEqual('test.other.js'); + }); +}); diff --git a/src/types.ts b/src/types.ts index e6a16722..8990b122 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,6 +9,7 @@ export type ReturnPluginsFn = (sls: Serverless) => Plugins; export interface ImprovedServerlessOptions extends Serverless.Options { package?: string; + stripResolveExtensions?: boolean; } export interface WatchConfiguration { From 061bc8427e6128f96d9f6c5d7a7142bc7370b10c Mon Sep 17 00:00:00 2001 From: JohnGrisham Date: Mon, 4 Dec 2023 09:33:43 -0600 Subject: [PATCH 3/3] feat(resolveExtensions): renamed to stripEntryResolveExtensions as esbuild setting, updated readme --- README.md | 1 + src/bundle.ts | 1 + src/helper.ts | 2 +- src/index.ts | 1 + src/pack.ts | 6 +++--- src/tests/helper.test.ts | 8 ++++---- src/types.ts | 2 +- 7 files changed, 12 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d1d679bb..a5758eac 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/bundle.ts b/src/bundle.ts index e8824cf1..8b0cf67c 100644 --- a/src/bundle.ts +++ b/src/bundle.ts @@ -39,6 +39,7 @@ export async function bundle(this: EsbuildServerlessPlugin): Promise { 'nodeExternals', 'skipBuild', 'skipBuildExcludeFns', + 'stripEntryResolveExtensions', ].reduce>((options, optionName) => { const { [optionName]: _, ...rest } = options; diff --git a/src/helper.ts b/src/helper.ts index 624475d4..1ef3a124 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -316,7 +316,7 @@ export const buildServerlessV3LoggerFromLegacyLogger = ( success: legacyLogger.log.bind(legacyLogger), }); -export const stripResolveExtensions = (file: IFile, extensions: string[]): IFile => { +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])) { diff --git a/src/index.ts b/src/index.ts index 82ad92de..e4e3c35c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -320,6 +320,7 @@ class EsbuildServerlessPlugin implements ServerlessPlugin { outputFileExtension: '.js', skipBuild: false, skipBuildExcludeFns: [], + stripEntryResolveExtensions: false, }; const providerRuntime = this.serverless.service.provider.runtime; diff --git a/src/pack.ts b/src/pack.ts index 86d3fe77..d0dce850 100644 --- a/src/pack.ts +++ b/src/pack.ts @@ -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, stripResolveExtensions } from './helper'; +import { assertIsString, doSharePath, flatDep, getDepsFromBundle, isESM, stripEntryResolveExtensions } from './helper'; import { getPackager } from './packagers'; import { humanSize, trimExtension, zip } from './utils'; @@ -117,8 +117,8 @@ export async function pack(this: EsbuildServerlessPlugin) { .map((localPath) => ({ localPath, rootPath: path.join(buildDirPath, localPath) })) .map((file) => { if (this.buildOptions?.resolveExtensions && this.buildOptions.resolveExtensions.length > 0) { - if (this.options.stripResolveExtensions) { - return stripResolveExtensions(file, this.buildOptions.resolveExtensions); + if (this.buildOptions.stripEntryResolveExtensions) { + return stripEntryResolveExtensions(file, this.buildOptions.resolveExtensions); } } diff --git a/src/tests/helper.test.ts b/src/tests/helper.test.ts index 54d960d4..9709d464 100644 --- a/src/tests/helper.test.ts +++ b/src/tests/helper.test.ts @@ -2,7 +2,7 @@ import fs from 'fs-extra'; import os from 'os'; import path from 'path'; -import { extractFunctionEntries, flatDep, getDepsFromBundle, isESM, stripResolveExtensions } from '../helper'; +import { extractFunctionEntries, flatDep, getDepsFromBundle, isESM, stripEntryResolveExtensions } from '../helper'; import type { Configuration, DependencyMap, IFile } from '../types'; @@ -644,14 +644,14 @@ describe('flatDeps', () => { }); }); -describe('stripResolveExtensions', () => { +describe('stripEntryResolveExtensions', () => { it('should remove custom extension prefixes', () => { - const result = stripResolveExtensions({ localPath: 'test.custom.js' } as IFile, ['.custom.js']); + 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 = stripResolveExtensions({ localPath: 'test.other.js' } as IFile, ['.custom.js']); + const result = stripEntryResolveExtensions({ localPath: 'test.other.js' } as IFile, ['.custom.js']); expect(result.localPath).toEqual('test.other.js'); }); }); diff --git a/src/types.ts b/src/types.ts index 8990b122..6b5bf3f4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,7 +9,6 @@ export type ReturnPluginsFn = (sls: Serverless) => Plugins; export interface ImprovedServerlessOptions extends Serverless.Options { package?: string; - stripResolveExtensions?: boolean; } export interface WatchConfiguration { @@ -48,6 +47,7 @@ export interface Configuration extends EsbuildOptions { nodeExternals?: NodeExternalsOptions; skipBuild?: boolean; skipBuildExcludeFns: string[]; + stripEntryResolveExtensions?: boolean; } export interface EsbuildFunctionDefinitionHandler extends Serverless.FunctionDefinitionHandler {