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): run parcel in a docker container #7842

Merged
merged 5 commits into from
May 7, 2020
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions buildspec-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ version: 0.2
phases:
install:
commands:
# Start docker daemon inside the container
- nohup /usr/bin/dockerd --host=unix:///var/run/docker.sock --host=tcp://127.0.0.1:2375 --storage-driver=overlay2&
- timeout 15 sh -c "until docker info; do echo .; sleep 1; done"

# Install yarn if it wasn't already present in the image
- yarn --version || npm -g install yarn
build:
Expand Down
4 changes: 4 additions & 0 deletions buildspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ version: 0.2
phases:
install:
commands:
# Start docker daemon inside the container
- nohup /usr/bin/dockerd --host=unix:///var/run/docker.sock --host=tcp://127.0.0.1:2375 --storage-driver=overlay2&
- timeout 15 sh -c "until docker info; do echo .; sleep 1; done"

# Install yarn if it wasn't already present in the image
- yarn --version || npm -g install yarn
pre_build:
Expand Down
9 changes: 1 addition & 8 deletions packages/@aws-cdk/aws-lambda-nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,7 @@

This library provides constructs for Node.js Lambda functions.

To use this module, you will need to add a dependency on `parcel-bundler` in your
`package.json`:

```
yarn add parcel-bundler@^1
# or
npm install parcel-bundler@^1
```
To use this module, you will need to have Docker installed.

### Node.js Function
Define a `NodejsFunction`:
Expand Down
123 changes: 88 additions & 35 deletions packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { findPkgPath, updatePkg } from './util';
import { findPkgPath } from './util';

/**
* Builder options
Expand Down Expand Up @@ -40,74 +40,127 @@ export interface BuilderOptions {
/**
* The node version to use as target for Babel
*/
readonly nodeVersion?: string;
readonly nodeVersion: string;

/**
* The docker tag of the node base image to use in the parcel-bundler docker image
*
* @see https://hub.docker.com/_/node/?tab=tags
*/
readonly nodeDockerTag: string;

/**
* The root of the project. This will be used as the source for the volume
* mounted in the Docker container.
*/
readonly projectRoot: string;
}

