├── .gitignore
├── README.md
├── dist
├── bin
│ └── command.js
└── lib
│ ├── build
│ ├── build.js
│ └── buildController.js
│ ├── deploy
│ ├── deploy.js
│ └── deployController.js
│ ├── serve
│ ├── serve.js
│ ├── serveController.js
│ └── serveController.test.js
│ └── types.js
├── package-lock.json
├── package.json
├── src
├── bin
│ └── command.ts
└── lib
│ ├── build
│ ├── build.ts
│ └── buildController.ts
│ ├── deploy
│ ├── deploy.ts
│ └── deployController.ts
│ ├── serve
│ ├── serve.ts
│ ├── serveController.test.ts
│ └── serveController.ts
│ └── types.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Optional REPL history
57 | .node_repl_history
58 |
59 | # Output of 'npm pack'
60 | *.tgz
61 |
62 | # Yarn Integrity file
63 | .yarn-integrity
64 |
65 | # dotenv environment variables file
66 | .env
67 | .env.test
68 |
69 | # parcel-bundler cache (https://parceljs.org/)
70 | .cache
71 |
72 | # next.js build output
73 | .next
74 |
75 | # nuxt.js build output
76 | .nuxt
77 |
78 | # vuepress build output
79 | .vuepress/dist
80 |
81 | # Serverless directories
82 | .serverless/
83 |
84 | # FuseBox cache
85 | .fusebox/
86 |
87 | # DynamoDB Local files
88 | .dynamodb/
89 |
90 | # VS Code
91 | .vscode/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Airfn
2 |
3 | Airfn is a CLI tool that enables users to easily and quickly serve and deploy their AWS lambda functions to AWS with their existing development environment, increasing developer productivity by abstracting configuration process
4 |
5 | ## How it works
6 |
7 | Users (developers) are able to use our CLI to serve functions locally, build functions to
8 | optimize speed and performance, and deploy those functions as serverless Lambdas by
9 | creating an account on the Airfn Web App and doing the following:
10 |
11 | * User installs our Node.js CLI globally by running `npm install -g airfn` in terminal.
12 |
13 | * User initializes a configuration file by entering `air init` in terminal
14 | within project directory.
15 |
16 | * User serves functions locally as Lambdas by entering `air serve`.
17 |
18 | > CLI spins up an Express server and serves the user’s functions, using the
19 | names of the functions as the names of the API endpoints. User can now
20 | locally test her Lambdas by sending requests to the endpoints.
21 |
22 | * User builds functions for deployment by entering `air build`.
23 |
24 | > CLI processes user's functions to transpile functions source code and any
25 | imported Node modules to her chosen Node.js version via Babel.
26 |
27 | * User deploys functions to AWS by entering `air deploy`.
28 |
29 | > CLI gets function source code of user's functions that will be used to deploy as Lambdas to AWS and return endpoints.
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/dist/bin/command.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | "use strict";
3 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
4 | return new (P || (P = Promise))(function (resolve, reject) {
5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
8 | step((generator = generator.apply(thisArg, _arguments || [])).next());
9 | });
10 | };
11 | var __importDefault = (this && this.__importDefault) || function (mod) {
12 | return (mod && mod.__esModule) ? mod : { "default": mod };
13 | };
14 | Object.defineProperty(exports, "__esModule", { value: true });
15 | const os_1 = __importDefault(require("os"));
16 | const fs_1 = __importDefault(require("fs"));
17 | const path_1 = __importDefault(require("path"));
18 | const commander_1 = __importDefault(require("commander"));
19 | const inquirer_1 = __importDefault(require("inquirer"));
20 | const ora_1 = __importDefault(require("ora"));
21 | const chalk_1 = __importDefault(require("chalk"));
22 | const axios_1 = __importDefault(require("axios"));
23 | const serve_1 = __importDefault(require("../lib/serve/serve"));
24 | const build_1 = require("../lib/build/build");
25 | const deploy_1 = __importDefault(require("../lib/deploy/deploy"));
26 | // TODO allow custom configuration of API Gateway subdomain
27 | const ROOT_CONFIG_FILENAME = 'config.json';
28 | const ROOT_CONFIG_DIRNAME = '.airfn';
29 | const BASE_API_GATEWAY_ENDPOINT = 'lambda9.cloud';
30 | const AUTH_ENDPOINT = 'https://test.lambda9.cloud/cli/cliauth';
31 | const SPINNER_TIMEOUT = 1000;
32 | const JSONpackage = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', '..', 'package.json')));
33 | commander_1.default.version(JSONpackage.version);
34 | commander_1.default
35 | .command('init')
36 | .description('Initialize configuration for serving, building, and deploying lambda functions')
37 | .action(() => __awaiter(this, void 0, void 0, function* () {
38 | const airfnConfig = {};
39 | const cwdName = path_1.default.parse(process.cwd()).name;
40 | console.log(`\n👤 Please login with your username and password\nYou can sign up for an account at https://airfn.io/signup\n`);
41 | // TODO: Implement actual auth
42 | yield inquirer_1.default
43 | .prompt([
44 | {
45 | name: 'username',
46 | message: 'Username:',
47 | },
48 | ])
49 | .then((answers) => __awaiter(this, void 0, void 0, function* () {
50 | const username = answers.username;
51 | airfnConfig.user = answers.username;
52 | yield inquirer_1.default
53 | .prompt([
54 | {
55 | name: 'password',
56 | type: 'password',
57 | message: 'Password:',
58 | },
59 | ])
60 | .then((answers) => __awaiter(this, void 0, void 0, function* () {
61 | const password = answers.password;
62 | const credentials = {
63 | username,
64 | password
65 | };
66 | yield axios_1.default.post(AUTH_ENDPOINT, credentials).then((response) => {
67 | const homedir = os_1.default.homedir();
68 | const rootConfigDir = path_1.default.join(homedir, ROOT_CONFIG_DIRNAME);
69 | const rootConfigPath = path_1.default.join(rootConfigDir, ROOT_CONFIG_FILENAME);
70 | const rootConfig = {
71 | clientId: response.data
72 | };
73 | if (!fs_1.default.existsSync(rootConfigDir)) {
74 | fs_1.default.mkdir(rootConfigDir, (err) => {
75 | if (err)
76 | console.log(`😓 Failed to build config: ${err}`);
77 | });
78 | }
79 | fs_1.default.writeFile(rootConfigPath, JSON.stringify(rootConfig), err => {
80 | if (err)
81 | console.log(`😓 Failed to build config: ${err}`);
82 | });
83 | }).catch((err) => {
84 | console.log(`❌ Wrong username/password combination.\n Retry by running 'air init' again`);
85 | process.exit();
86 | });
87 | }));
88 | }));
89 | yield inquirer_1.default
90 | .prompt([
91 | {
92 | name: 'project',
93 | message: 'Enter project name for your lambda functions:',
94 | default: cwdName,
95 | },
96 | ])
97 | .then((answers) => {
98 | airfnConfig.project = answers.project;
99 | });
100 | yield inquirer_1.default
101 | .prompt([
102 | {
103 | name: 'functionsSrc',
104 | message: 'In which directory are your lambda functions?',
105 | default: 'src/functions',
106 | },
107 | ])
108 | .then((answers) => __awaiter(this, void 0, void 0, function* () {
109 | const functionsSrc = answers.functionsSrc;
110 | airfnConfig.functionsSrc = functionsSrc;
111 | if (!fs_1.default.existsSync(answers.functionsSrc)) {
112 | yield inquirer_1.default
113 | .prompt([
114 | {
115 | type: 'confirm',
116 | name: 'createSrcDir',
117 | message: `There's no directory at ${answers.functionsSrc}. Would you like to create one now?`,
118 | },
119 | ])
120 | .then((answers) => {
121 | if (answers.createSrcDir === true && functionsSrc) {
122 | fs_1.default.mkdirSync(path_1.default.join(process.cwd(), functionsSrc), {
123 | recursive: true,
124 | });
125 | }
126 | });
127 | }
128 | }));
129 | yield inquirer_1.default
130 | .prompt([
131 | {
132 | name: 'functionsOutput',
133 | message: 'In which directory would you like your built lambda functions? (a root level directory is recommended)',
134 | default: '/functions',
135 | },
136 | ])
137 | .then((answers) => {
138 | airfnConfig.functionsOutput = answers.functionsOutput;
139 | });
140 | yield inquirer_1.default
141 | .prompt([
142 | {
143 | type: 'list',
144 | name: 'nodeRuntime',
145 | message: 'Which NodeJS runtime will your lambda functions use?',
146 | choices: ['10.15', '8.10'],
147 | },
148 | ])
149 | .then((answers) => {
150 | airfnConfig.nodeRuntime = answers.nodeRuntime;
151 | });
152 | yield inquirer_1.default
153 | .prompt([
154 | {
155 | name: 'functionsOutput',
156 | message: 'On which local port do you want to serve your lambda functions?',
157 | default: '9000',
158 | },
159 | ])
160 | .then((answers) => {
161 | airfnConfig.port = Number(answers.functionsOutput);
162 | });
163 | fs_1.default.writeFile('airfn.json', JSON.stringify(airfnConfig), err => {
164 | if (err)
165 | console.log(`😓 Failed to build config: ${err}`);
166 | console.log('\n💾 Your Airfn config has been saved!');
167 | });
168 | }));
169 | commander_1.default
170 | .command('serve')
171 | .description('Serve and watch functions')
172 | .action(() => {
173 | getUserAccessKey();
174 | const airfnConfig = getUserLambdaConfig();
175 | const spinner = ora_1.default('☁️ Airfn: Serving functions...').start();
176 | setTimeout(() => {
177 | const useStatic = Boolean(commander_1.default.static);
178 | let server;
179 | const startServer = () => {
180 | server = serve_1.default(airfnConfig.functionsOutput, airfnConfig.port || 9000, useStatic, Number(commander_1.default.timeout) || 10);
181 | };
182 | if (useStatic) {
183 | startServer();
184 | return;
185 | }
186 | const { config: userWebpackConfig, babelrc: useBabelrc = true } = commander_1.default;
187 | build_1.watch(airfnConfig.functionsSrc, airfnConfig.functionsOutput, airfnConfig.nodeRuntime, { userWebpackConfig, useBabelrc }, (err, stats) => {
188 | if (err) {
189 | console.error(err);
190 | return;
191 | }
192 | console.log(chalk_1.default.hex('#24c4f4')(stats.toString()));
193 | spinner.stop();
194 | if (!server) {
195 | startServer();
196 | console.log('\n✅ Done serving!');
197 | }
198 | else {
199 | console.log('\n🔨 Done rebuilding!');
200 | }
201 | stats.compilation.chunks.forEach((chunk) => {
202 | server.clearCache(chunk.name || chunk.id().toString());
203 | });
204 | });
205 | }, SPINNER_TIMEOUT);
206 | });
207 | commander_1.default
208 | .command('build')
209 | .description('Build functions')
210 | .action(() => {
211 | getUserAccessKey();
212 | const spinner = ora_1.default('☁️ Airfn: Building functions...').start();
213 | setTimeout(() => {
214 | const airfnConfig = getUserLambdaConfig();
215 | spinner.color = 'green';
216 | const { config: userWebpackConfig, babelrc: useBabelrc = true } = commander_1.default;
217 | build_1.run(airfnConfig.functionsSrc, airfnConfig.functionsOutput, airfnConfig.nodeRuntime, {
218 | userWebpackConfig,
219 | useBabelrc,
220 | })
221 | .then((stats) => {
222 | console.log(chalk_1.default.hex('#f496f4')(stats.toString()));
223 | spinner.stop();
224 | console.log('\n✅ Done building!');
225 | })
226 | .catch((err) => {
227 | console.error(err);
228 | process.exit(1);
229 | });
230 | }, SPINNER_TIMEOUT);
231 | });
232 | commander_1.default
233 | .command('deploy')
234 | .description('Deploys functions to AWS')
235 | .action(() => {
236 | const accessKey = getUserAccessKey();
237 | const airfnConfig = getUserLambdaConfig();
238 | const spinner = ora_1.default('☁️ Airfn: Deploying functions...').start();
239 | setTimeout(() => {
240 | const { config: userWebpackConfig, babelrc: useBabelrc = true } = commander_1.default;
241 | // TODO: Handle already built functions
242 | build_1.run(airfnConfig.functionsSrc, airfnConfig.functionsOutput, airfnConfig.nodeRuntime, { userWebpackConfig, useBabelrc })
243 | .then((stats) => {
244 | console.log(chalk_1.default.hex('#f496f4')(stats.toString()));
245 | deploy_1.default(airfnConfig.user, accessKey, airfnConfig.project, airfnConfig.functionsSrc, airfnConfig.functionsOutput)
246 | .then((result) => {
247 | // TODO: Give lambda endpoints to user
248 | spinner.stop();
249 | console.log(`\n🚀 Successfully deployed! ${result.data}`);
250 | console.log(`\n🔗 Lambda endpoints:`);
251 | result.endpoints.forEach((endpoint) => {
252 | console.log(`https://${airfnConfig.project}.${BASE_API_GATEWAY_ENDPOINT}/${endpoint}`);
253 | });
254 | })
255 | .catch((err) => {
256 | spinner.stop();
257 | console.log(`😓 Failed to deploy: ${err}`);
258 | });
259 | })
260 | .catch((err) => {
261 | console.error(err);
262 | process.exit(1);
263 | });
264 | }, SPINNER_TIMEOUT);
265 | });
266 | commander_1.default
267 | .command('logout')
268 | .description('Log out of Airfn CLI')
269 | .action(() => {
270 | const { configFound, configDir } = rootConfigExists();
271 | if (configFound) {
272 | try {
273 | removeDir(configDir);
274 | console.log('Logged out of Airfn CLI');
275 | process.exit(0);
276 | }
277 | catch (err) {
278 | console.error(`Failed to log out`);
279 | }
280 | }
281 | else {
282 | console.log(`Already logged out`);
283 | process.exit(1);
284 | }
285 | });
286 | commander_1.default.on('command:*', function () {
287 | console.error(`\n❌ "${commander_1.default.args.join(' ')}" command not found!`);
288 | process.exit(1);
289 | });
290 | commander_1.default.parse(process.argv);
291 | const NO_COMMAND_SPECIFIED = commander_1.default.args.length === 0;
292 | if (NO_COMMAND_SPECIFIED) {
293 | commander_1.default.help();
294 | }
295 | function getUserAccessKey() {
296 | const { configFound, configPath } = rootConfigExists();
297 | if (configFound) {
298 | try {
299 | const rootConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
300 | return rootConfig.clientId;
301 | }
302 | catch (err) {
303 | console.log(`❌ Error reading config`);
304 | }
305 | }
306 | else {
307 | console.log(`❗️ Please login first by running 'air init'`);
308 | process.exit(1);
309 | }
310 | }
311 | function rootConfigExists() {
312 | const homedir = os_1.default.homedir();
313 | const rootConfigDir = path_1.default.join(homedir, ROOT_CONFIG_DIRNAME);
314 | const rootConfigPath = path_1.default.join(rootConfigDir, ROOT_CONFIG_FILENAME);
315 | const configFound = fs_1.default.existsSync(rootConfigPath);
316 | const configProps = {
317 | configFound: configFound,
318 | configDir: rootConfigDir,
319 | configPath: rootConfigPath
320 | };
321 | return configProps;
322 | }
323 | function getUserLambdaConfig() {
324 | try {
325 | const config = JSON.parse(fs_1.default.readFileSync(path_1.default.join(process.cwd(), 'airfn.json'), 'utf-8'));
326 | return config;
327 | }
328 | catch (err) {
329 | console.log(`❌ No Airfn config found. Did you first run 'l9 init'?`);
330 | process.exit(1);
331 | }
332 | }
333 | function removeDir(dir) {
334 | const list = fs_1.default.readdirSync(dir);
335 | for (let i = 0; i < list.length; i++) {
336 | const filename = path_1.default.join(dir, list[i]);
337 | const stat = fs_1.default.statSync(filename);
338 | if (stat.isDirectory()) {
339 | removeDir(filename);
340 | }
341 | else {
342 | fs_1.default.unlinkSync(filename);
343 | }
344 | }
345 | fs_1.default.rmdirSync(dir);
346 | }
347 |
--------------------------------------------------------------------------------
/dist/lib/build/build.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | const path_1 = __importDefault(require("path"));
7 | const fs_1 = __importDefault(require("fs"));
8 | const webpack_1 = __importDefault(require("webpack"));
9 | const buildController_1 = __importDefault(require("./buildController"));
10 | const createWebpack = buildController_1.default(path_1.default, fs_1.default, webpack_1.default);
11 | function run(srcDir, outputDir, runtime, additionalConfig) {
12 | return new Promise((resolve, reject) => {
13 | webpack_1.default(createWebpack(srcDir, outputDir, runtime, additionalConfig), (err, stats) => {
14 | if (err) {
15 | return reject(err);
16 | }
17 | resolve(stats);
18 | });
19 | });
20 | }
21 | exports.run = run;
22 | function watch(srcDir, outputDir, runtime, additionalConfig, cb) {
23 | var compiler = webpack_1.default(createWebpack(srcDir, outputDir, runtime, additionalConfig));
24 | compiler.watch(createWebpack(srcDir, outputDir, runtime, additionalConfig), cb);
25 | }
26 | exports.watch = watch;
27 |
--------------------------------------------------------------------------------
/dist/lib/build/buildController.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | const testFilePattern = '\\.(test|spec)\\.?';
4 | exports.default = (path, fs, webpack) => (srcDir, outputDir, runtime, { userWebpackConfig, useBabelrc } = {}) => {
5 | const babelOpts = {
6 | cacheDirectory: true,
7 | presets: [
8 | [
9 | require.resolve('@babel/preset-env'),
10 | { targets: { node: getBabelTarget({}, runtime) } },
11 | ],
12 | ],
13 | plugins: [
14 | require.resolve('@babel/plugin-proposal-class-properties'),
15 | require.resolve('@babel/plugin-transform-object-assign'),
16 | require.resolve('@babel/plugin-proposal-object-rest-spread'),
17 | ],
18 | };
19 | const functionsDir = outputDir;
20 | const functionsPath = path.join(process.cwd(), functionsDir);
21 | const dirPath = path.join(process.cwd(), srcDir);
22 | const defineEnv = {};
23 | const nodeEnv = process.env.NODE_ENV || 'production';
24 | const webpackMode = ['production', 'development'].includes(nodeEnv)
25 | ? nodeEnv
26 | : 'none';
27 | const webpackConfig = {
28 | mode: webpackMode,
29 | resolve: {
30 | extensions: ['.wasm', '.mjs', '.js', '.json', '.ts'],
31 | mainFields: ['module', 'main'],
32 | },
33 | module: {
34 | rules: [
35 | {
36 | test: /\.(m?js|ts)?$/,
37 | exclude: new RegExp(`(node_modules|bower_components|${testFilePattern})`),
38 | use: {
39 | loader: require.resolve('babel-loader'),
40 | options: Object.assign({}, babelOpts, { babelrc: useBabelrc }),
41 | },
42 | },
43 | ],
44 | },
45 | context: dirPath,
46 | entry: {},
47 | target: 'node',
48 | plugins: [
49 | new webpack.IgnorePlugin(/vertx/),
50 | new webpack.DefinePlugin(defineEnv),
51 | ],
52 | output: {
53 | path: functionsPath,
54 | filename: '[name].js',
55 | libraryTarget: 'commonjs',
56 | },
57 | optimization: {
58 | nodeEnv,
59 | },
60 | bail: true,
61 | devtool: false,
62 | };
63 | fs.readdirSync(dirPath).forEach((file) => {
64 | if (file.match(/\.(m?js|ts)$/)) {
65 | var name = file.replace(/\.(m?js|ts)$/, '');
66 | if (!name.match(new RegExp(testFilePattern))) {
67 | webpackConfig.entry[name] = './' + file;
68 | }
69 | }
70 | });
71 | return webpackConfig;
72 | };
73 | function getBabelTarget(envConfig, runtime) {
74 | const key = 'AWS_LAMBDA_JS_RUNTIME';
75 | // If NodeJS runtime specified during l9 init
76 | if (runtime)
77 | return runtime;
78 | // Otherwise use user webpack settings if exists
79 | const runtimes = ['nodejs8.15.0', 'nodejs6.10.3'];
80 | const current = envConfig[key] || process.env[key] || 'nodejs8.15.0';
81 | const unknown = runtimes.indexOf(current) === -1;
82 | return unknown ? '8.15.0' : current.replace(/^nodejs/, '');
83 | }
84 |
--------------------------------------------------------------------------------
/dist/lib/deploy/deploy.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | const path_1 = require("path");
7 | const fs_1 = require("fs");
8 | const axios_1 = __importDefault(require("axios"));
9 | const js_yaml_1 = require("js-yaml");
10 | const deployController_1 = require("./deployController");
11 | const DEPLOY_ENDPOINT = 'http://api.lambda9.cloud/lambda/deploy';
12 | const S3_CREATE_LAMBDA_ENDPOINT = 'https://cli.lambda9.cloud/createbucket';
13 | const CREATE_DOMAIN_ENDPOINT = 'https://cli.lambda9.cloud/deploydomain';
14 | const SAVE_FUNCTIONS_ENDPOINT = 'https://cli.lambda9.cloud/savefunctions';
15 | const LOGS_SUBSCRIPTION_ENDPOINT = 'https://cli.lambda9.cloud/subscribelogs';
16 | const LOG_GROUP_PREFIX = '/aws/lambda/';
17 | const BASE_DOMAIN = 'lambda9.cloud';
18 | exports.default = (user, accessKey, project, functionsSrc, functionsOutput) => {
19 | return new Promise((resolve, reject) => {
20 | getFunctionsSourceCode();
21 | const deployArtifacts = deployController_1.createDeployArtifacts(functionsOutput, path_1.join, {
22 | readFileSync: fs_1.readFileSync,
23 | readdirSync: fs_1.readdirSync,
24 | }, js_yaml_1.safeDump);
25 | deployController_1.createUserS3Bucket(S3_CREATE_LAMBDA_ENDPOINT, user, axios_1.default.post)
26 | .then((response) => {
27 | const requestData = Object.assign({ user,
28 | project }, deployArtifacts);
29 | axios_1.default({
30 | method: 'post',
31 | url: DEPLOY_ENDPOINT,
32 | data: requestData,
33 | maxContentLength: Infinity,
34 | })
35 | .then((response) => {
36 | const lambdaData = {
37 | endpoints,
38 | data: response.data,
39 | };
40 | createDomain(project, project);
41 | saveFunctions(functionsSourceCode, project, accessKey);
42 | subscribeToLogs(logGroupPrefixes);
43 | return resolve(lambdaData);
44 | })
45 | .catch(err => {
46 | return reject(err);
47 | });
48 | })
49 | .catch((err) => {
50 | console.log('😓 Error making S3 buckets for lambda functions');
51 | });
52 | const logGroupPrefixes = createLogGroupPrefixes(deployArtifacts.funcArr, project);
53 | const functionsSourceCode = getFunctionsSourceCode();
54 | const endpoints = createEndpoints(deployArtifacts.funcArr);
55 | });
56 | function createDomain(subdomainPrefix, stackName) {
57 | const data = {
58 | domainName: `${subdomainPrefix}.${BASE_DOMAIN}`,
59 | stackName
60 | };
61 | axios_1.default({
62 | method: "post",
63 | url: CREATE_DOMAIN_ENDPOINT,
64 | data
65 | })
66 | .then((response) => {
67 | console.log(`\n${response.data}`);
68 | })
69 | .catch(err => {
70 | console.log('😓 Error creating lambda subdomain');
71 | });
72 | }
73 | function saveFunctions(functions, projectName, accessKey) {
74 | const data = {
75 | functions,
76 | projectName,
77 | accessKey
78 | };
79 | axios_1.default({
80 | method: "post",
81 | url: SAVE_FUNCTIONS_ENDPOINT,
82 | data,
83 | maxContentLength: Infinity
84 | })
85 | .then((response) => {
86 | console.log('Saved lambda functions');
87 | })
88 | .catch(err => {
89 | });
90 | }
91 | function createLogGroupPrefixes(functions, projectName) {
92 | return functions.map((funcObj) => {
93 | const funcName = path_1.parse(funcObj.funcName).name;
94 | return `${LOG_GROUP_PREFIX}${projectName}-${funcName}`;
95 | });
96 | }
97 | function subscribeToLogs(logGroupsPrefixes) {
98 | const data = {
99 | logGroupsPrefixes
100 | };
101 | axios_1.default({
102 | method: "post",
103 | url: LOGS_SUBSCRIPTION_ENDPOINT,
104 | data
105 | })
106 | .then((response) => {
107 | })
108 | .catch(err => {
109 | });
110 | }
111 | function createEndpoints(functions) {
112 | return functions.map((funcObj) => {
113 | return path_1.parse(funcObj.funcName).name.toLowerCase();
114 | });
115 | }
116 | function getFunctionsSourceCode() {
117 | const funcArr = [];
118 | fs_1.readdirSync(path_1.join(process.cwd(), String(functionsSrc))).forEach((file) => {
119 | const data = fs_1.readFileSync(path_1.join(process.cwd(), `${functionsSrc}/${file}`), 'utf8');
120 | const funcObj = {
121 | funcName: file,
122 | funcDef: data,
123 | };
124 | funcArr.push(funcObj);
125 | });
126 | return funcArr;
127 | }
128 | };
129 |
--------------------------------------------------------------------------------
/dist/lib/deploy/deployController.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | const YAML_CONFIG_TEMPLATE = {
4 | AWSTemplateFormatVersion: '2010-09-09',
5 | Transform: 'AWS::Serverless-2016-10-31',
6 | Description: 'Deployed with Airfn CLI.',
7 | Outputs: {
8 | ApiGatewayId: {
9 | Value: {
10 | Ref: "ServerlessRestApi"
11 | }
12 | }
13 | },
14 | Resources: {}
15 | };
16 | function createDeployArtifacts(functionsOutput, join, fs, safeDump) {
17 | const funcArr = [];
18 | const yamlConfig = YAML_CONFIG_TEMPLATE;
19 | fs.readdirSync(join(process.cwd(), functionsOutput)).forEach((file) => {
20 | createFunctionResource(file, yamlConfig);
21 | const data = fs.readFileSync(join(process.cwd(), `${functionsOutput}/${file}`), 'utf8');
22 | const funcObj = {
23 | funcName: file,
24 | funcDef: data,
25 | };
26 | funcArr.push(funcObj);
27 | });
28 | return {
29 | yaml: safeDump(yamlConfig, { noCompatMode: true, noRefs: true }),
30 | funcArr,
31 | };
32 | }
33 | exports.createDeployArtifacts = createDeployArtifacts;
34 | ;
35 | function createUserS3Bucket(endpoint, user, post) {
36 | const data = {
37 | user
38 | };
39 | return new Promise((resolve, reject) => {
40 | post(endpoint, data).then((response) => resolve(response.data)).catch((err) => reject(err));
41 | });
42 | }
43 | exports.createUserS3Bucket = createUserS3Bucket;
44 | ;
45 | function createFunctionResource(fileName, yamlConfig) {
46 | fileName = fileName.replace(/\.[^/.]+$/, '');
47 | const funcTemplate = {
48 | Type: 'AWS::Serverless::Function',
49 | Properties: {
50 | Handler: `${fileName}.handler`,
51 | Runtime: 'nodejs8.10',
52 | CodeUri: '.',
53 | Description: 'A function deployed with Airfn CLI',
54 | MemorySize: 512,
55 | Timeout: 10,
56 | Events: {
57 | Api1: {
58 | Type: 'Api',
59 | Properties: {
60 | Path: `/${fileName}`.toLowerCase(),
61 | Method: 'ANY',
62 | },
63 | },
64 | },
65 | },
66 | };
67 | yamlConfig.Resources[fileName] = funcTemplate;
68 | }
69 |
--------------------------------------------------------------------------------
/dist/lib/serve/serve.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | const express_1 = __importDefault(require("express"));
7 | const serveController_1 = __importDefault(require("./serveController"));
8 | const path_1 = __importDefault(require("path"));
9 | const querystring_1 = __importDefault(require("querystring"));
10 | const body_parser_1 = __importDefault(require("body-parser"));
11 | const createHandler = serveController_1.default(path_1.default, querystring_1.default);
12 | const chalk_1 = __importDefault(require("chalk"));
13 | function listen(src, port, useStatic, timeout) {
14 | const app = express_1.default();
15 | app.use(body_parser_1.default.json());
16 | app.use(body_parser_1.default.urlencoded({ extended: true }));
17 | app.get('/favicon.ico', function (req, res) {
18 | return res.status(204).end();
19 | });
20 | app.all('*', createHandler(src, false, 10), (req, res) => {
21 | return res.end();
22 | });
23 | const server = app.listen(port, () => {
24 | console.log(chalk_1.default.green(`Example app listening on port ${port}!`));
25 | });
26 | app.get('/favicon.ico', function (req, res) {
27 | res.status(204).end();
28 | });
29 | return {
30 | clearCache: (chunk) => {
31 | const module = path_1.default.join(process.cwd(), String(src), chunk);
32 | delete require.cache[require.resolve(module)];
33 | }
34 | };
35 | }
36 | exports.default = listen;
37 |
--------------------------------------------------------------------------------
/dist/lib/serve/serveController.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | // TODO: Proper TypeScript types for modules
4 | exports.default = (path, queryString) => (dir, useStatic, timeout) => {
5 | return function (req, res, next) {
6 | const fn = req.path.split('/').filter(name => name)[0];
7 | const joinModPath = path.join(process.cwd(), dir, fn);
8 | const handler = require(joinModPath);
9 | const lambdaReq = {
10 | path: req.path,
11 | httpMethod: req.method,
12 | queryStringParameters: queryString.parse(req.url.split(/\?(.+)/)[1]),
13 | headers: req.headers,
14 | body: req.body,
15 | };
16 | const callback = createCallback(res);
17 | const promise = handler.handler(lambdaReq, null, callback);
18 | Promise.all([promisifyHandler(promise, callback)]) // TODO: Implement promise with timeout
19 | .then(() => {
20 | return next();
21 | })
22 | .catch(err => {
23 | throw err;
24 | });
25 | };
26 | };
27 | function createCallback(res) {
28 | return function callback(err, lambdaRes) {
29 | if (err)
30 | return err; // TODO: Proper error handling
31 | res.statusCode = lambdaRes.statusCode;
32 | for (let key in lambdaRes.headers) {
33 | res.setHeader(key, lambdaRes.headers[key]);
34 | }
35 | if (lambdaRes.body) {
36 | res.write(lambdaRes.body);
37 | }
38 | };
39 | }
40 | function promisifyHandler(promise, callback) {
41 | if (!promise ||
42 | typeof promise.then !== 'function' ||
43 | typeof callback !== 'function')
44 | return;
45 | return promise
46 | .then((data) => {
47 | callback(null, data);
48 | })
49 | .catch((err) => {
50 | callback(err, null);
51 | });
52 | }
53 |
--------------------------------------------------------------------------------
/dist/lib/serve/serveController.test.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 | return new (P || (P = Promise))(function (resolve, reject) {
4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
7 | step((generator = generator.apply(thisArg, _arguments || [])).next());
8 | });
9 | };
10 | const { createHandler, createCallback, promiseHandler, } = require('./serveController')();
11 | /* Sets up test mocks for Express request and response objects */
12 | function setup() {
13 | const req = {
14 | body: {},
15 | path: '',
16 | url: '',
17 | };
18 | const res = {
19 | locals: {
20 | error: {},
21 | lambdaResponse: {},
22 | },
23 | statusCode: null,
24 | body: '',
25 | headers: {},
26 | };
27 | const next = jest.fn();
28 | Object.assign(res, {
29 | status: jest.fn(function status() {
30 | return this;
31 | }.bind(res)),
32 | json: jest.fn(function json() {
33 | return this;
34 | }.bind(res)),
35 | send: jest.fn(function send() {
36 | return this;
37 | }.bind(res)),
38 | });
39 | return { req, res, next };
40 | }
41 | describe('createHandler', () => {
42 | test('Should have proper error object in res.locals if requiring function module fails', () => __awaiter(this, void 0, void 0, function* () {
43 | const { req, res, next } = setup();
44 | req.path = '/helloasync';
45 | const dir = '/functions';
46 | const useStatic = false;
47 | const timeout = 5;
48 | const errorObj = {
49 | code: 500,
50 | type: 'Server',
51 | message: 'Loading function failed',
52 | };
53 | yield createHandler(dir, useStatic, timeout)(req, res, () => {
54 | expect(res.locals.error).toEqual(errorObj);
55 | });
56 | }));
57 | test('Should have a proper error object in res.locals if lambda is not invoked before timeout', () => __awaiter(this, void 0, void 0, function* () {
58 | const { req, res, next } = setup();
59 | req.path = '/helloasync';
60 | const dir = '/functions';
61 | const useStatic = false;
62 | const timeout = 5;
63 | const errorObj = {
64 | code: 400,
65 | type: 'Client',
66 | message: 'Failed to invoke function before timeout',
67 | };
68 | yield createHandler(dir, useStatic, timeout)(req, res, () => {
69 | expect(res.locals.error).not.toEqual(errorObj);
70 | });
71 | }));
72 | test('Should have a proper lambdaResponse object in res.locals', () => __awaiter(this, void 0, void 0, function* () {
73 | const { req, res, next } = setup();
74 | req.path = '/helloasync';
75 | req.url = 'http://localhost:9000/helloasync';
76 | const dir = '/functions';
77 | const useStatic = false;
78 | const timeout = 5;
79 | const lambdaResponse = [
80 | {
81 | statusCode: 200,
82 | body: 'Hello, World',
83 | },
84 | ];
85 | yield createHandler(dir, useStatic, timeout)(req, res, () => {
86 | expect(res.locals.lambdaResponse).not.toEqual(lambdaResponse);
87 | });
88 | }));
89 | });
90 | describe('createCallback', () => {
91 | const res = {
92 | headers: {
93 | 'Access-Control-Allow-Origin': '*',
94 | 'Access-Control-Allow-Headers': 'Content-Type',
95 | },
96 | statusCode: 200,
97 | body: 'Why should you never trust a pig with a ' +
98 | "secret? Because it's bound to squeal.",
99 | };
100 | test('Should return a function', () => __awaiter(this, void 0, void 0, function* () {
101 | const res = {};
102 | yield expect(typeof createCallback(res)).toEqual('function');
103 | }));
104 | test('Returned callback should be able to handle errors', () => __awaiter(this, void 0, void 0, function* () {
105 | const res = {
106 | headers: {
107 | 'Access-Control-Allow-Origin': '*',
108 | 'Access-Control-Allow-Headers': 'Content-Type',
109 | },
110 | statusCode: 200,
111 | body: 'Why should you never trust a pig with a ' +
112 | "secret? Because it's bound to squeal.",
113 | };
114 | const returnFunc = yield createCallback(res);
115 | expect(returnFunc(new Error('this is an error'), null)).toBeInstanceOf(Error);
116 | }));
117 | test('Callback should set proper response object with status code, headers, and body', () => __awaiter(this, void 0, void 0, function* () {
118 | const { res } = setup();
119 | const lambdaResponse = {
120 | statusCode: 200,
121 | headers: {
122 | header1: 'Facebook',
123 | header2: 'Google',
124 | },
125 | body: 'Hello, World',
126 | };
127 | yield createCallback(res)(null, lambdaResponse);
128 | yield expect(res).toMatchObject(lambdaResponse);
129 | }));
130 | });
131 | describe('promiseHandler', () => { });
132 |
--------------------------------------------------------------------------------
/dist/lib/types.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Airfn",
3 | "version": "0.0.1",
4 | "description": "Serve, build, and deploy AWS Lambda functions",
5 | "main": "index.js",
6 | "dependencies": {
7 | "aws-kcl": "^2.0.0",
8 | "axios": "^0.19.0",
9 | "body-parser": "^1.19.0",
10 | "commander": "^2.20.0",
11 | "express": "^4.17.1",
12 | "inquirer": "^6.4.1",
13 | "js-yaml": "^3.13.1",
14 | "ora": "^3.4.0",
15 | "querystring": "^0.2.0"
16 | },
17 | "devDependencies": {
18 | "@babel/plugin-proposal-class-properties": "^7.4.4",
19 | "@babel/plugin-proposal-object-rest-spread": "^7.4.4",
20 | "@babel/plugin-transform-object-assign": "^7.2.0",
21 | "@babel/preset-env": "^7.4.5",
22 | "@types/body-parser": "^1.17.0",
23 | "@types/chalk": "^2.2.0",
24 | "@types/express": "^4.17.0",
25 | "@types/inquirer": "^6.0.3",
26 | "@types/jest": "^24.0.15",
27 | "@types/js-yaml": "^3.12.1",
28 | "@types/node": "^12.0.10",
29 | "@types/webpack": "^4.4.34",
30 | "babel-loader": "^8.0.6",
31 | "jest": "^24.8.0",
32 | "ts-jest": "^24.0.2",
33 | "ts-node": "^8.3.0",
34 | "typescript": "^3.5.2",
35 | "webpack": "^4.35.2"
36 | },
37 | "bin": {
38 | "airfn": "./dist/bin/command.js",
39 | "air": "./dist/bin/command.js"
40 | },
41 | "scripts": {
42 | "start": "npm run dev",
43 | "dev": "nodemon --exec 'ts-node' src/bin/command.ts",
44 | "prod": "tsc && node ./build/app.js",
45 | "tsc": "tsc",
46 | "test": "jest"
47 | },
48 | "keywords": [
49 | "lambda",
50 | "serverless",
51 | "faas",
52 | "aws"
53 | ],
54 | "author": "Bruce Wong, Esther Lee, Jayvee Aspa, Jun Lee",
55 | "license": "MIT"
56 | }
57 |
--------------------------------------------------------------------------------
/src/bin/command.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import os from 'os';
4 | import fs from 'fs';
5 | import path from 'path';
6 | import program from 'commander';
7 | import inquirer from 'inquirer';
8 | import ora from 'ora';
9 | import chalk from 'chalk';
10 | import axios, { AxiosResponse } from 'axios';
11 | import listen from '../lib/serve/serve';
12 | import { run, watch } from '../lib/build/build';
13 | import deploy from '../lib/deploy/deploy';
14 | import { projConfig } from '../lib/types';
15 | import { config } from 'rxjs';
16 |
17 | // TODO allow custom configuration of API Gateway subdomain
18 | const ROOT_CONFIG_FILENAME = 'config.json';
19 | const ROOT_CONFIG_DIRNAME = '.airfn';
20 | const BASE_API_GATEWAY_ENDPOINT = 'lambda9.cloud';
21 | const AUTH_ENDPOINT = 'https://test.lambda9.cloud/cli/cliauth';
22 | const SPINNER_TIMEOUT = 1000;
23 | declare global {
24 | interface JSON {
25 | parse(text: Buffer, reviver?: (key: any, value: any) => any): any;
26 | }
27 | }
28 |
29 | const JSONpackage = JSON.parse(
30 | fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'))
31 | );
32 |
33 | program.version(JSONpackage.version);
34 |
35 | program
36 | .command('init')
37 | .description(
38 | 'Initialize configuration for serving, building, and deploying lambda functions'
39 | )
40 | .action(async () => {
41 | const airfnConfig: projConfig = {};
42 | const cwdName: string = path.parse(process.cwd()).name;
43 |
44 | console.log(`\n👤 Please login with your username and password\nYou can sign up for an account at https://airfn.io/signup\n`);
45 |
46 | // TODO: Implement actual auth
47 | await inquirer
48 | .prompt([
49 | {
50 | name: 'username',
51 | message: 'Username:',
52 | },
53 | ])
54 | .then(async (answers: any) => {
55 | const username = answers.username;
56 | airfnConfig.user = answers.username;
57 | await inquirer
58 | .prompt([
59 | {
60 | name: 'password',
61 | type: 'password',
62 | message: 'Password:',
63 | },
64 | ])
65 | .then(async (answers: any) => {
66 | const password = answers.password;
67 | const credentials = {
68 | username,
69 | password
70 | }
71 | await axios.post(AUTH_ENDPOINT, credentials).then((response: AxiosResponse) => {
72 | const homedir = os.homedir();
73 | const rootConfigDir = path.join(homedir, ROOT_CONFIG_DIRNAME);
74 | const rootConfigPath = path.join(rootConfigDir, ROOT_CONFIG_FILENAME);
75 |
76 | const rootConfig = {
77 | clientId: response.data
78 | };
79 | if (!fs.existsSync(rootConfigDir)){
80 | fs.mkdir(rootConfigDir, (err) => {
81 | if (err) console.log(`😓 Failed to build config: ${err}`);
82 | }); }
83 | fs.writeFile(rootConfigPath, JSON.stringify(rootConfig), err => {
84 | if (err) console.log(`😓 Failed to build config: ${err}`);
85 | });
86 | }).catch((err: Error) => {
87 | console.log(`❌ Wrong username/password combination.\n Retry by running 'air init' again`);
88 | process.exit()
89 | })
90 | });
91 | });
92 |
93 | await inquirer
94 | .prompt([
95 | {
96 | name: 'project',
97 | message: 'Enter project name for your lambda functions:',
98 | default: cwdName,
99 | },
100 | ])
101 | .then((answers: any) => {
102 | airfnConfig.project = answers.project;
103 | });
104 |
105 | await inquirer
106 | .prompt([
107 | {
108 | name: 'functionsSrc',
109 | message: 'In which directory are your lambda functions?',
110 | default: 'src/functions',
111 | },
112 | ])
113 | .then(async (answers: any) => {
114 | const functionsSrc = answers.functionsSrc;
115 | airfnConfig.functionsSrc = functionsSrc;
116 | if (!fs.existsSync(answers.functionsSrc)) {
117 | await inquirer
118 | .prompt([
119 | {
120 | type: 'confirm',
121 | name: 'createSrcDir',
122 | message: `There's no directory at ${
123 | answers.functionsSrc
124 | }. Would you like to create one now?`,
125 | },
126 | ])
127 | .then((answers: any) => {
128 | if (answers.createSrcDir === true && functionsSrc) {
129 | fs.mkdirSync(path.join(process.cwd(), functionsSrc!), {
130 | recursive: true,
131 | });
132 | }
133 | });
134 | }
135 | });
136 |
137 | await inquirer
138 | .prompt([
139 | {
140 | name: 'functionsOutput',
141 | message:
142 | 'In which directory would you like your built lambda functions? (a root level directory is recommended)',
143 | default: '/functions',
144 | },
145 | ])
146 | .then((answers: any) => {
147 | airfnConfig.functionsOutput = answers.functionsOutput;
148 | });
149 |
150 | await inquirer
151 | .prompt([
152 | {
153 | type: 'list',
154 | name: 'nodeRuntime',
155 | message: 'Which NodeJS runtime will your lambda functions use?',
156 | choices: ['10.15', '8.10'],
157 | },
158 | ])
159 | .then((answers: any) => {
160 | airfnConfig.nodeRuntime = answers.nodeRuntime;
161 | });
162 |
163 | await inquirer
164 | .prompt([
165 | {
166 | name: 'functionsOutput',
167 | message:
168 | 'On which local port do you want to serve your lambda functions?',
169 | default: '9000',
170 | },
171 | ])
172 | .then((answers: any) => {
173 | airfnConfig.port = Number(answers.functionsOutput);
174 | });
175 |
176 | fs.writeFile('airfn.json', JSON.stringify(airfnConfig), err => {
177 | if (err) console.log(`😓 Failed to build config: ${err}`);
178 | console.log('\n💾 Your Airfn config has been saved!');
179 | });
180 | });
181 |
182 | program
183 | .command('serve')
184 | .description('Serve and watch functions')
185 | .action(() => {
186 | getUserAccessKey();
187 | const airfnConfig = getUserLambdaConfig()!;
188 | const spinner = ora('☁️ Airfn: Serving functions...').start();
189 | setTimeout(() => {
190 | const useStatic = Boolean(program.static);
191 | let server: any;
192 | const startServer = () => {
193 | server = listen(
194 | airfnConfig.functionsOutput,
195 | airfnConfig.port || 9000,
196 | useStatic,
197 | Number(program.timeout) || 10
198 | );
199 | };
200 | if (useStatic) {
201 | startServer();
202 | return;
203 | }
204 | const { config: userWebpackConfig, babelrc: useBabelrc = true } = program;
205 | watch(
206 | airfnConfig.functionsSrc,
207 | airfnConfig.functionsOutput,
208 | airfnConfig.nodeRuntime,
209 | { userWebpackConfig, useBabelrc },
210 | (err: Error, stats: any) => {
211 | if (err) {
212 | console.error(err);
213 | return;
214 | }
215 | console.log(chalk.hex('#24c4f4')(stats.toString()));
216 | spinner.stop();
217 | if (!server) {
218 | startServer();
219 | console.log('\n✅ Done serving!');
220 | } else {
221 | console.log('\n🔨 Done rebuilding!');
222 | }
223 |
224 | stats.compilation.chunks.forEach((chunk: any) => {
225 | server.clearCache(chunk.name || chunk.id().toString());
226 | });
227 | }
228 | );
229 | }, SPINNER_TIMEOUT);
230 | });
231 |
232 | program
233 | .command('build')
234 | .description('Build functions')
235 | .action(() => {
236 | getUserAccessKey();
237 | const spinner = ora('☁️ Airfn: Building functions...').start();
238 | setTimeout(() => {
239 | const airfnConfig = getUserLambdaConfig()!;
240 | spinner.color = 'green';
241 | const { config: userWebpackConfig, babelrc: useBabelrc = true } = program;
242 | run(
243 | airfnConfig.functionsSrc,
244 | airfnConfig.functionsOutput,
245 | airfnConfig.nodeRuntime,
246 | {
247 | userWebpackConfig,
248 | useBabelrc,
249 | }
250 | )
251 | .then((stats: any) => {
252 | console.log(chalk.hex('#f496f4')(stats.toString()));
253 | spinner.stop();
254 | console.log('\n✅ Done building!');
255 | })
256 | .catch((err: Error) => {
257 | console.error(err);
258 | process.exit(1);
259 | });
260 | }, SPINNER_TIMEOUT);
261 | });
262 |
263 | program
264 | .command('deploy')
265 | .description('Deploys functions to AWS')
266 | .action(() => {
267 | const accessKey = getUserAccessKey();
268 | const airfnConfig = getUserLambdaConfig()!;
269 | const spinner = ora('☁️ Airfn: Deploying functions...').start();
270 | setTimeout(() => {
271 | const { config: userWebpackConfig, babelrc: useBabelrc = true } = program;
272 | // TODO: Handle already built functions
273 | run(
274 | airfnConfig.functionsSrc,
275 | airfnConfig.functionsOutput,
276 | airfnConfig.nodeRuntime,
277 | { userWebpackConfig, useBabelrc }
278 | )
279 | .then((stats: any) => {
280 | console.log(chalk.hex('#f496f4')(stats.toString()));
281 | deploy(
282 | airfnConfig.user,
283 | accessKey,
284 | airfnConfig.project,
285 | airfnConfig.functionsSrc,
286 | airfnConfig.functionsOutput
287 | )
288 | .then((result: any) => {
289 | // TODO: Give lambda endpoints to user
290 | spinner.stop();
291 | console.log(`\n🚀 Successfully deployed! ${result.data}`);
292 | console.log(`\n🔗 Lambda endpoints:`);
293 | result.endpoints.forEach((endpoint: string) => {
294 | console.log(
295 | `https://${airfnConfig.project}.${BASE_API_GATEWAY_ENDPOINT}/${endpoint}`
296 | );
297 | });
298 | })
299 | .catch((err: Error) => {
300 | spinner.stop();
301 | console.log(`😓 Failed to deploy: ${err}`);
302 | });
303 | })
304 | .catch((err: Error) => {
305 | console.error(err);
306 | process.exit(1);
307 | });
308 | }, SPINNER_TIMEOUT);
309 | });
310 |
311 | program
312 | .command('logout')
313 | .description('Log out of Airfn CLI')
314 | .action(() => {
315 | const { configFound, configDir } = rootConfigExists();
316 | if (configFound) {
317 | try {
318 | removeDir(configDir);
319 | console.log('Logged out of Airfn CLI');
320 | process.exit(0);
321 | } catch(err) {
322 | console.error(`Failed to log out`);
323 | }
324 | } else {
325 | console.log(`Already logged out`);
326 | process.exit(1);
327 | }
328 | });
329 |
330 | program.on('command:*', function() {
331 | console.error(`\n❌ "${program.args.join(' ')}" command not found!`);
332 | process.exit(1);
333 | });
334 |
335 | program.parse(process.argv);
336 |
337 | const NO_COMMAND_SPECIFIED = program.args.length === 0;
338 |
339 | if (NO_COMMAND_SPECIFIED) {
340 | program.help();
341 | }
342 |
343 | function getUserAccessKey() {
344 | const { configFound, configPath } = rootConfigExists();
345 | if (configFound) {
346 | try {
347 | const rootConfig = JSON.parse(
348 | fs.readFileSync(configPath, 'utf-8')
349 | );
350 | return rootConfig.clientId;
351 | } catch (err) {
352 | console.log(`❌ Error reading config`)
353 | }
354 |
355 | } else {
356 | console.log(`❗️ Please login first by running 'air init'`);
357 | process.exit(1);
358 | }
359 | }
360 |
361 | function rootConfigExists() {
362 | const homedir = os.homedir();
363 | const rootConfigDir = path.join(homedir, ROOT_CONFIG_DIRNAME);
364 | const rootConfigPath = path.join(rootConfigDir, ROOT_CONFIG_FILENAME);
365 | const configFound = fs.existsSync(rootConfigPath);
366 | const configProps = {
367 | configFound: configFound,
368 | configDir: rootConfigDir,
369 | configPath: rootConfigPath
370 | };
371 | return configProps;
372 | }
373 |
374 | function getUserLambdaConfig() {
375 | try {
376 | const config: projConfig = JSON.parse(
377 | fs.readFileSync(path.join(process.cwd(), 'airfn.json'), 'utf-8')
378 | );
379 | return config;
380 | } catch (err) {
381 | console.log(`❌ No Airfn config found. Did you first run 'l9 init'?`);
382 | process.exit(1);
383 | }
384 | }
385 |
386 | function removeDir(dir: string) {
387 | const list = fs.readdirSync(dir);
388 | for(let i = 0; i < list.length; i++) {
389 | const filename = path.join(dir, list[i]);
390 | const stat = fs.statSync(filename);
391 | if (stat.isDirectory()) {
392 | removeDir(filename);
393 | } else {
394 | fs.unlinkSync(filename);
395 | }
396 | }
397 | fs.rmdirSync(dir);
398 | }
399 |
400 |
--------------------------------------------------------------------------------
/src/lib/build/build.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import fs from 'fs';
3 | import webpack from 'webpack';
4 | import build from './buildController';
5 | const createWebpack = build(path, fs, webpack);
6 |
7 | function run(
8 | srcDir: string | void,
9 | outputDir: string | void,
10 | runtime: string | void,
11 | additionalConfig: object
12 | ) {
13 | return new Promise((resolve, reject) => {
14 | webpack(
15 | createWebpack(srcDir, outputDir, runtime, additionalConfig),
16 | (err, stats) => {
17 | if (err) {
18 | return reject(err);
19 | }
20 | resolve(stats);
21 | }
22 | );
23 | });
24 | }
25 |
26 | function watch(
27 | srcDir: string | void,
28 | outputDir: string | void,
29 | runtime: string | void,
30 | additionalConfig: object,
31 | cb: webpack.ICompiler.Handler
32 | ) {
33 | var compiler = webpack(
34 | createWebpack(srcDir, outputDir, runtime, additionalConfig)
35 | );
36 | compiler.watch(
37 | createWebpack(srcDir, outputDir, runtime, additionalConfig),
38 | cb
39 | );
40 | }
41 |
42 | export { run, watch };
43 |
--------------------------------------------------------------------------------
/src/lib/build/buildController.ts:
--------------------------------------------------------------------------------
1 | import { DefinePlugin, IgnorePlugin } from 'webpack';
2 |
3 | const testFilePattern = '\\.(test|spec)\\.?';
4 |
5 | interface babelOptsObj {
6 | cacheDirectory: boolean;
7 | presets: any[];
8 | plugins: any[];
9 | }
10 |
11 | interface webpackPlugins {
12 | DefinePlugin: typeof DefinePlugin;
13 | IgnorePlugin: typeof IgnorePlugin;
14 | }
15 |
16 | export default (
17 | path: { join: Function },
18 | fs: { readdirSync: Function },
19 | webpack: webpackPlugins
20 | ) => (
21 | srcDir: string | void,
22 | outputDir: string | void,
23 | runtime: string | void,
24 | { userWebpackConfig, useBabelrc }: any = {}
25 | ) => {
26 | const babelOpts: babelOptsObj = {
27 | cacheDirectory: true,
28 | presets: [
29 | [
30 | require.resolve('@babel/preset-env'),
31 | { targets: { node: getBabelTarget({}, runtime) } },
32 | ],
33 | ],
34 | plugins: [
35 | require.resolve('@babel/plugin-proposal-class-properties'),
36 | require.resolve('@babel/plugin-transform-object-assign'),
37 | require.resolve('@babel/plugin-proposal-object-rest-spread'),
38 | ],
39 | };
40 |
41 | const functionsDir = outputDir;
42 | const functionsPath = path.join(process.cwd(), functionsDir);
43 | const dirPath = path.join(process.cwd(), srcDir);
44 |
45 | const defineEnv = {};
46 | const nodeEnv = process.env.NODE_ENV || 'production';
47 |
48 | const webpackMode = ['production', 'development'].includes(nodeEnv)
49 | ? nodeEnv
50 | : 'none';
51 |
52 | const webpackConfig: any = {
53 | mode: webpackMode,
54 | resolve: {
55 | extensions: ['.wasm', '.mjs', '.js', '.json', '.ts'],
56 | mainFields: ['module', 'main'],
57 | },
58 | module: {
59 | rules: [
60 | {
61 | test: /\.(m?js|ts)?$/,
62 | exclude: new RegExp(
63 | `(node_modules|bower_components|${testFilePattern})`
64 | ),
65 | use: {
66 | loader: require.resolve('babel-loader'),
67 | options: { ...babelOpts, babelrc: useBabelrc },
68 | },
69 | },
70 | ],
71 | },
72 | context: dirPath,
73 | entry: {},
74 | target: 'node',
75 | plugins: [
76 | new webpack.IgnorePlugin(/vertx/),
77 | new webpack.DefinePlugin(defineEnv),
78 | ],
79 | output: {
80 | path: functionsPath,
81 | filename: '[name].js',
82 | libraryTarget: 'commonjs',
83 | },
84 | optimization: {
85 | nodeEnv,
86 | },
87 | bail: true,
88 | devtool: false,
89 | };
90 |
91 | fs.readdirSync(dirPath).forEach((file: string) => {
92 | if (file.match(/\.(m?js|ts)$/)) {
93 | var name = file.replace(/\.(m?js|ts)$/, '');
94 | if (!name.match(new RegExp(testFilePattern))) {
95 | webpackConfig.entry[name] = './' + file;
96 | }
97 | }
98 | });
99 |
100 | return webpackConfig;
101 | };
102 |
103 | function getBabelTarget(envConfig: any, runtime: string | void) {
104 | const key = 'AWS_LAMBDA_JS_RUNTIME';
105 | // If NodeJS runtime specified during l9 init
106 | if (runtime) return runtime;
107 | // Otherwise use user webpack settings if exists
108 | const runtimes = ['nodejs8.15.0', 'nodejs6.10.3'];
109 | const current = envConfig[key] || process.env[key] || 'nodejs8.15.0';
110 | const unknown = runtimes.indexOf(current) === -1;
111 | return unknown ? '8.15.0' : current.replace(/^nodejs/, '');
112 | }
113 |
--------------------------------------------------------------------------------
/src/lib/deploy/deploy.ts:
--------------------------------------------------------------------------------
1 | import { join, parse } from 'path';
2 | import { readFileSync, readdirSync, writeFileSync } from 'fs';
3 | import axios, { AxiosResponse } from 'axios';
4 | import { safeDump } from 'js-yaml';
5 | import { createDeployArtifacts, createUserS3Bucket } from './deployController';
6 |
7 | const DEPLOY_ENDPOINT = 'http://api.lambda9.cloud/lambda/deploy';
8 | const S3_CREATE_LAMBDA_ENDPOINT = 'https://cli.lambda9.cloud/createbucket';
9 | const CREATE_DOMAIN_ENDPOINT = 'https://cli.lambda9.cloud/deploydomain';
10 | const SAVE_FUNCTIONS_ENDPOINT = 'https://cli.lambda9.cloud/savefunctions';
11 | const LOGS_SUBSCRIPTION_ENDPOINT = 'https://cli.lambda9.cloud/subscribelogs';
12 | const LOG_GROUP_PREFIX = '/aws/lambda/';
13 | const BASE_DOMAIN = 'lambda9.cloud';
14 |
15 | interface funcObj {
16 | funcName: string;
17 | funcDef: string;
18 | }
19 |
20 | export default (
21 | user: string | void,
22 | accessKey: string,
23 | project: string | void,
24 | functionsSrc: string | void,
25 | functionsOutput: string | void
26 | ) => {
27 | return new Promise((resolve, reject) => {
28 | getFunctionsSourceCode();
29 | const deployArtifacts = createDeployArtifacts(
30 | functionsOutput,
31 | join,
32 | {
33 | readFileSync,
34 | readdirSync,
35 | },
36 | safeDump
37 | );
38 | createUserS3Bucket(S3_CREATE_LAMBDA_ENDPOINT, user, axios.post)
39 | .then((response: any) => {
40 | const requestData = {
41 | user,
42 | project,
43 | ...deployArtifacts,
44 | };
45 | axios({
46 | method: 'post',
47 | url: DEPLOY_ENDPOINT,
48 | data: requestData,
49 | maxContentLength: Infinity,
50 | })
51 | .then((response: AxiosResponse) => {
52 | const lambdaData = {
53 | endpoints,
54 | data: response.data,
55 | };
56 |
57 | createDomain(project, project);
58 | saveFunctions(functionsSourceCode, project, accessKey);
59 | subscribeToLogs(logGroupPrefixes);
60 |
61 | return resolve(lambdaData);
62 | })
63 | .catch(err => {
64 | return reject(err);
65 | });
66 | })
67 | .catch((err: Error) => {
68 | console.log('😓 Error making S3 buckets for lambda functions');
69 | });
70 |
71 | const logGroupPrefixes = createLogGroupPrefixes(deployArtifacts.funcArr, project);
72 | const functionsSourceCode = getFunctionsSourceCode();
73 | const endpoints = createEndpoints(deployArtifacts.funcArr);
74 | });
75 |
76 | function createDomain(subdomainPrefix: string | void, stackName: string | void) {
77 | const data = {
78 | domainName: `${subdomainPrefix}.${BASE_DOMAIN}`,
79 | stackName
80 | }
81 |
82 | axios({
83 | method: "post",
84 | url: CREATE_DOMAIN_ENDPOINT,
85 | data
86 | })
87 | .then((response: AxiosResponse) => {
88 | console.log(`\n${response.data}`);
89 | })
90 | .catch(err => {
91 | console.log('😓 Error creating lambda subdomain');
92 | });
93 | }
94 |
95 | function saveFunctions(functions: any, projectName: string | void, accessKey: string) {
96 | const data = {
97 | functions,
98 | projectName,
99 | accessKey
100 | };
101 |
102 | axios({
103 | method: "post",
104 | url: SAVE_FUNCTIONS_ENDPOINT,
105 | data,
106 | maxContentLength: Infinity
107 | })
108 | .then((response: AxiosResponse) => {
109 | console.log('Saved lambda functions');
110 | })
111 | .catch(err => {
112 | });
113 | }
114 |
115 | function createLogGroupPrefixes(functions: any, projectName: string | void) {
116 | return functions.map((funcObj: funcObj) => {
117 | const funcName = parse(funcObj.funcName).name;
118 | return `${LOG_GROUP_PREFIX}${projectName}-${funcName}`;
119 | })
120 | }
121 |
122 | function subscribeToLogs(logGroupsPrefixes: [any]) {
123 | const data = {
124 | logGroupsPrefixes
125 | };
126 |
127 | axios({
128 | method: "post",
129 | url: LOGS_SUBSCRIPTION_ENDPOINT,
130 | data
131 | })
132 | .then((response: AxiosResponse) => {
133 | })
134 | .catch(err => {
135 | });
136 | }
137 |
138 | function createEndpoints(functions: any) {
139 | return functions.map((funcObj: funcObj) => {
140 | return parse(funcObj.funcName).name.toLowerCase();
141 | });
142 | }
143 |
144 | function getFunctionsSourceCode() {
145 | const funcArr: any = [];
146 | readdirSync(join(process.cwd(), String(functionsSrc))).forEach((file: string) => {
147 | const data = readFileSync(
148 | join(process.cwd(), `${functionsSrc}/${file}`),
149 | 'utf8'
150 | );
151 | const funcObj: funcObj = {
152 | funcName: file,
153 | funcDef: data,
154 | };
155 | funcArr.push(funcObj);
156 | });
157 | return funcArr;
158 | }
159 | };
160 |
--------------------------------------------------------------------------------
/src/lib/deploy/deployController.ts:
--------------------------------------------------------------------------------
1 | const YAML_CONFIG_TEMPLATE: config = {
2 | AWSTemplateFormatVersion: '2010-09-09',
3 | Transform: 'AWS::Serverless-2016-10-31',
4 | Description: 'Deployed with Airfn CLI.',
5 | Outputs: {
6 | ApiGatewayId: {
7 | Value: {
8 | Ref: "ServerlessRestApi"
9 | }
10 | }
11 | },
12 | Resources: {}
13 | };
14 |
15 | interface config {
16 | AWSTemplateFormatVersion: string;
17 | Transform: string;
18 | Description: string;
19 | Resources: object;
20 | Outputs: object;
21 | }
22 |
23 | function createDeployArtifacts(
24 | functionsOutput: string | void,
25 | join: Function,
26 | fs: { readFileSync: Function; readdirSync: Function },
27 | safeDump: Function
28 | ) {
29 | const funcArr: any = [];
30 | const yamlConfig: config = YAML_CONFIG_TEMPLATE;
31 |
32 | fs.readdirSync(join(process.cwd(), functionsOutput)).forEach((file: string) => {
33 | createFunctionResource(file, yamlConfig);
34 | const data = fs.readFileSync(
35 | join(process.cwd(), `${functionsOutput}/${file}`),
36 | 'utf8'
37 | );
38 | const funcObj: object = {
39 | funcName: file,
40 | funcDef: data,
41 | };
42 | funcArr.push(funcObj);
43 | });
44 |
45 | return {
46 | yaml: safeDump(yamlConfig, { noCompatMode: true, noRefs: true}),
47 | funcArr,
48 | };
49 | };
50 |
51 | function createUserS3Bucket(endpoint: string, user: string | void, post: Function) {
52 | const data = {
53 | user
54 | };
55 | return new Promise((resolve, reject) => {
56 | post(endpoint, data).then((response: any) => resolve(response.data)).catch((err: Error) => reject(err));
57 | });
58 | };
59 |
60 | function createFunctionResource(fileName: string, yamlConfig: any): void {
61 | fileName = fileName.replace(/\.[^/.]+$/, '');
62 | const funcTemplate: object = {
63 | Type: 'AWS::Serverless::Function',
64 | Properties: {
65 | Handler: `${fileName}.handler`,
66 | Runtime: 'nodejs8.10',
67 | CodeUri: '.',
68 | Description: 'A function deployed with Airfn CLI',
69 | MemorySize: 512,
70 | Timeout: 10,
71 | Events: {
72 | Api1: {
73 | Type: 'Api',
74 | Properties: {
75 | Path: `/${fileName}`.toLowerCase(),
76 | Method: 'ANY',
77 | },
78 | },
79 | },
80 | },
81 | };
82 | yamlConfig.Resources[fileName] = funcTemplate;
83 | }
84 |
85 | export { createDeployArtifacts, createUserS3Bucket }
--------------------------------------------------------------------------------
/src/lib/serve/serve.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import serve from './serveController';
3 | import path from 'path';
4 | import queryString from 'querystring';
5 | import bodyParser from 'body-parser';
6 | const createHandler = serve(path, queryString);
7 |
8 | import chalk from 'chalk';
9 |
10 | function listen(
11 | src: string | void,
12 | port: number,
13 | useStatic: boolean,
14 | timeout: number
15 | ) : any {
16 | const app: express.Application = express();
17 | app.use(bodyParser.json());
18 | app.use(bodyParser.urlencoded({ extended: true }));
19 | app.get('/favicon.ico', function(req, res) {
20 | return res.status(204).end();
21 | });
22 |
23 | app.all('*', createHandler(src, false, 10), (req, res) => {
24 | return res.end();
25 | });
26 |
27 | const server: any = app.listen(port, () => {
28 | console.log(chalk.green(`Example app listening on port ${port}!`));
29 | });
30 |
31 |
32 | app.get('/favicon.ico', function(req, res) {
33 | res.status(204).end();
34 | });
35 |
36 | return {
37 | clearCache: (chunk : any) => {
38 | const module = path.join(process.cwd(), String(src), chunk);
39 | delete require.cache[require.resolve(module)];
40 | }
41 | };
42 |
43 | }
44 | export default listen;
45 |
--------------------------------------------------------------------------------
/src/lib/serve/serveController.test.ts:
--------------------------------------------------------------------------------
1 | const {
2 | createHandler,
3 | createCallback,
4 | promiseHandler,
5 | } = require('./serveController')();
6 |
7 | /* Sets up test mocks for Express request and response objects */
8 | function setup() {
9 | const req = {
10 | body: {},
11 | path: '',
12 | url: '',
13 | };
14 | const res = {
15 | locals: {
16 | error: {},
17 | lambdaResponse: {},
18 | },
19 | statusCode: null,
20 | body: '',
21 | headers: {},
22 | };
23 | const next = jest.fn();
24 | Object.assign(res, {
25 | status: jest.fn(
26 | function status(this: object) {
27 | return this;
28 | }.bind(res)
29 | ),
30 | json: jest.fn(
31 | function json(this: object) {
32 | return this;
33 | }.bind(res)
34 | ),
35 | send: jest.fn(
36 | function send(this: object) {
37 | return this;
38 | }.bind(res)
39 | ),
40 | });
41 | return { req, res, next };
42 | }
43 |
44 | describe('createHandler', () => {
45 | test('Should have proper error object in res.locals if requiring function module fails', async () => {
46 | const { req, res, next } = setup();
47 | req.path = '/helloasync';
48 | const dir = '/functions';
49 | const useStatic = false;
50 | const timeout = 5;
51 | const errorObj = {
52 | code: 500,
53 | type: 'Server',
54 | message: 'Loading function failed',
55 | };
56 | await createHandler(dir, useStatic, timeout)(req, res, () => {
57 | expect(res.locals.error).toEqual(errorObj);
58 | });
59 | });
60 |
61 | test('Should have a proper error object in res.locals if lambda is not invoked before timeout', async () => {
62 | const { req, res, next } = setup();
63 | req.path = '/helloasync';
64 | const dir = '/functions';
65 | const useStatic = false;
66 | const timeout = 5;
67 | const errorObj = {
68 | code: 400,
69 | type: 'Client',
70 | message: 'Failed to invoke function before timeout',
71 | };
72 | await createHandler(dir, useStatic, timeout)(req, res, () => {
73 | expect(res.locals.error).not.toEqual(errorObj);
74 | });
75 | });
76 |
77 | test('Should have a proper lambdaResponse object in res.locals', async () => {
78 | const { req, res, next } = setup();
79 | req.path = '/helloasync';
80 | req.url = 'http://localhost:9000/helloasync';
81 | const dir = '/functions';
82 | const useStatic = false;
83 | const timeout = 5;
84 | const lambdaResponse = [
85 | {
86 | statusCode: 200,
87 | body: 'Hello, World',
88 | },
89 | ];
90 | await createHandler(dir, useStatic, timeout)(req, res, () => {
91 | expect(res.locals.lambdaResponse).not.toEqual(lambdaResponse);
92 | });
93 | });
94 | });
95 |
96 | describe('createCallback', () => {
97 | const res = {
98 | headers: {
99 | 'Access-Control-Allow-Origin': '*',
100 | 'Access-Control-Allow-Headers': 'Content-Type',
101 | },
102 | statusCode: 200,
103 | body:
104 | 'Why should you never trust a pig with a ' +
105 | "secret? Because it's bound to squeal.",
106 | };
107 |
108 | test('Should return a function', async () => {
109 | const res = {};
110 | await expect(typeof createCallback(res)).toEqual('function');
111 | });
112 |
113 | test('Returned callback should be able to handle errors', async () => {
114 | const res = {
115 | headers: {
116 | 'Access-Control-Allow-Origin': '*',
117 | 'Access-Control-Allow-Headers': 'Content-Type',
118 | },
119 | statusCode: 200,
120 | body:
121 | 'Why should you never trust a pig with a ' +
122 | "secret? Because it's bound to squeal.",
123 | };
124 | const returnFunc = await createCallback(res);
125 | expect(returnFunc(new Error('this is an error'), null)).toBeInstanceOf(
126 | Error
127 | );
128 | });
129 |
130 | test('Callback should set proper response object with status code, headers, and body', async () => {
131 | const { res } = setup();
132 | const lambdaResponse = {
133 | statusCode: 200,
134 | headers: {
135 | header1: 'Facebook',
136 | header2: 'Google',
137 | },
138 | body: 'Hello, World',
139 | };
140 | await createCallback(res)(null, lambdaResponse);
141 | await expect(res).toMatchObject(lambdaResponse);
142 | });
143 | });
144 | describe('promiseHandler', () => {});
145 |
--------------------------------------------------------------------------------
/src/lib/serve/serveController.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from 'express';
2 |
3 | // TODO: Proper TypeScript types for modules
4 | export default (path: { join: Function }, queryString: { parse: Function }) => (
5 | dir: string | void,
6 | useStatic: boolean,
7 | timeout: number
8 | ) => {
9 | return function(req: Request, res: Response, next: Function) {
10 | const fn: string = req.path.split('/').filter(name => name)[0];
11 | const joinModPath = path.join(process.cwd(), dir, fn);
12 | const handler = require(joinModPath);
13 | const lambdaReq = {
14 | path: req.path,
15 | httpMethod: req.method,
16 | queryStringParameters: queryString.parse(req.url.split(/\?(.+)/)[1]),
17 | headers: req.headers,
18 | body: req.body,
19 | };
20 | const callback = createCallback(res);
21 | const promise = handler.handler(lambdaReq, null, callback);
22 | Promise.all([promisifyHandler(promise, callback)]) // TODO: Implement promise with timeout
23 | .then(() => {
24 | return next();
25 | })
26 | .catch(err => {
27 | throw err;
28 | });
29 | };
30 | };
31 |
32 | function createCallback(res: Response) {
33 | return function callback(err: Error | null, lambdaRes: any) {
34 | if (err) return err; // TODO: Proper error handling
35 | res.statusCode = lambdaRes.statusCode;
36 | for (let key in lambdaRes.headers) {
37 | res.setHeader(key, lambdaRes.headers[key]);
38 | }
39 | if (lambdaRes.body) {
40 | res.write(lambdaRes.body);
41 | }
42 | };
43 | }
44 |
45 | function promisifyHandler(promise: { then: Function }, callback: Function) {
46 | if (
47 | !promise ||
48 | typeof promise.then !== 'function' ||
49 | typeof callback !== 'function'
50 | )
51 | return;
52 |
53 | return promise
54 | .then((data: object) => {
55 | callback(null, data);
56 | })
57 | .catch((err: Error | null) => {
58 | callback(err, null);
59 | });
60 | }
61 |
--------------------------------------------------------------------------------
/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | export type projConfig = {
2 | user?: string;
3 | project?: string;
4 | functionsSrc?: string;
5 | functionsOutput?: string;
6 | nodeRuntime?: string;
7 | port?: number;
8 | };
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | // "incremental": true, /* Enable incremental compilation */
5 | "target": "es2015" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
7 | // "lib": [], /* Specify library files to be included in the compilation. */
8 | // "allowJs": true, /* Allow javascript files to be compiled. */
9 | // "checkJs": true, /* Report errors in .js files. */
10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
13 | // "sourceMap": true, /* Generates corresponding '.map' file. */
14 | // "outFile": "./", /* Concatenate and emit output to single file. */
15 | "outDir": "./dist" /* Redirect output structure to the directory. */,
16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
17 | // "composite": true, /* Enable project compilation */
18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
19 | // "removeComments": true, /* Do not emit comments to output. */
20 | // "noEmit": true, /* Do not emit outputs. */
21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
24 | /* Strict Type-Checking Options */
25 | "strict": true /* Enable all strict type-checking options. */,
26 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
27 | // "strictNullChecks": true, /* Enable strict null checks. */
28 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
29 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
30 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
31 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
32 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
33 | /* Additional Checks */
34 | // "noUnusedLocals": true, /* Report errors on unused locals. */
35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
38 | /* Module Resolution Options */
39 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
40 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
41 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
42 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
43 | "typeRoots": [
44 | "node_modules/@types"
45 | ] /* List of folders to include type definitions from. */,
46 | "types": [
47 | "node",
48 | "express",
49 | "body-parser",
50 | "jest",
51 | "webpack"
52 | ] /* Type declaration files to be included in compilation. */,
53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
54 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
57 | /* Source Map Options */
58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
62 | /* Experimental Options */
63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
65 | },
66 | "exclude": ["node_modules", "dist"]
67 | }
68 |
--------------------------------------------------------------------------------