diff --git a/src/pack-externals.ts b/src/pack-externals.ts index e4ffb0e5..5f82a8be 100644 --- a/src/pack-externals.ts +++ b/src/pack-externals.ts @@ -1,5 +1,6 @@ -import fse from 'fs-extra'; import path from 'path'; + +import fse from 'fs-extra'; import { compose, forEach, @@ -24,11 +25,11 @@ import { without, } from 'ramda'; -import * as Packagers from './packagers'; -import { JSONObject } from './types'; +import { getPackager } from './packagers'; import { findProjectRoot, findUp } from './utils'; -import EsbuildServerlessPlugin from './index'; +import type EsbuildServerlessPlugin from './index'; +import type { JSONObject } from './types'; function rebaseFileReferences(pathToPackageRoot: string, moduleVersion: string) { if (/^(?:file:[^/]{2}|\.\/|\.\.\/)/.test(moduleVersion)) { @@ -242,7 +243,7 @@ export async function packExternalModules(this: EsbuildServerlessPlugin) { path.relative(process.cwd(), path.join(findUp('package.json'), './package.json')); // Determine and create packager - const packager = await Packagers.get(this.buildOptions.packager); + const packager = await getPackager.call(this, this.buildOptions.packager); // Fetch needed original package.json sections const sectionNames = packager.copyPackageSectionNames; diff --git a/src/pack.ts b/src/pack.ts index a2e80587..65ca87aa 100644 --- a/src/pack.ts +++ b/src/pack.ts @@ -1,6 +1,7 @@ +import path from 'path'; + import fs from 'fs-extra'; import globby from 'globby'; -import path from 'path'; import { intersection, isEmpty, @@ -14,13 +15,15 @@ import { without, } from 'ramda'; import semver from 'semver'; -import EsbuildServerlessPlugin from '.'; + import { ONLY_PREFIX, SERVERLESS_FOLDER } from './constants'; import { doSharePath, flatDep, getDepsFromBundle, isESM } from './helper'; -import * as Packagers from './packagers'; -import { IFiles } from './types'; +import { getPackager } from './packagers'; import { humanSize, zip, trimExtension } from './utils'; +import type EsbuildServerlessPlugin from './index'; +import type { IFiles } from './types'; + function setFunctionArtifactPath(this: EsbuildServerlessPlugin, func, artifactPath) { const version = this.serverless.getVersion(); // Serverless changed the artifact path location in version 1.18 @@ -137,7 +140,7 @@ export async function pack(this: EsbuildServerlessPlugin) { } // 2) If individually is set, we'll optimize files and zip per-function - const packager = await Packagers.get(this.buildOptions.packager); + const packager = await getPackager.call(this, this.buildOptions.packager); // get a list of every function bundle const buildResults = this.buildResults; diff --git a/src/packagers/index.ts b/src/packagers/index.ts index 419aac38..7070caa8 100644 --- a/src/packagers/index.ts +++ b/src/packagers/index.ts @@ -1,44 +1,58 @@ /** * Factory for supported packagers. * - * All packagers must implement the following interface: + * All packagers must extend the Packager class. * - * interface Packager { - * - * static get lockfileName(): string; - * static get copyPackageSectionNames(): Array; - * static get mustCopyModules(): boolean; - * static getProdDependencies(cwd: string, depth: number = 1): BbPromise; - * static rebaseLockfile(pathToPackageRoot: string, lockfile: Object): void; - * static install(cwd: string): BbPromise; - * static prune(cwd: string): BbPromise; - * static runScripts(cwd: string, scriptNames): BbPromise; - * - * } + * @see Packager */ +import { memoizeWith } from 'ramda'; + +import { isPackagerId } from '../type-predicate'; + +import type EsbuildServerlessPlugin from '../index'; +import type { PackagerId } from '../types'; +import type { Packager } from './packager'; + +const packagerFactories: Record Promise> = { + async npm() { + const { NPM } = await import('./npm'); + + return new NPM(); + }, + async pnpm() { + const { Pnpm } = await import('./pnpm'); -import { Packager } from './packager'; -import { NPM } from './npm'; -import { Pnpm } from './pnpm'; -import { Yarn } from './yarn'; + return new Pnpm(); + }, + async yarn() { + const { Yarn } = await import('./yarn'); -const registeredPackagers = { - npm: new NPM(), - pnpm: new Pnpm(), - yarn: new Yarn(), + return new Yarn(); + }, }; /** - * Factory method. - * @this ServerlessWebpack - Active plugin instance - * @param {string} packagerId - Well known packager id. - * @returns {Promise} - Promised packager to allow packagers be created asynchronously. + * Asynchronously create a Packager instance and memoize it. + * + * @this EsbuildServerlessPlugin - Active plugin instance + * @param {string} packagerId - Well known packager id + * @returns {Promise} - The selected Packager */ -export function get(packagerId: string): Promise { - if (!(packagerId in registeredPackagers)) { - const message = `Could not find packager '${packagerId}'`; - this.log.error(`ERROR: ${message}`); - throw new this.serverless.classes.Error(message); +export const getPackager = memoizeWith( + (packagerId) => packagerId, + async function (this: EsbuildServerlessPlugin, packagerId: PackagerId): Promise { + this.log.debug(`Trying to create packager: ${packagerId}`); + + if (!isPackagerId(packagerId)) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Serverless typings (as of v3.0.2) are incorrect + throw new this.serverless.classes.Error(`Could not find packager '${packagerId}'`); + } + + const packager = await packagerFactories[packagerId](); + + this.log.debug(`Packager created: ${packagerId}`); + + return packager; } - return registeredPackagers[packagerId]; -} +); diff --git a/src/tests/packagers/index.test.ts b/src/tests/packagers/index.test.ts new file mode 100644 index 00000000..7f929f2d --- /dev/null +++ b/src/tests/packagers/index.test.ts @@ -0,0 +1,17 @@ +import { getPackager } from '../../packagers'; + +import type EsbuildServerlessPlugin from '../../index'; + +describe('getPackager()', () => { + const mockPlugin = { + log: { + debug: jest.fn(), + }, + } as unknown as EsbuildServerlessPlugin; + + it('Returns a Packager instance', async () => { + const npm = await getPackager.call(mockPlugin, 'npm'); + + expect(npm).toEqual(expect.any(Object)); + }); +}); diff --git a/src/tests/type-predicate.test.ts b/src/tests/type-predicate.test.ts new file mode 100644 index 00000000..a42d1f4c --- /dev/null +++ b/src/tests/type-predicate.test.ts @@ -0,0 +1,15 @@ +import { isPackagerId } from '../type-predicate'; + +describe('isPackagerId()', () => { + it('Returns true for valid input', () => { + ['npm', 'pnpm', 'yarn'].forEach((id) => { + expect(isPackagerId(id)).toBeTruthy(); + }); + }); + + it('Returns false for invalid input', () => { + ['not-a-real-packager-id', false, 123, [], {}].forEach((id) => { + expect(isPackagerId(id)).toBeFalsy(); + }); + }); +}); diff --git a/src/type-predicate.ts b/src/type-predicate.ts new file mode 100644 index 00000000..b7426427 --- /dev/null +++ b/src/type-predicate.ts @@ -0,0 +1,5 @@ +import type { PackagerId } from './types'; + +export function isPackagerId(input: unknown): input is PackagerId { + return input === 'npm' || input === 'pnpm' || input === 'yarn'; +} diff --git a/src/types.ts b/src/types.ts index 5c7ee73e..84c42a69 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ -import { BuildOptions, BuildResult, Plugin } from 'esbuild'; -import Serverless from 'serverless'; +import type { BuildOptions, BuildResult, Plugin } from 'esbuild'; +import type Serverless from 'serverless'; export type ConfigFn = (sls: Serverless) => Configuration; @@ -74,3 +74,5 @@ export interface IFile { readonly rootPath: string; } export type IFiles = readonly IFile[]; + +export type PackagerId = 'npm' | 'pnpm' | 'yarn';