/**
* Builder
*/
export class Builder {
private readonly parcelBinPath: string;
private readonly pkgPath: string;

constructor(private readonly options: BuilderOptions) {
let parcelPkgPath: string;
try {
parcelPkgPath = require.resolve('parcel-bundler/package.json'); // This will throw if `parcel-bundler` cannot be found
} catch (err) {
throw new Error('It looks like parcel-bundler is not installed. Please install v1.x of parcel-bundler with yarn or npm.');
}
const parcelDir = path.dirname(parcelPkgPath);
const parcelPkg = JSON.parse(fs.readFileSync(parcelPkgPath, 'utf8'));
private readonly originalPkg: Buffer;

if (!parcelPkg.version || !/^1\./.test(parcelPkg.version)) { // Peer dependency on parcel v1.x
throw new Error(`This module has a peer dependency on parcel-bundler v1.x. Got v${parcelPkg.version}.`);
}
private readonly originalPkgJson: { [key: string]: any };

this.parcelBinPath = path.join(parcelDir, parcelPkg.bin.parcel);
constructor(private readonly options: BuilderOptions) {
// Original package.json
const pkgPath = findPkgPath();
if (!pkgPath) {
throw new Error('Cannot find a `package.json` in this project.');
}
this.pkgPath = pkgPath;
this.originalPkg = fs.readFileSync(this.pkgPath);
this.originalPkgJson = JSON.parse(this.originalPkg.toString());
}

/**
* Build with parcel in a Docker container
*/
public build(): void {
const pkgPath = findPkgPath();
let originalPkg;

try {
if (this.options.nodeVersion && pkgPath) {
// Update engines.node (Babel target)
originalPkg = updatePkg(pkgPath, {
engines: { node: `>= ${this.options.nodeVersion}` },
});
this.updatePkg();

const dockerBuildArgs = [
'build',
'--build-arg', `NODE_TAG=${this.options.nodeDockerTag}`,
'-t', 'parcel-bundler',
path.join(__dirname, '../parcel-bundler'),
];

const build = spawnSync('docker', dockerBuildArgs);

if (build.error) {
throw build.error;
}

if (build.status !== 0) {
throw new Error(`[Status ${build.status}] stdout: ${build.stdout?.toString().trim()}\n\n\nstderr: ${build.stderr?.toString().trim()}`);
}

const args = [
'build', this.options.entry,
'--out-dir', this.options.outDir,
const containerProjectRoot = '/project';
const containerOutDir = '/out';
const containerCacheDir = '/cache';
const containerEntryPath = path.join(containerProjectRoot, path.relative(this.options.projectRoot, path.resolve(this.options.entry)));

const dockerRunArgs = [
'run', '--rm',
'-v', `${this.options.projectRoot}:${containerProjectRoot}`,
'-v', `${path.resolve(this.options.outDir)}:${containerOutDir}`,
...(this.options.cacheDir ? ['-v', `${path.resolve(this.options.cacheDir)}:${containerCacheDir}`] : []),
'-w', path.dirname(containerEntryPath),
'parcel-bundler',
];
const parcelArgs = [
'parcel', 'build', containerEntryPath,
'--out-dir', containerOutDir,
'--out-file', 'index.js',
'--global', this.options.global,
'--target', 'node',
'--bundle-node-modules',
'--log-level', '2',
!this.options.minify && '--no-minify',
!this.options.sourceMaps && '--no-source-maps',
...this.options.cacheDir
? ['--cache-dir', this.options.cacheDir]
: [],
...(this.options.cacheDir ? ['--cache-dir', containerCacheDir] : []),
].filter(Boolean) as string[];

const parcel = spawnSync(this.parcelBinPath, args);
const parcel = spawnSync('docker', [...dockerRunArgs, ...parcelArgs]);

if (parcel.error) {
throw parcel.error;
}

if (parcel.status !== 0) {
throw new Error(parcel.stdout.toString().trim());
throw new Error(`[Status ${parcel.status}] stdout: ${parcel.stdout?.toString().trim()}\n\n\nstderr: ${parcel.stderr?.toString().trim()}`);
}
} catch (err) {
throw new Error(`Failed to build file at ${this.options.entry}: ${err}`);
} finally { // Always restore package.json to original
if (pkgPath && originalPkg) {
fs.writeFileSync(pkgPath, originalPkg);
}
this.restorePkg();
}
}

/**
* Updates the package.json to configure Parcel
*/
private updatePkg() {
const updateData: { [key: string]: any } = {};
// Update engines.node (Babel target)
updateData.engines = { node: `>= ${this.options.nodeVersion}` };

// Write new package.json
if (Object.keys(updateData).length !== 0) {
fs.writeFileSync(this.pkgPath, JSON.stringify({
...this.originalPkgJson,
...updateData,
}, null, 2));
}
}

private restorePkg() {
fs.writeFileSync(this.pkgPath, this.originalPkg);
}
}
31 changes: 28 additions & 3 deletions packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import { Builder } from './builder';
import { nodeMajorVersion, parseStackTrace } from './util';
import { findGitPath, nodeMajorVersion, parseStackTrace } from './util';

/**
* Properties for a NodejsFunction
Expand Down Expand Up @@ -65,6 +65,25 @@ export interface NodejsFunctionProps extends lambda.FunctionOptions {
* @default - `.cache` in the root directory
*/
readonly cacheDir?: string;

/**
* The docker tag of the node base image to use in the parcel-bundler docker image
*
* @see https://hub.docker.com/_/node/?tab=tags
*
* @default - the `process.versions.node` alpine image
*/
readonly nodeDockerTag?: string;

/**
* The root of the project. This will be used as the source for the volume
* mounted in the Docker container. If you specify this prop, ensure that
* this path includes `entry` and any module/dependencies used by your
* function otherwise bundling will not be possible.
*
* @default - the closest path containing a .git folder
*/
readonly projectRoot?: string;
}

/**
Expand All @@ -84,6 +103,10 @@ export class NodejsFunction extends lambda.Function {
? lambda.Runtime.NODEJS_12_X
: lambda.Runtime.NODEJS_10_X;
const runtime = props.runtime || defaultRunTime;
const projectRoot = props.projectRoot ?? findGitPath();
if (!projectRoot) {
throw new Error('Cannot find project root. Please specify it with `projectRoot`.');
}

// Build with Parcel
const builder = new Builder({
Expand All @@ -94,6 +117,8 @@ export class NodejsFunction extends lambda.Function {
sourceMaps: props.sourceMaps,
cacheDir: props.cacheDir,
nodeVersion: extractVersion(runtime),
nodeDockerTag: props.nodeDockerTag || `${process.versions.node}-alpine`,
projectRoot: path.dirname(path.resolve(projectRoot)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why dirname?

Copy link
Contributor Author

@jogold jogold May 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong... was because findGitPath() returned the path with .git/.

});
builder.build();

Expand Down Expand Up @@ -156,11 +181,11 @@ function findDefiningFile(): string {
/**
* Extracts the version from the runtime
*/
function extractVersion(runtime: lambda.Runtime): string | undefined {
function extractVersion(runtime: lambda.Runtime): string {
const match = runtime.name.match(/nodejs(\d+)/);

if (!match) {
return undefined;
throw new Error('Cannot extract version from runtime.');
}

return match[1];
Expand Down
37 changes: 17 additions & 20 deletions packages/@aws-cdk/aws-lambda-nodejs/lib/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as fs from 'fs';
import * as path from 'path';

// From https://github.com/errwischt/stacktrace-parser/blob/master/src/stack-trace-parser.js
const STACK_RE = /^\s*at (?:((?:\[object object\])?[^\\/]+(?: \[as \S+\])?) )?\(?(.*?):(\d+)(?::(\d+))?\)?\s*$/i;
Expand Down Expand Up @@ -50,35 +51,31 @@ export function nodeMajorVersion(): number {
}

/**
* Finds closest package.json path
* Finds the closest path containg a path
*/
export function findPkgPath(): string | undefined {
let pkgPath;
function findClosestPathContaining(p: string): string | undefined {
let closestPath;

for (const path of module.paths) {
pkgPath = path.replace(/node_modules$/, 'package.json');
if (fs.existsSync(pkgPath)) {
for (const nodeModulesPath of module.paths) {
closestPath = path.join(path.dirname(nodeModulesPath), p);
if (fs.existsSync(closestPath)) {
break;
}
}

return pkgPath;
return closestPath;
}

/**
* Updates the package.json and returns the original
* Finds closest package.json path
*/
export function updatePkg(pkgPath: string, data: any): Buffer {
const original = fs.readFileSync(pkgPath);

const pkgJson = JSON.parse(original.toString());

const updated = {
...pkgJson,
...data,
};

fs.writeFileSync(pkgPath, JSON.stringify(updated, null, 2));
export function findPkgPath(): string | undefined {
return findClosestPathContaining('package.json');
}

return original;
/**
* Finds closest .git/
*/
export function findGitPath(): string | undefined {
return findClosestPathContaining(`.git${path.sep}`);
}
1 change: 0 additions & 1 deletion packages/@aws-cdk/aws-lambda-nodejs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@
"cdk-build-tools": "0.0.0",
"cdk-integ-tools": "0.0.0",
"fs-extra": "^8.1.0",
"parcel-bundler": "^1.12.4",
"pkglint": "0.0.0"
},
"dependencies": {
Expand Down
10 changes: 10 additions & 0 deletions packages/@aws-cdk/aws-lambda-nodejs/parcel-bundler/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# runs the parcel-bundler npm package to package and install dependencies of nodejs lambda functions
ARG NODE_TAG
FROM node:${NODE_TAG}

RUN yarn global add parcel-bundler@^1

# add the global node_modules folder to NODE_PATH so that plugins can find parcel-bundler
ENV NODE_PATH /usr/local/share/.config/yarn/global/node_modules

CMD [ "parcel" ]
Loading