From d0e3caba632d76307c17eb07129f5792b008ab97 Mon Sep 17 00:00:00 2001 From: lulzneko Date: Fri, 1 Oct 2021 16:12:13 +0900 Subject: [PATCH] feat(packager): add support for pnpm (#204) --- README.md | 10 ++-- src/pack.ts | 4 +- src/packagers/index.ts | 4 +- src/packagers/pnpm.ts | 124 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 src/packagers/pnpm.ts diff --git a/README.md b/README.md index 0377dcb1..862b34ac 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ _Note_: The default JavaScript syntax target is determined from serverless provi yarn add --dev serverless-esbuild # or npm install -D serverless-esbuild +# or +pnpm install -D serverless-esbuild ``` Add the following plugin to your `serverless.yml`: @@ -62,9 +64,9 @@ Packages that are marked as `external` and exist in the package.json's `dependen ```yml custom: esbuild: - packager: yarn # optional - npm or yarn, default is npm + packager: yarn # optional - npm, pnpm or yarn, default is npm packagePath: absolute/path/to/package.json # optional - by default it looks for a package.json in the working directory - packagerOptions: # optional - packager related options, currently supports only 'scripts' for both npm and yarn + packagerOptions: # optional - packager related options, currently supports only 'scripts' for both npm, pnpm and yarn scripts: # scripts to be executed, can be a string or array of strings - echo 'Hello World!' - rm -rf node_modules @@ -208,10 +210,10 @@ Options are: These options belong under `custom.esbuild` in your `serverless.yml` or `serverless.ts` file, and are specific to this plugin (these are not esbuild API options): -- `packager`: Package to use (npm or yarn - npm is default) +- `packager`: Package to use (npm, pnpm or yarn - npm is default) - `packagePath`: Path to the `package.json` file (`./package.json` is default) - `packagerOptions`: - - `scripts`: A string or array of scripts to be executed, currently only supports 'scripts' for npm and yarn + - `scripts`: A string or array of scripts to be executed, currently only supports 'scripts' for npm, pnpm and yarn - `exclude`: An array of dependencies to exclude (declares it as an external as well as excludes it from Lambda ZIP file) ## Author diff --git a/src/pack.ts b/src/pack.ts index 47cb81ef..b998a9c3 100644 --- a/src/pack.ts +++ b/src/pack.ts @@ -36,7 +36,7 @@ function setFunctionArtifactPath(this: EsbuildServerlessPlugin, func, artifactPa } } -const excludedFilesDefault = ['package-lock.json', 'yarn.lock', 'package.json']; +const excludedFilesDefault = ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'package.json']; export async function pack(this: EsbuildServerlessPlugin) { // GOOGLE Provider requires a package.json and NO node_modules @@ -100,7 +100,7 @@ export async function pack(this: EsbuildServerlessPlugin) { // get the list of externals to include only if exclude is not set to * if (this.buildOptions.exclude !== '*' && !this.buildOptions.exclude.includes('*')) { externals = without(this.buildOptions.exclude, this.buildOptions.external); - } + } const hasExternals = !!externals?.length; diff --git a/src/packagers/index.ts b/src/packagers/index.ts index ec70d3a0..0f3b8ac2 100644 --- a/src/packagers/index.ts +++ b/src/packagers/index.ts @@ -19,11 +19,13 @@ import { Packager } from './packager'; import { NPM } from './npm'; +import { Pnpm } from './pnpm'; import { Yarn } from './yarn'; const registeredPackagers = { npm: new NPM(), - yarn: new Yarn() + pnpm: new Pnpm(), + yarn: new Yarn(), }; /** diff --git a/src/packagers/pnpm.ts b/src/packagers/pnpm.ts new file mode 100644 index 00000000..dbbfe1e8 --- /dev/null +++ b/src/packagers/pnpm.ts @@ -0,0 +1,124 @@ +import { any, isEmpty, reduce, replace, split, startsWith } from 'ramda'; + +import { JSONObject } from '../types'; +import { SpawnError, spawnProcess } from '../utils'; +import { Packager } from './packager'; + +/** + * pnpm packager. + */ +export class Pnpm implements Packager { + get lockfileName() { + return 'pnpm-lock.yaml'; + } + + get copyPackageSectionNames() { + return []; + } + + get mustCopyModules() { + return false; + } + + async getProdDependencies(cwd: string, depth?: number) { + // Get first level dependency graph + const command = /^win/.test(process.platform) ? 'pnpm.cmd' : 'pnpm'; + const args = [ + 'ls', + '--prod', // Only prod dependencies + '--json', + depth ? `--depth=${depth}` : null, + ].filter(Boolean); + + // If we need to ignore some errors add them here + const ignoredPnpmErrors = []; + + try { + const processOutput = await spawnProcess(command, args, { cwd }); + const depJson = processOutput.stdout; + + return JSON.parse(depJson); + } catch (err) { + if (err instanceof SpawnError) { + // Only exit with an error if we have critical npm errors for 2nd level inside + const errors = split('\n', err.stderr); + const failed = reduce( + (f, error) => { + if (f) { + return true; + } + return ( + !isEmpty(error) && + !any( + (ignoredError) => startsWith(`npm ERR! ${ignoredError.npmError}`, error), + ignoredPnpmErrors + ) + ); + }, + false, + errors + ); + + if (!failed && !isEmpty(err.stdout)) { + return { stdout: err.stdout }; + } + } + + throw err; + } + } + + _rebaseFileReferences(pathToPackageRoot: string, moduleVersion: string) { + if (/^file:[^/]{2}/.test(moduleVersion)) { + const filePath = replace(/^file:/, '', moduleVersion); + return replace(/\\/g, '/', `file:${pathToPackageRoot}/${filePath}`); + } + + return moduleVersion; + } + + /** + * We should not be modifying 'pnpm-lock.yaml' + * because this file should be treated as internal to pnpm. + */ + rebaseLockfile(pathToPackageRoot: string, lockfile: JSONObject) { + if (lockfile.version) { + lockfile.version = this._rebaseFileReferences(pathToPackageRoot, lockfile.version); + } + + if (lockfile.dependencies) { + for (const lockedDependency in lockfile.dependencies) { + this.rebaseLockfile(pathToPackageRoot, lockedDependency); + } + } + + return lockfile; + } + + async install(cwd, useLockfile = true) { + const command = /^win/.test(process.platform) ? 'pnpm.cmd' : 'pnpm'; + + const args = useLockfile ? ['install', '--frozen-lockfile'] : ['install']; + + await spawnProcess(command, args, { cwd }); + } + + async prune(cwd) { + const command = /^win/.test(process.platform) ? 'pnpm.cmd' : 'pnpm'; + const args = ['prune']; + + await spawnProcess(command, args, { cwd }); + } + + async runScripts(cwd, scriptNames) { + const command = /^win/.test(process.platform) ? 'pnpm.cmd' : 'pnpm'; + + await Promise.all( + scriptNames.map((scriptName) => { + const args = ['run', scriptName]; + + return spawnProcess(command, args, { cwd }); + }) + ); + } +}