Skip to content

Commit fdabe95

Browse files
authored
feat(applets): integrate into toolkit (#1039)
Integrate the Applet runner into the toolkit. When the --app argument points to a .yml or .yaml file, the target will be assumed to be a JavaScript applet, and run through the applet interpreter. Enhancements to JS applet interpreter: - Allow using non-Stack constructs. - Multiple Stacks in one applet file by supplying an array. - Allow referencing packages directly from NPM. Non-Stack constructs can now be used as applets. If a non-Stack applet is selected, a Stack will be constructed around it. This makes it possible to reuse Applet constructs in regular CDK applications (if they meet the requirements). BREAKING CHANGE: The applet schema has changed to allow Multiple applets can be define in one file by structuring the files like this: BREAKING CHANGE: The applet schema has changed to allow definition of multiple applets in the same file. The schema now looks like this: applets: MyApplet: type: ./my-applet-file properties: property1: value ... By starting an applet specifier with npm://, applet modules can directly be referenced in NPM. You can include a version specifier (@1.2.3) to reference specific versions. Fixes #849, #342, #291.
1 parent ceeaf6e commit fdabe95

28 files changed

+6202
-455
lines changed

docs/src/applets.rst

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,61 +16,71 @@ Applets
1616

1717
.. note:: Currently the |cdk| only supports applets published as JavaScript modules.
1818

19-
Applets are files in the YAML or JSON format that have the following root attribute,
20-
where MODULE can represent
21-
a local file, such as :code:`./my-module`,
22-
a local dependency, such as :code:`my-dependency`,
23-
or a global module, such as :code:`@aws-cdk/aws-s3`
24-
and CLASS is the name of a class exported by the module.
19+
Applets are files in the YAML format that instantiate constructs directly,
20+
without writing any code. The structure of an applet file looks like this:
2521

2622
.. code:: js
2723
28-
applet: MODULE[:CLASS]
24+
applets:
25+
Applet1:
26+
type: MODULE[:CLASS]
27+
properties:
28+
property1: value1
29+
property2: value2
30+
...
31+
Applet2:
32+
type: MODULE[:CLASS]
33+
properties:
34+
...
2935
30-
If CLASS is not specified, :code:`Applet` is used as the default class name.
31-
Therefore, you need only refer to |cdk| construct libraries that export
32-
an :code:`Applet` class by their library name.
36+
Every applet will be synthesized to its own stack, named after the key used
37+
in the applet definition.
3338

34-
The rest of the YAML file is applet-dependent.
35-
The object is passed as :code:`props` when the applet object is instantiated
36-
and added to an |cdk| app created by **cdk-applet-js**.
39+
Specifying the applet to load
40+
=============================
3741

38-
Use **cdk-applet-js** *applet* to run the applet, create an |cdk| app,
39-
and use that with the |cdk| tools, as shown in the following example.
42+
An applet ``type`` specification looks like this:
4043

41-
.. code-block:: sh
44+
.. code:: js
4245
43-
cdk --app "cdk-applet-js ./my-applet.yaml" synth
46+
applet: MODULE[:CLASS]
4447
45-
To make the applet file executable and use the host as a shebang
46-
on Unix-based systems, such as Linux, MacOS, or Windows Bash shell,
47-
create a script similar to the following.
48+
**MODULE** can be used to indicate:
4849

49-
.. code-block:: sh
50+
* A local file, such as ``./my-module`` (expects ``my-module.js`` in the same
51+
directory).
52+
* A local module such as ``my-dependency`` (expects an NPM package at
53+
``node_modules/my-dependency``).
54+
* A global module, such as ``@aws-cdk/aws-s3`` (expects the package to have been
55+
globally installed using NPM).
56+
* An NPM package, such as ``npm://some-package@1.2.3`` (the version specifier
57+
may be omitted to refer to the latest version of the package).
58+
59+
**CLASS** should reference the name of a class exported by the indicated module.
60+
If the class name is omitted, ``Applet`` is used as the default class name.
61+
62+
Properties
63+
==========
5064

51-
#!/usr/bin/env cdk-applet-js
65+
Pass properties to the applet by specifying them in the ``properties`` object.
66+
The properties will be passed to the instantiation of the class in the ``type``
67+
parameter.
5268

