Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lambda-nodejs): pnpm support #14772

Merged
merged 4 commits into from
May 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions packages/@aws-cdk/aws-lambda-nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,9 @@ when working with the AWS SDK for JavaScript. Set the `awsSdkConnectionReuse` pr

## Lock file

The `NodejsFunction` requires a dependencies lock file (`yarn.lock` or
`package-lock.json`). When bundling in a Docker container, the path containing this
lock file is used as the source (`/asset-input`) for the volume mounted in the
container.
The `NodejsFunction` requires a dependencies lock file (`yarn.lock`, `pnpm-lock.yaml` or
`package-lock.json`). When bundling in a Docker container, the path containing this lock file is
used as the source (`/asset-input`) for the volume mounted in the container.

By default, the construct will try to automatically determine your project lock file.
Alternatively, you can specify the `depsLockFilePath` prop manually. In this
Expand Down Expand Up @@ -114,8 +113,9 @@ new lambda.NodejsFunction(this, 'my-handler', {
```

The modules listed in `nodeModules` must be present in the `package.json`'s dependencies or
installed. The same version will be used for installation. The lock file (`yarn.lock` or
`package-lock.json`) will be used along with the right installer (`yarn` or `npm`).
installed. The same version will be used for installation. The lock file (`yarn.lock`,
`pnpm-lock.yaml` or `package-lock.json`) will be used along with the right installer (`yarn`,
`pnpm` or `npm`).

When working with `nodeModules` using native dependencies, you might want to force bundling in a
Docker container even if `esbuild` is available in your environment. This can be done by setting
Expand Down Expand Up @@ -219,7 +219,7 @@ new lambda.NodejsFunction(this, 'my-handler', {
```

This image should have `esbuild` installed **globally**. If you plan to use `nodeModules` it
should also have `npm` or `yarn` depending on the lock file you're using.
should also have `npm`, `yarn` or `pnpm` depending on the lock file you're using.

Use the [default image provided by `@aws-cdk/aws-lambda-nodejs`](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-lambda-nodejs/lib/Dockerfile)
as a source of inspiration.
5 changes: 4 additions & 1 deletion packages/@aws-cdk/aws-lambda-nodejs/lib/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
# The correct AWS SAM build image based on the runtime of the function will be
# passed as build arg. The default allows to do `docker build .` when testing.
ARG IMAGE=public.ecr.aws/sam/build-nodejs12.x
ARG IMAGE=public.ecr.aws/sam/build-nodejs14.x
FROM $IMAGE

# Install yarn
RUN npm install --global yarn@1.22.5

# Install pnpm
RUN npm install --global pnpm

# Install esbuild
# (unsafe-perm because esbuild has a postinstall script)
ARG ESBUILD_VERSION=0
Expand Down
46 changes: 28 additions & 18 deletions packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,28 +84,11 @@ export class NodejsFunction extends lambda.Function {
throw new Error('Only `NODEJS` runtimes are supported.');
}

// Find lock file
let depsLockFilePath: string;
if (props.depsLockFilePath) {
if (!fs.existsSync(props.depsLockFilePath)) {
throw new Error(`Lock file at ${props.depsLockFilePath} doesn't exist`);
}
if (!fs.statSync(props.depsLockFilePath).isFile()) {
throw new Error('`depsLockFilePath` should point to a file');
}
depsLockFilePath = path.resolve(props.depsLockFilePath);
} else {
const lockFile = findUp(PackageManager.YARN.lockFile) ?? findUp(PackageManager.NPM.lockFile);
if (!lockFile) {
throw new Error('Cannot find a package lock file (`yarn.lock` or `package-lock.json`). Please specify it with `depsFileLockPath`.');
}
depsLockFilePath = lockFile;
}

// Entry and defaults
const entry = path.resolve(findEntry(id, props.entry));
const handler = props.handler ?? 'handler';
const runtime = props.runtime ?? lambda.Runtime.NODEJS_14_X;
const depsLockFilePath = findLockFile(props.depsLockFilePath);

super(scope, id, {
...props,
Expand All @@ -126,6 +109,33 @@ export class NodejsFunction extends lambda.Function {
}
}

/**
* Checks given lock file or searches for a lock file
*/
function findLockFile(depsLockFilePath?: string): string {
if (depsLockFilePath) {
if (!fs.existsSync(depsLockFilePath)) {
throw new Error(`Lock file at ${depsLockFilePath} doesn't exist`);
}

if (!fs.statSync(depsLockFilePath).isFile()) {
throw new Error('`depsLockFilePath` should point to a file');
}

return path.resolve(depsLockFilePath);
}

const lockFile = findUp(PackageManager.PNPM.lockFile)
?? findUp(PackageManager.YARN.lockFile)
?? findUp(PackageManager.NPM.lockFile);

if (!lockFile) {
throw new Error('Cannot find a package lock file (`pnpm-lock.yaml`, `yarn.lock` or `package-lock.json`). Please specify it with `depsFileLockPath`.');
}

return lockFile;
}

/**
* Searches for an entry file. Preference order is the following:
* 1. Given entry file
Expand Down
8 changes: 8 additions & 0 deletions packages/@aws-cdk/aws-lambda-nodejs/lib/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ export class PackageManager {
runCommand: ['yarn', 'run'],
});

public static PNPM = new PackageManager({
lockFile: 'pnpm-lock.yaml',
installCommand: ['pnpm', 'install'],
runCommand: ['pnpm', 'run'],
});

public static fromLockFile(lockFilePath: string): PackageManager {
const lockFile = path.basename(lockFilePath);

Expand All @@ -31,6 +37,8 @@ export class PackageManager {
return PackageManager.NPM;
case PackageManager.YARN.lockFile:
return PackageManager.YARN;
case PackageManager.PNPM.lockFile:
return PackageManager.PNPM;
default:
return PackageManager.NPM;
}
Expand Down
21 changes: 21 additions & 0 deletions packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,27 @@ test('Detects yarn.lock', () => {
});
});

test('Detects pnpm-lock.yaml', () => {
const pnpmLock = '/project/pnpm-lock.yaml';
Bundling.bundle({
entry: __filename,
depsLockFilePath: pnpmLock,
runtime: Runtime.NODEJS_12_X,
nodeModules: ['delay'],
forceDockerBundling: true,
});

// Correctly bundles with esbuild
expect(Code.fromAsset).toHaveBeenCalledWith(path.dirname(pnpmLock), {
assetHashType: AssetHashType.OUTPUT,
bundling: expect.objectContaining({
command: expect.arrayContaining([
expect.stringMatching(/pnpm-lock\.yaml.+pnpm install/),
]),
}),
});
});

test('with Docker build args', () => {
Bundling.bundle({
entry,
Expand Down
13 changes: 13 additions & 0 deletions packages/@aws-cdk/aws-lambda-nodejs/test/docker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ test('can yarn install with non root user', () => {
expect(proc.status).toEqual(0);
});

test('can pnpm install with non root user', () => {
const proc = spawnSync('docker', [
'run', '-u', '500:500',
'esbuild',
'bash', '-c', [
'mkdir /tmp/test',
'cd /tmp/test',
'pnpm add constructs',
].join(' && '),
]);
expect(proc.status).toEqual(0);
});

test('cache folders have the right permissions', () => {
const proc = spawnSync('docker', [
'run', 'esbuild',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,15 @@ test('from a yarn.lock', () => {
expect(packageManager.runBinCommand('my-bin')).toBe('yarn run my-bin');
});

test('defaults to NPM', () => {
test('from a pnpm-lock.yaml', () => {
const packageManager = PackageManager.fromLockFile('/path/to/pnpm-lock.yaml');
expect(packageManager).toEqual(PackageManager.PNPM);

expect(packageManager.runBinCommand('my-bin')).toBe('pnpm run my-bin');
});

test('defaults to NPM', () => {
const packageManager = PackageManager.fromLockFile('/path/to/other.lock');
expect(packageManager).toEqual(PackageManager.NPM);
});

Expand Down