From 6c15f357eed9e3090e384f2781dc21a53dfe3985 Mon Sep 17 00:00:00 2001 From: John Reeves Date: Mon, 3 Feb 2020 14:35:09 -0800 Subject: [PATCH 1/2] moved TypeScriptPlugin class into its own file. This fixes issues with the mixed types of exports so other code (tests) can import TypeScriptPlugin. --- src/index.ts | 277 +--------------------------------------- src/typeScriptPlugin.ts | 276 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+), 276 deletions(-) create mode 100644 src/typeScriptPlugin.ts diff --git a/src/index.ts b/src/index.ts index a249ce58..48ae52de 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,278 +1,3 @@ -import * as path from 'path' -import * as fs from 'fs-extra' -import * as _ from 'lodash' -import * as globby from 'globby' - -import * as typescript from './typescript' -import { watchFiles } from './watchFiles' - -const SERVERLESS_FOLDER = '.serverless' -const BUILD_FOLDER = '.build' - -export class TypeScriptPlugin { - private originalServicePath: string - private isWatching: boolean - - serverless: Serverless.Instance - options: Serverless.Options - hooks: { [key: string]: Function } - - constructor(serverless: Serverless.Instance, options: Serverless.Options) { - this.serverless = serverless - this.options = options - - this.hooks = { - 'before:run:run': async () => { - await this.compileTs() - await this.copyExtras() - await this.copyDependencies() - }, - 'before:offline:start': async () => { - await this.compileTs() - await this.copyExtras() - await this.copyDependencies() - this.watchAll() - }, - 'before:offline:start:init': async () => { - await this.compileTs() - await this.copyExtras() - await this.copyDependencies() - this.watchAll() - }, - 'before:package:createDeploymentArtifacts': async () => { - await this.compileTs() - await this.copyExtras() - await this.copyDependencies(true) - }, - 'after:package:createDeploymentArtifacts': async () => { - await this.cleanup() - }, - 'before:deploy:function:packageFunction': async () => { - await this.compileTs() - await this.copyExtras() - await this.copyDependencies(true) - }, - 'after:deploy:function:packageFunction': async () => { - await this.cleanup() - }, - 'before:invoke:local:invoke': async () => { - const emitedFiles = await this.compileTs() - await this.copyExtras() - await this.copyDependencies() - if (this.isWatching) { - emitedFiles.forEach(filename => { - const module = require.resolve(path.resolve(this.originalServicePath, filename)) - delete require.cache[module] - }) - } - }, - 'after:invoke:local:invoke': () => { - if (this.options.watch) { - this.watchFunction() - this.serverless.cli.log('Waiting for changes...') - } - } - } - } - - get functions() { - const { options } = this - const { service } = this.serverless - - if (options.function) { - return { - [options.function]: service.functions[this.options.function] - } - } - - return service.functions - } - - get rootFileNames() { - return typescript.extractFileNames( - this.originalServicePath, - this.serverless.service.provider.name, - this.functions - ) - } - - prepare() { - // exclude serverless-plugin-typescript - for (const fnName in this.functions) { - const fn = this.functions[fnName] - fn.package = fn.package || { - exclude: [], - include: [], - } - - // Add plugin to excluded packages or an empty array if exclude is undefined - fn.package.exclude = _.uniq([...fn.package.exclude || [], 'node_modules/serverless-plugin-typescript']) - } - } - - async watchFunction(): Promise { - if (this.isWatching) { - return - } - - this.serverless.cli.log(`Watch function ${this.options.function}...`) - - this.isWatching = true - watchFiles(this.rootFileNames, this.originalServicePath, () => { - this.serverless.pluginManager.spawn('invoke:local') - }) - } - - async watchAll(): Promise { - if (this.isWatching) { - return - } - - this.serverless.cli.log(`Watching typescript files...`) - - this.isWatching = true - watchFiles(this.rootFileNames, this.originalServicePath, this.compileTs.bind(this)) - } - - async compileTs(): Promise { - this.prepare() - this.serverless.cli.log('Compiling with Typescript...') - - if (!this.originalServicePath) { - // Save original service path and functions - this.originalServicePath = this.serverless.config.servicePath - // Fake service path so that serverless will know what to zip - this.serverless.config.servicePath = path.join(this.originalServicePath, BUILD_FOLDER) - } - - const tsconfig = typescript.getTypescriptConfig( - this.originalServicePath, - this.isWatching ? null : this.serverless.cli - ) - - tsconfig.outDir = BUILD_FOLDER - - const emitedFiles = await typescript.run(this.rootFileNames, tsconfig) - this.serverless.cli.log('Typescript compiled.') - return emitedFiles - } - - /** Link or copy extras such as node_modules or package.include definitions */ - async copyExtras() { - const { service } = this.serverless - - // include any "extras" from the "include" section - if (service.package.include && service.package.include.length > 0) { - const files = await globby(service.package.include) - - for (const filename of files) { - const destFileName = path.resolve(path.join(BUILD_FOLDER, filename)) - const dirname = path.dirname(destFileName) - - if (!fs.existsSync(dirname)) { - fs.mkdirpSync(dirname) - } - - if (!fs.existsSync(destFileName)) { - fs.copySync(path.resolve(filename), path.resolve(path.join(BUILD_FOLDER, filename))) - } - } - } - } - - /** - * Copy the `node_modules` folder and `package.json` files to the output - * directory. - * @param isPackaging Provided if serverless is packaging the service for deployment - */ - async copyDependencies(isPackaging = false) { - const outPkgPath = path.resolve(path.join(BUILD_FOLDER, 'package.json')) - const outModulesPath = path.resolve(path.join(BUILD_FOLDER, 'node_modules')) - - // copy development dependencies during packaging - if (isPackaging) { - if (fs.existsSync(outModulesPath)) { - fs.unlinkSync(outModulesPath) - } - - fs.copySync( - path.resolve('node_modules'), - path.resolve(path.join(BUILD_FOLDER, 'node_modules')) - ) - } else { - if (!fs.existsSync(outModulesPath)) { - await this.linkOrCopy(path.resolve('node_modules'), outModulesPath, 'junction') - } - } - - // copy/link package.json - if (!fs.existsSync(outPkgPath)) { - await this.linkOrCopy(path.resolve('package.json'), outPkgPath, 'file') - } - } - - /** - * Move built code to the serverless folder, taking into account individual - * packaging preferences. - */ - async moveArtifacts(): Promise { - const { service } = this.serverless - - await fs.copy( - path.join(this.originalServicePath, BUILD_FOLDER, SERVERLESS_FOLDER), - path.join(this.originalServicePath, SERVERLESS_FOLDER) - ) - - if (this.options.function) { - const fn = service.functions[this.options.function] - fn.package.artifact = path.join( - this.originalServicePath, - SERVERLESS_FOLDER, - path.basename(fn.package.artifact) - ) - return - } - - if (service.package.individually) { - const functionNames = service.getAllFunctions() - functionNames.forEach(name => { - service.functions[name].package.artifact = path.join( - this.originalServicePath, - SERVERLESS_FOLDER, - path.basename(service.functions[name].package.artifact) - ) - }) - return - } - - service.package.artifact = path.join( - this.originalServicePath, - SERVERLESS_FOLDER, - path.basename(service.package.artifact) - ) - } - - async cleanup(): Promise { - await this.moveArtifacts() - // Restore service path - this.serverless.config.servicePath = this.originalServicePath - // Remove temp build folder - fs.removeSync(path.join(this.originalServicePath, BUILD_FOLDER)) - } - - /** - * Attempt to symlink a given path or directory and copy if it fails with an - * `EPERM` error. - */ - private async linkOrCopy(srcPath: string, dstPath: string, type?: fs.FsSymlinkType): Promise { - return fs.symlink(srcPath, dstPath, type) - .catch(error => { - if (error.code === 'EPERM' && error.errno === -4048) { - return fs.copy(srcPath, dstPath) - } - throw error - }) - } -} +import {TypeScriptPlugin} from './typeScriptPlugin' module.exports = TypeScriptPlugin diff --git a/src/typeScriptPlugin.ts b/src/typeScriptPlugin.ts new file mode 100644 index 00000000..371b80b7 --- /dev/null +++ b/src/typeScriptPlugin.ts @@ -0,0 +1,276 @@ +import * as path from 'path' +import * as fs from 'fs-extra' +import * as _ from 'lodash' +import * as globby from 'globby' + +import * as typescript from './typescript' +import { watchFiles } from './watchFiles' + +const SERVERLESS_FOLDER = '.serverless' +const BUILD_FOLDER = '.build' + +export class TypeScriptPlugin { + private originalServicePath: string + private isWatching: boolean + + serverless: Serverless.Instance + options: Serverless.Options + hooks: { [key: string]: Function } + + constructor(serverless: Serverless.Instance, options: Serverless.Options) { + this.serverless = serverless + this.options = options + + this.hooks = { + 'before:run:run': async () => { + await this.compileTs() + await this.copyExtras() + await this.copyDependencies() + }, + 'before:offline:start': async () => { + await this.compileTs() + await this.copyExtras() + await this.copyDependencies() + this.watchAll() + }, + 'before:offline:start:init': async () => { + await this.compileTs() + await this.copyExtras() + await this.copyDependencies() + this.watchAll() + }, + 'before:package:createDeploymentArtifacts': async () => { + await this.compileTs() + await this.copyExtras() + await this.copyDependencies(true) + }, + 'after:package:createDeploymentArtifacts': async () => { + await this.cleanup() + }, + 'before:deploy:function:packageFunction': async () => { + await this.compileTs() + await this.copyExtras() + await this.copyDependencies(true) + }, + 'after:deploy:function:packageFunction': async () => { + await this.cleanup() + }, + 'before:invoke:local:invoke': async () => { + const emitedFiles = await this.compileTs() + await this.copyExtras() + await this.copyDependencies() + if (this.isWatching) { + emitedFiles.forEach(filename => { + const module = require.resolve(path.resolve(this.originalServicePath, filename)) + delete require.cache[module] + }) + } + }, + 'after:invoke:local:invoke': () => { + if (this.options.watch) { + this.watchFunction() + this.serverless.cli.log('Waiting for changes...') + } + } + } + } + + get functions() { + const { options } = this + const { service } = this.serverless + + if (options.function) { + return { + [options.function]: service.functions[this.options.function] + } + } + + return service.functions + } + + get rootFileNames() { + return typescript.extractFileNames( + this.originalServicePath, + this.serverless.service.provider.name, + this.functions + ) + } + + prepare() { + // exclude serverless-plugin-typescript + for (const fnName in this.functions) { + const fn = this.functions[fnName] + fn.package = fn.package || { + exclude: [], + include: [], + } + + // Add plugin to excluded packages or an empty array if exclude is undefined + fn.package.exclude = _.uniq([...fn.package.exclude || [], 'node_modules/serverless-plugin-typescript']) + } + } + + async watchFunction(): Promise { + if (this.isWatching) { + return + } + + this.serverless.cli.log(`Watch function ${this.options.function}...`) + + this.isWatching = true + watchFiles(this.rootFileNames, this.originalServicePath, () => { + this.serverless.pluginManager.spawn('invoke:local') + }) + } + + async watchAll(): Promise { + if (this.isWatching) { + return + } + + this.serverless.cli.log(`Watching typescript files...`) + + this.isWatching = true + watchFiles(this.rootFileNames, this.originalServicePath, this.compileTs.bind(this)) + } + + async compileTs(): Promise { + this.prepare() + this.serverless.cli.log('Compiling with Typescript...') + + if (!this.originalServicePath) { + // Save original service path and functions + this.originalServicePath = this.serverless.config.servicePath + // Fake service path so that serverless will know what to zip + this.serverless.config.servicePath = path.join(this.originalServicePath, BUILD_FOLDER) + } + + const tsconfig = typescript.getTypescriptConfig( + this.originalServicePath, + this.isWatching ? null : this.serverless.cli + ) + + tsconfig.outDir = BUILD_FOLDER + + const emitedFiles = await typescript.run(this.rootFileNames, tsconfig) + this.serverless.cli.log('Typescript compiled.') + return emitedFiles + } + + /** Link or copy extras such as node_modules or package.include definitions */ + async copyExtras() { + const { service } = this.serverless + + // include any "extras" from the "include" section + if (service.package.include && service.package.include.length > 0) { + const files = await globby(service.package.include) + + for (const filename of files) { + const destFileName = path.resolve(path.join(BUILD_FOLDER, filename)) + const dirname = path.dirname(destFileName) + + if (!fs.existsSync(dirname)) { + fs.mkdirpSync(dirname) + } + + if (!fs.existsSync(destFileName)) { + fs.copySync(path.resolve(filename), path.resolve(path.join(BUILD_FOLDER, filename))) + } + } + } + } + + /** + * Copy the `node_modules` folder and `package.json` files to the output + * directory. + * @param isPackaging Provided if serverless is packaging the service for deployment + */ + async copyDependencies(isPackaging = false) { + const outPkgPath = path.resolve(path.join(BUILD_FOLDER, 'package.json')) + const outModulesPath = path.resolve(path.join(BUILD_FOLDER, 'node_modules')) + + // copy development dependencies during packaging + if (isPackaging) { + if (fs.existsSync(outModulesPath)) { + fs.unlinkSync(outModulesPath) + } + + fs.copySync( + path.resolve('node_modules'), + path.resolve(path.join(BUILD_FOLDER, 'node_modules')) + ) + } else { + if (!fs.existsSync(outModulesPath)) { + await this.linkOrCopy(path.resolve('node_modules'), outModulesPath, 'junction') + } + } + + // copy/link package.json + if (!fs.existsSync(outPkgPath)) { + await this.linkOrCopy(path.resolve('package.json'), outPkgPath, 'file') + } + } + + /** + * Move built code to the serverless folder, taking into account individual + * packaging preferences. + */ + async moveArtifacts(): Promise { + const { service } = this.serverless + + await fs.copy( + path.join(this.originalServicePath, BUILD_FOLDER, SERVERLESS_FOLDER), + path.join(this.originalServicePath, SERVERLESS_FOLDER) + ) + + if (this.options.function) { + const fn = service.functions[this.options.function] + fn.package.artifact = path.join( + this.originalServicePath, + SERVERLESS_FOLDER, + path.basename(fn.package.artifact) + ) + return + } + + if (service.package.individually) { + const functionNames = service.getAllFunctions() + functionNames.forEach(name => { + service.functions[name].package.artifact = path.join( + this.originalServicePath, + SERVERLESS_FOLDER, + path.basename(service.functions[name].package.artifact) + ) + }) + return + } + + service.package.artifact = path.join( + this.originalServicePath, + SERVERLESS_FOLDER, + path.basename(service.package.artifact) + ) + } + + async cleanup(): Promise { + await this.moveArtifacts() + // Restore service path + this.serverless.config.servicePath = this.originalServicePath + // Remove temp build folder + fs.removeSync(path.join(this.originalServicePath, BUILD_FOLDER)) + } + + /** + * Attempt to symlink a given path or directory and copy if it fails with an + * `EPERM` error. + */ + private async linkOrCopy(srcPath: string, dstPath: string, type?: fs.FsSymlinkType): Promise { + return fs.symlink(srcPath, dstPath, type) + .catch(error => { + if (error.code === 'EPERM' && error.errno === -4048) { + return fs.copy(srcPath, dstPath) + } + throw error + }) + } +} From 6cd5e459e2fda97eaf412693daa1ac5c647afb48 Mon Sep 17 00:00:00 2001 From: John Reeves Date: Mon, 3 Feb 2020 14:24:31 -0800 Subject: [PATCH 2/2] Skip functions that do not use node runtime. Allows mixed runtime projects. --- src/Serverless.d.ts | 2 + src/typeScriptPlugin.ts | 18 +++++++- tests/TypeScriptPlugin.test.ts | 58 ++++++++++++++++++++++++ tests/typescript.extractFileName.test.ts | 3 ++ 4 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 tests/TypeScriptPlugin.test.ts diff --git a/src/Serverless.d.ts b/src/Serverless.d.ts index ec7d7049..d2e83e1c 100644 --- a/src/Serverless.d.ts +++ b/src/Serverless.d.ts @@ -11,6 +11,7 @@ declare namespace Serverless { service: { provider: { name: string + runtime: string } functions: { [key: string]: Serverless.Function @@ -30,6 +31,7 @@ declare namespace Serverless { interface Function { handler: string + runtime: string package: Serverless.Package } diff --git a/src/typeScriptPlugin.ts b/src/typeScriptPlugin.ts index 371b80b7..ec347271 100644 --- a/src/typeScriptPlugin.ts +++ b/src/typeScriptPlugin.ts @@ -92,10 +92,24 @@ export class TypeScriptPlugin { return typescript.extractFileNames( this.originalServicePath, this.serverless.service.provider.name, - this.functions + this.nodeFunctions ) } + get nodeFunctions() { + const { service } = this.serverless + const functions = this.functions + + // filter out functions that have a non-node runtime because they can't even typescript + return Object.keys(this.functions) + .map(fn => ({fn, runtime: functions[fn].runtime || service.provider.runtime})) + .filter(fnObj => fnObj.runtime.match(/nodejs/)) + .reduce((prev, cur) => ({ + ...prev, + [cur.fn]: service.functions[cur.fn] + }), {} as {[key: string]: Serverless.Function}) + } + prepare() { // exclude serverless-plugin-typescript for (const fnName in this.functions) { @@ -234,7 +248,7 @@ export class TypeScriptPlugin { } if (service.package.individually) { - const functionNames = service.getAllFunctions() + const functionNames = Object.keys(this.nodeFunctions) functionNames.forEach(name => { service.functions[name].package.artifact = path.join( this.originalServicePath, diff --git a/tests/TypeScriptPlugin.test.ts b/tests/TypeScriptPlugin.test.ts new file mode 100644 index 00000000..7489c028 --- /dev/null +++ b/tests/TypeScriptPlugin.test.ts @@ -0,0 +1,58 @@ +import {TypeScriptPlugin} from '../src/typeScriptPlugin' + +describe('TypeScriptPlugin', () => { + it('rootFileNames includes only node runtimes', () => { + const slsInstance: Serverless.Instance = { + cli: { + log: jest.fn() + }, + config: { + servicePath: 'servicePath' + }, + service: { + provider: { + name: 'aws', + runtime: 'nodejs99' + }, + package: { + individually: true, + include: [], + exclude: [] + }, + functions: { + func1: { + handler: 'java-fn', + runtime: 'java8', + package: { + include: [], + exclude: [] + } + }, + func2: { + handler: 'node-fn', + runtime: 'nodejs99', + package: { + include: [], + exclude: [] + } + } + }, + + getAllFunctions: jest.fn() + }, + pluginManager: { + spawn: jest.fn() + } + } + + const plugin = new TypeScriptPlugin(slsInstance, {}) + + expect( + Object.keys(plugin.nodeFunctions) + ).toEqual( + [ + 'func2' + ], + ) + }) +}) diff --git a/tests/typescript.extractFileName.test.ts b/tests/typescript.extractFileName.test.ts index 82fac1d8..ad3446fc 100644 --- a/tests/typescript.extractFileName.test.ts +++ b/tests/typescript.extractFileName.test.ts @@ -4,6 +4,7 @@ import * as path from 'path' const functions: { [key: string]: Serverless.Function } = { hello: { handler: 'tests/assets/hello.handler', + runtime: 'nodejs10.1', package: { include: [], exclude: [] @@ -11,6 +12,7 @@ const functions: { [key: string]: Serverless.Function } = { }, world: { handler: 'tests/assets/world.handler', + runtime: 'nodejs10.1', package: { include: [], exclude: [] @@ -18,6 +20,7 @@ const functions: { [key: string]: Serverless.Function } = { }, js: { handler: 'tests/assets/jsfile.create', + runtime: 'nodejs10.1', package: { include: [], exclude: []