53-
applet: aws-cdk-codebuild
54-
source: arn:aws:codecommit:::my-repository
55-
image: node:8.9.4
56-
compute: large
57-
build:
58-
- npm install --unsafe-perm
59-
- npm test
60-
- npm pack --unsafe-perm
69+
Running
70+
=======
6171

62-
To execute the applet and synthesize an |CFN| template,
63-
use the following command.
72+
To run an applet, pass its YAML file directly as the ``--app`` argument to a
73+
``cdk`` invocation:
6474

6575
.. code-block:: sh
6676
67-
cdk synth --app "./build.yaml"
77+
cdk --app ./my-applet.yaml deploy
6878
69-
To avoid needing **--app** for every invocation,
70-
add the following entry to *cdk.json*.
79+
To avoid needing to specify ``--app`` for every invocation, make a ``cdk.json``
80+
file and add in the application in the config as usual:
7181

7282
.. code-block:: json
7383
7484
{
75-
"app": "./build.yaml"
85+
"app": "./my-applet.yaml"
7686
}

packages/@aws-cdk/applet-js/bin/cdk-applet-js.ts

Lines changed: 63 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
import 'source-map-support/register';
33

44
import cdk = require('@aws-cdk/cdk');
5+
import child_process = require('child_process');
56
import fs = require('fs-extra');
7+
import os = require('os');
68
import path = require('path');
9+
import { isStackConstructor, parseApplet } from '../lib/applet-helpers';
710

811
// tslint:disable-next-line:no-var-requires
912
const YAML = require('js-yaml');
@@ -19,65 +22,84 @@ async function main() {
1922

2023
const appletFile = process.argv[2];
2124
if (!appletFile) {
22-
throw new Error(`Usage: ${progname}| <applet.yaml>`);
25+
throw new Error(`Usage: ${progname} <applet.yaml>`);
2326
}
2427

25-
// read applet properties from the provided file
26-
const props = YAML.safeLoad(await fs.readFile(appletFile, { encoding: 'utf-8' }));
28+
// read applet(s) properties from the provided file
29+
const fileContents = YAML.safeLoad(await fs.readFile(appletFile, { encoding: 'utf-8' }));
30+
if (typeof fileContents !== 'object') {
31+
throw new Error(`${appletFile}: should contain a YAML object`);
32+
}
33+
const appletMap = fileContents.applets;
34+
if (!appletMap) {
35+
throw new Error(`${appletFile}: must have an 'applets' key`);
36+
}
37+
38+
const searchDir = path.dirname(appletFile);
39+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdkapplet'));
40+
try {
41+
const app = new cdk.App();
42+
43+
for (const [name, definition] of Object.entries(appletMap)) {
44+
await constructStack(app, searchDir, tempDir, name, definition);
45+
}
46+
app.run();
47+
} finally {
48+
await fs.remove(tempDir);
49+
}
50+
}
2751

