sam-goodwin / punchcard

Type-safe AWS infrastructure.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Eliminate the export default app convention

sam-goodwin opened this issue · comments

commented

The convention to export default app is flakey.

When trying to add snapshot tests, the process.mainModule!.filename call breaks - it is too dependent on how the node process is executed.

export function code(scope: cdk.IConstruct): lambda.Code {
if (isRuntime()) {
class MockCode extends lambda.Code {
public readonly isInline: boolean = false;
public _toJSON(_resource?: cdk.CfnResource): lambda.CfnFunction.CodeProperty {
return {};
}
}
return new MockCode();
}
const app = findApp(scope);
if ((app as any)[codeSymbol] === undefined) {
const index = process.mainModule!.filename;
// TODO: probably better to stash things in the CWD instead of next to the app
const dist = path.resolve(path.dirname(index), '.punchcard');
const name = path.basename(index, '.js');
const codePath = path.join(dist, name);
// HACK: this block is effectively erased at runtime:
// 1) it is guarded by an environment variable expected to only be set at runtime
// 2) webpack removes calls to itself, i.e. require('webpack')
if (!fs.existsSync(dist)) {
fs.mkdirSync(dist);
}
if (!fs.existsSync(codePath)) {
fs.mkdirSync(codePath);
}
const webpack = require('webpack');
const compiler = webpack({
mode: scope.node.tryGetContext(WEBPACK_MODE) || 'production',
entry: index,
target: 'node',
output: {
path: codePath,
filename: 'app.js',
libraryTarget: 'umd',
},
externals: ['aws-sdk', 'webpack'],
plugins: [new webpack.IgnorePlugin({
resourceRegExp: /^webpack$/ // don't generate imports for webpack
})]
});
compiler.run((err: Error) => {
if (err) {
console.log(err);
}
});
fs.writeFileSync(path.join(codePath, 'index.js'), `
const app = require('./app').default;
if (app === undefined) {
throw new Error('app is null, are you exporting your cdk.App as the default in your main module (i.e. index.js)?');
}
app.run = (() => {
// no-op when at runtime
console.log('cdk.App.run: no-op');
return null;
});
var handler;
exports.handler = async (event, context) => {
if (!handler) {
const runPath = process.env['${RUNTIME_ENV}'];
const target = findChild(app, runPath);
if (target[Symbol.for('${ENTRYPOINT_SYMBOL_NAME}')] === true) {
handler = await target.boot();
} else {
throw new Error(\`path '\${runPath}' did not point to an Entrypoint\`);
}
}
return await handler(event, context);
};
function findChild(scope, path) {
const elements = path.split('/');
for (const e of elements) {
scope = scope.node.tryFindChild(e);
if (!scope) {
break;
}
}
if (!scope) {
throw new Error(\`no child found with path: \${path}\`);
}
return scope;
}
`);
(app as any)[codeSymbol] = lambda.Code.asset(codePath);
}
return (app as any)[codeSymbol] as lambda.Code;
}

A punchcard app should ideally be no different than a CDK app.

Is there anything we can do in the CDK to aid?

commented

I don't think it can help eliminate the export default app, but it could certainly help identify the entry-point JS file. E.g. cdk deploy -a ./index.js <- I need to find the path to this file. Is there an environment or context value I can use to discover this? Or, is there at least a way for me to determine if the app was run by the CDK CLI? This would at least allow punchcard to work in unit test environments.

The only other solution I can think of is something like Pulumi's closure serializer. I think it works from within the node environment by access the V8 compiler context. Although I need to dive deeper.

commented

Hmm, I'm being dumb. If we just assume that if process.mainModule.filename is null, then it wasn't run by the CDK CLI. This heuristic would at least unblock unit testing.

commented

I think the best approach is to do closure serialization. It cleans up the API and better isolates runtime from infrastructure code. This should be a requirement of 1.0.