Skip to content

Commit

Permalink
feat(packager): add support for pnpm (#204)
Browse files Browse the repository at this point in the history
  • Loading branch information
lulzneko authored Oct 1, 2021
1 parent fbd9981 commit d0e3cab
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 7 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<string>(this.buildOptions.exclude, this.buildOptions.external);
}
}

const hasExternals = !!externals?.length;

Expand Down
4 changes: 3 additions & 1 deletion src/packagers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
};

/**
Expand Down
124 changes: 124 additions & 0 deletions src/packagers/pnpm.ts
Original file line number Diff line number Diff line change
@@ -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 });
})
);
}
}

0 comments on commit d0e3cab

Please sign in to comment.