Skip to content

Commit

Permalink
feat(lambda-nodejs): run parcel in a docker container
Browse files Browse the repository at this point in the history
Redo of aws#7169.
  • Loading branch information
jogold committed May 7, 2020
1 parent e277bbd commit 84914ed
Show file tree
Hide file tree
Showing 10 changed files with 442 additions and 2,935 deletions.
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
117 changes: 82 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 { findGitPath, findPkgPath } from './util';

/**
* Builder options
Expand Down Expand Up @@ -40,74 +40,121 @@ 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;
}

/**
* 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
this.pkgPath = findPkgPath();
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,
// Find the git root and mount it in the container. This allows Parcel to
// find the same modules/dependencies as the ones available "locally". It
// also supports monorepos.
const projectRoot = path.dirname(findGitPath());
const containerProjectRoot = '/project';
const containerOutDir = '/out';
const containerCacheDir = '/cache';
const containerEntryPath = path.join(containerProjectRoot, path.relative(projectRoot, path.resolve(this.options.entry)));

const dockerRunArgs = [
'run', '--rm',
'-v', `${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);
}
}
14 changes: 12 additions & 2 deletions packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ 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;
}

/**
Expand Down Expand Up @@ -94,6 +103,7 @@ export class NodejsFunction extends lambda.Function {
sourceMaps: props.sourceMaps,
cacheDir: props.cacheDir,
nodeVersion: extractVersion(runtime),
nodeDockerTag: props.nodeDockerTag || `${process.versions.node}-alpine`,
});
builder.build();

Expand Down Expand Up @@ -156,11 +166,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
41 changes: 21 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,35 @@ 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 {
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;
if (!closestPath) {
throw new Error(`Cannot find path ${p}.`);
}

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 {
return findClosestPathContaining('package.json');
}

return original;
/**
* Finds closest .git/ and returns the path containing this directory
*/
export function findGitPath(): string {
return findClosestPathContaining(`.git${path.sep}`);
}
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-lambda-nodejs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,11 @@
"license": "Apache-2.0",
"devDependencies": {
"@aws-cdk/assert": "0.0.0",
"@aws-cdk/aws-iam": "0.0.0",
"aws-sdk": "^2.667.0",
"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
7 changes: 7 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,7 @@
# 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

CMD [ "parcel" ]

0 comments on commit 84914ed

Please sign in to comment.