Skip to content

Commit

Permalink
0.0.22 (#29)
Browse files Browse the repository at this point in the history
* chore: release

* feat: upload function code on initial run (#25)

* chore: release

* feat: add warning for when node_modules layer is out of sync (#26)

* chore: release

* doc: add docs on node modules layer (#27)

* chore: release

* Update README.md

* chore: release

Co-authored-by: kirkness <[email protected]>
  • Loading branch information
kirkness and planes-bot authored May 21, 2021
1 parent 72cd432 commit c3cc401
Show file tree
Hide file tree
Showing 10 changed files with 189 additions and 61 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Your code will be watched and built with the same esbuild config as when deploying your stack
- Simply switch-out your existing `NodejsFunction` with the `WatchableNodejsFunction` construct
- Opt-in to real-time logs, so no more digging through CloudWatch to find your Lambda's logs
- Load your node modules as a separate lambda layer (allows for faster build/upload times)
- Written in TypeScript
- No extra infrastructure required, unless you are opting-in to real-time logs

Expand Down Expand Up @@ -78,6 +79,30 @@ assign a Lambda Layer Extension to wrap your lambda, the wrapper will patch the
multiple lambdas in your stack it'll only create the require infrastructure
once, and reuse it for all lambdas that need it.

## Node Modules Layer

CDK-Watch allows you to install your node-modules as a stand alone layer. This
means that when you deploy, `cdk-watch` will install your modules in a separate
asset and install them as the lambda's layer. This is great for dev-performance
as the upload bundle will be much smaller. You can configure this using the
`bundling.nodeModulesLayer` property:

```ts
bundling: {
// Install only "knex" as a standalone layer
nodeModulesLayer: {include: ['knex']}
}
```

OR:

```ts
bundling: {
// Install every module found in your package.json except "knex"
nodeModulesLayer: {exclude: ['knex']}
}
```

## How, what & why?

### Why would you want to do this?
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"bin": {
"cdkw": "lib/cli.js"
},
"version": "0.0.21",
"version": "0.0.22-next.4",
"scripts": {
"type-check": "tsc -p tsconfig.json --noEmit",
"build": "tsup src/index.ts src/cli.ts src/index.ts src/websocketHandlers/index.ts src/lambda-extension/cdk-watch-lambda-wrapper/index.ts --no-splitting -d lib --clean --dts=src/index.ts",
Expand Down Expand Up @@ -39,6 +39,7 @@
"fs-extra": "^9.1.0",
"json5": "^2.2.0",
"minimatch": "^3.0.4",
"object-hash": "^2.1.1",
"reconnecting-websocket": "^4.4.0",
"stream-buffers": "^3.0.2",
"twisters": "^1.1.0",
Expand Down Expand Up @@ -74,6 +75,7 @@
"@types/jest": "^26.0.20",
"@types/minimatch": "^3.0.3",
"@types/node": "^14.14.25",
"@types/object-hash": "^2.1.0",
"@types/stream-buffers": "^3.0.3",
"@types/ws": "^7.4.0",
"esbuild": "^0.8.43",
Expand Down
40 changes: 37 additions & 3 deletions src/commands/__tests__/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ describe('CLI commands', () => {
test('program runs watch as the default command', async () => {
const program = new CdkWatchCommand('1');
await program.parseAsync(buildArgv('**'));
expect(watch).toBeCalledWith('**', {logs: true}, expect.anything());
expect(watch).toBeCalledWith(
'**',
expect.objectContaining({logs: true}),
expect.anything(),
);
expect(list).toBeCalledTimes(0);
expect(logs).toBeCalledTimes(0);
expect(once).toBeCalledTimes(0);
Expand All @@ -28,7 +32,37 @@ describe('CLI commands', () => {
test('program runs watch when command name is provided', async () => {
const program = new CdkWatchCommand('1');
await program.parseAsync(buildArgv('watch My/Path'));
expect(watch).toBeCalledWith('My/Path', {logs: true}, expect.anything());
expect(watch).toBeCalledWith(
'My/Path',
expect.objectContaining({logs: true}),
expect.anything(),
);
expect(list).toBeCalledTimes(0);
expect(logs).toBeCalledTimes(0);
expect(once).toBeCalledTimes(0);
});

test('program passes skip-initial boolean to watch function', async () => {
const program = new CdkWatchCommand('1');
await program.parseAsync(buildArgv('watch My/Path --skip-initial'));
expect(watch).toBeCalledWith(
'My/Path',
expect.objectContaining({skipInitial: true}),
expect.anything(),
);
expect(list).toBeCalledTimes(0);
expect(logs).toBeCalledTimes(0);
expect(once).toBeCalledTimes(0);
});

test('program passes skip-initial as false to watch function when not provided', async () => {
const program = new CdkWatchCommand('1');
await program.parseAsync(buildArgv('watch My/Path'));
expect(watch).toBeCalledWith(
'My/Path',
expect.objectContaining({skipInitial: false}),
expect.anything(),
);
expect(list).toBeCalledTimes(0);
expect(logs).toBeCalledTimes(0);
expect(once).toBeCalledTimes(0);
Expand Down Expand Up @@ -64,7 +98,7 @@ describe('CLI commands', () => {
await program.parseAsync(buildArgv(`watch My/Path ${flag}`));
expect(watch).toBeCalledWith(
'My/Path',
{logs: expected},
expect.objectContaining({logs: expected}),
expect.anything(),
);
},
Expand Down
5 changes: 5 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ class CdkWatchCommand extends Command {
'-a, --app <app>',
'pass the --app option to the underlying synth command',
);
const skipInitialOption = new Option(
'--skip-initial',
'prevent cdk from uploading the function code until a file has changed',
).default(false);

this.command('watch', {isDefault: true})
.arguments('<pathGlob>')
Expand All @@ -44,6 +48,7 @@ class CdkWatchCommand extends Command {
$ cdkw "**" --profile=planes --no-logs\n`,
)
.addOption(cdkContextOption)
.addOption(skipInitialOption)
.addOption(profileOption)
.addOption(cdkAppOption)
.addOption(logsOption)
Expand Down
69 changes: 45 additions & 24 deletions src/commands/watch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as path from 'path';
import * as esbuild from 'esbuild';
import chalk from 'chalk';
import {copyCdkAssetToWatchOutdir} from '../lib/copyCdkAssetToWatchOutdir';
import {filterManifestByPath} from '../lib/filterManifestByPath';
import {initAwsSdk} from '../lib/initAwsSdk';
Expand All @@ -18,6 +19,7 @@ export const watch = async (
app: string;
profile: string;
logs: boolean;
skipInitial: boolean;
forceCloudwatch?: boolean;
},
): Promise<void> => {
Expand Down Expand Up @@ -48,13 +50,53 @@ export const watch = async (
}
return Promise.all(
lambdaDetails.map(
async ({functionName, lambdaCdkPath, lambdaManifest}) => {
async ({functionName, lambdaCdkPath, layers, lambdaManifest}) => {
if (
lambdaManifest.nodeModulesLayerVersion &&
!layers.includes(lambdaManifest.nodeModulesLayerVersion)
) {
// eslint-disable-next-line no-console
console.warn(
chalk.yellow(
'[Warning]: Function modules layer is out of sync with published layer version, this can lead to runtime errors. To fix, do a full `cdk deploy`.',
),
);
}

const logger = createCLILoggerForLambda(
lambdaCdkPath,
lambdaDetails.length > 1,
);
const watchOutdir = copyCdkAssetToWatchOutdir(lambdaManifest);

const updateFunction = () => {
const uploadingProgressText = 'uploading function code';
twisters.put(`${lambdaCdkPath}:uploading`, {
meta: {prefix: logger.prefix},
text: uploadingProgressText,
});

return updateLambdaFunctionCode(watchOutdir, functionName)
.then(() => {
twisters.put(`${lambdaCdkPath}:uploading`, {
meta: {prefix: logger.prefix},
text: uploadingProgressText,
active: false,
});
})
.catch((e) => {
twisters.put(`${lambdaCdkPath}:uploading`, {
text: uploadingProgressText,
meta: {error: e},
active: false,
});
});
};

if (!options.skipInitial) {
await updateFunction();
}

logger.log('waiting for changes');
esbuild
.build({
Expand All @@ -72,30 +114,9 @@ export const watch = async (
logger.error(
`failed to rebuild lambda function code ${error.toString()}`,
);
return;
} else {
updateFunction();
}

const uploadingProgressText = 'uploading function code';
twisters.put(`${lambdaCdkPath}:uploading`, {
meta: {prefix: logger.prefix},
text: uploadingProgressText,
});

updateLambdaFunctionCode(watchOutdir, functionName)
.then(() => {
twisters.put(`${lambdaCdkPath}:uploading`, {
meta: {prefix: logger.prefix},
text: uploadingProgressText,
active: false,
});
})
.catch((e) => {
twisters.put(`${lambdaCdkPath}:uploading`, {
text: uploadingProgressText,
meta: {error: e},
active: false,
});
});
},
},
})
Expand Down
35 changes: 25 additions & 10 deletions src/constructs/NodeModulesLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import {Construct, RemovalPolicy} from '@aws-cdk/core';
import execa from 'execa';
import * as fs from 'fs-extra';
import * as path from 'path';
import objectHash from 'object-hash';
import {CDK_WATCH_OUTDIR} from '../consts';

interface NodeModulesLayerProps {
depsLockFilePath?: string;
pkgPath: string;
nodeModules: string[];
skip?: boolean;
}

enum Installer {
Expand Down Expand Up @@ -49,13 +51,17 @@ const getDepsLock = (propsDepsLockFilePath?: string) => {
};

export class NodeModulesLayer extends LayerVersion {
public readonly layerVersion: string;

constructor(scope: Construct, id: string, props: NodeModulesLayerProps) {
const depsLockFilePath = getDepsLock(props.depsLockFilePath);

const {pkgPath} = props;

// Determine dependencies versions, lock file and installer
const dependencies = extractDependencies(pkgPath, props.nodeModules);
const dependenciesPackageJson = {
dependencies: extractDependencies(pkgPath, props.nodeModules),
};
let installer = Installer.NPM;
let lockFile = LockFile.NPM;
if (depsLockFilePath.endsWith(LockFile.YARN)) {
Expand All @@ -74,21 +80,30 @@ export class NodeModulesLayer extends LayerVersion {

fs.ensureDirSync(outputDir);
fs.copyFileSync(depsLockFilePath, path.join(outputDir, lockFile));
fs.writeJsonSync(path.join(outputDir, 'package.json'), {dependencies});
fs.writeJsonSync(
path.join(outputDir, 'package.json'),
dependenciesPackageJson,
);
const layerVersion = objectHash(dependenciesPackageJson);

// eslint-disable-next-line no-console
console.log('Installing node_modules in layer');
execa.sync(installer, ['install'], {
cwd: outputDir,
stderr: 'inherit',
stdout: 'ignore',
stdin: 'ignore',
});
if (!props.skip) {
// eslint-disable-next-line no-console
console.log('Installing node_modules in layer');
execa.sync(installer, ['install'], {
cwd: outputDir,
stderr: 'inherit',
stdout: 'ignore',
stdin: 'ignore',
});
}

super(scope, id, {
removalPolicy: RemovalPolicy.DESTROY,
description: 'NodeJS Modules Packaged into a Layer by cdk-watch',
code: Code.fromAsset(layerBase),
layerVersionName: layerVersion,
});

this.layerVersion = layerVersion;
}
}
20 changes: 12 additions & 8 deletions src/constructs/WatchableNodejsFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ class WatchableNodejsFunction extends NodejsFunction {

public readonly local?: cdk.ILocalBundling;

private readonly nodeModulesLayerVersion: string | undefined;

constructor(
scope: cdk.Construct,
id: string,
Expand Down Expand Up @@ -127,14 +129,15 @@ class WatchableNodejsFunction extends NodejsFunction {
});
const shouldSkipInstall =
scope.node.tryGetContext(CDK_WATCH_CONTEXT_NODE_MODULES_DISABLED) === '1';
if (moduleNames && !shouldSkipInstall) {
this.addLayers(
new NodeModulesLayer(this, 'NodeModulesLayer', {
nodeModules: moduleNames,
pkgPath,
depsLockFilePath: props.depsLockFilePath,
}),
);
if (moduleNames) {
const nodeModulesLayer = new NodeModulesLayer(this, 'NodeModulesLayer', {
nodeModules: moduleNames,
pkgPath,
depsLockFilePath: props.depsLockFilePath,
skip: shouldSkipInstall,
});
this.addLayers(nodeModulesLayer);
this.nodeModulesLayerVersion = nodeModulesLayer.layerVersion;
}

const {entry} = props;
Expand Down Expand Up @@ -247,6 +250,7 @@ class WatchableNodejsFunction extends NodejsFunction {
: {};
cdkWatchManifest.lambdas[this.node.path] = {
assetPath,
nodeModulesLayerVersion: this.nodeModulesLayerVersion,
realTimeLogsStackLogicalId: this.cdkWatchLogsApi
? this.stack.getLogicalId(
this.cdkWatchLogsApi.nestedStackResource as CfnElement,
Expand Down
Loading

0 comments on commit c3cc401

Please sign in to comment.