52+
/**
53+
* Construct a stack from the given props
54+
* @param props Const
55+
*/
56+
async function constructStack(app: cdk.App, searchDir: string, tempDir: string, stackName: string, spec: any) {
2857
// the 'applet' attribute tells us how to load the applet. in the javascript case
2958
// it will be in the format <module>:<class> where <module> is technically passed to "require"
3059
// and <class> is expected to be exported from the module.
31-
const applet: string = props.applet;
32-
if (!applet) {
33-
throw new Error('Applet file missing "applet" attribute');
60+
const appletSpec: string | undefined = spec.type;
61+
if (!appletSpec) {
62+
throw new Error(`Applet ${stackName} missing "type" attribute`);
3463
}
3564

36-
const { moduleName, className } = parseApplet(applet);
37-
38-
// remove the 'applet' attribute as we pass it along to the applet class.
39-
delete props.applet;
65+
const applet = parseApplet(appletSpec);
66+
67+
const props = spec.properties || {};
68+
69+
if (applet.npmPackage) {
70+
// tslint:disable-next-line:no-console
71+
console.error(`Installing NPM package ${applet.npmPackage}`);
72+
// Magic marker to download this package directly off of NPM
73+
// We're going to run NPM as a shell (since programmatic usage is not stable
74+
// by their own admission) and we're installing into a temporary directory.
75+
// (Installing into a permanent directory is useless since NPM doesn't do
76+
// any real caching anyway).
77+
child_process.execFileSync('npm', ['install', '--prefix', tempDir, '--global', applet.npmPackage], {
78+
stdio: 'inherit'
79+
});
80+
searchDir = path.join(tempDir, 'lib');
81+
}
4082

4183
// we need to resolve the module name relatively to where the applet file is
4284
// and not relative to this module or cwd.
43-
const resolve = require.resolve as any; // escapse type-checking since { paths } is not defined
44-
const modulePath = resolve(moduleName, { paths: [ path.dirname(appletFile) ] });
85+
const modulePath = require.resolve(applet.moduleName, { paths: [ searchDir ] });
4586

4687
// load the module
4788
const pkg = require(modulePath);
4889

4990
// find the applet class within the package
5091
// tslint:disable-next-line:variable-name
51-
const AppletStack = pkg[className];
52-
if (!AppletStack) {
53-
throw new Error(`Cannot find applet class "${className}" in module "${moduleName}"`);
54-
}
55-
56-
// create the CDK app
57-
const app = new cdk.App();
58-
59-
const constructName = props.name || className;
60-
61-
// add the applet stack into the app.
62-
new AppletStack(app, constructName, props);
63-
64-
// transfer control to the app
65-
app.run();
66-
}
67-
68-
function parseApplet(applet: string) {
69-
const components = applet.split(':');
70-
// tslint:disable-next-line:prefer-const
71-
let [ moduleName, className ] = components;
72-
73-
if (components.length > 2 || !moduleName) {
74-
throw new Error(`"applet" value is "${applet}" but it must be in the form "<js-module>[:<applet-class>]".
75-
If <applet-class> is not specified, "Applet" is the default`);
92+
const appletConstructor = pkg[applet.className];
93+
if (!appletConstructor) {
94+
throw new Error(`Cannot find applet class "${applet.className}" in module "${applet.moduleName}"`);
7695
}
7796

78-
if (!className) {
79-
className = 'Applet';
97+
if (isStackConstructor(appletConstructor)) {
98+
// add the applet stack into the app.
99+
new appletConstructor(app, stackName, props);
100+
} else {
101+
// Make a stack THEN add it in
102+
const stack = new cdk.Stack(app, stackName, props);
103+
new appletConstructor(stack, 'Default', props);
80104
}
81-
82-
return { moduleName, className };
83105
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Determine whether this constructorFunction is going to create an object that inherits from Stack
3+
*
4+
* We do structural typing.
5+
*/
6+
export function isStackConstructor(constructorFn: any) {
7+
// Test for a public method that Stack has
8+
return constructorFn.prototype.findResource !== undefined;
9+
}
10+
11+
/**
12+
* Extract module name from a NPM package specification
13+
*/
14+
export function extractModuleName(packageSpec: string) {
15+
const m = /^((?:@[a-zA-Z-]+\/)?[a-zA-Z-]+)/i.exec(packageSpec);
16+
if (!m) { throw new Error(`Could not find package name in ${packageSpec}`); }
17+
return m[1];
18+
}
19+
20+
export function parseApplet(applet: string): AppletSpec {
21+
const m = /^(npm:\/\/)?([a-z0-9_@./-]+)(:[a-z_0-9]+)?$/i.exec(applet);
22+
if (!m) {
23+
throw new Error(`"applet" value is "${applet}" but it must be in the form "[npm://]<js-module>[:<applet-class>]".
24+
If <applet-class> is not specified, "Applet" is the default`);
25+
}
26+
27+
if (m[1] === 'npm://') {
28+
return {
29+
npmPackage: m[2],
30+
moduleName: extractModuleName(m[2]),
31+
className: className(m[3]),
32+
};
33+
} else {
34+
return {
35+
moduleName: m[2],
36+
className: className(m[3]),
37+
};
38+
}
39+
40+
function className(s: string | undefined) {
41+
if (s) {
42+
return s.substr(1);
43+
}
44+
return 'Applet';
45+
}
46+
}
47+
48+
export interface AppletSpec {
49+
npmPackage?: string;
50+
moduleName: string;
51+
className: string;
52+
}

0 commit comments

Comments
 (0)