Skip to content

Commit

Permalink
Merge pull request #159 from prisma/refactor/type-definitions-and-cle…
Browse files Browse the repository at this point in the history
…anup

Dev deps fix + cleanup + refactor
  • Loading branch information
JackCuthbert authored Jul 13, 2019
2 parents 02b1efa + 9286d50 commit c99fe30
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 121 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ Serverless plugin for zero-config Typescript support
## Install

```sh
yarn add --dev serverless-plugin-typescript
yarn add --dev serverless-plugin-typescript typescript
# or
npm install -D serverless-plugin-typescript typescript
```

Add the following plugin to your `serverless.yml`:
Expand All @@ -38,6 +40,7 @@ The default `tsconfig.json` file used by the plugin looks like this:
"preserveConstEnums": true,
"strictNullChecks": true,
"sourceMap": true,
"allowJs": true,
"target": "es5",
"outDir": ".build",
"moduleResolution": "node",
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@
"@types/fs-extra": "5.0.5",
"@types/jest": "24.0.12",
"@types/lodash": "4.14.123",
"@types/node": "12.6.2",
"jest": "24.5.0",
"mock-fs": "4.9.0",
"rimraf": "^2.6.3",
"rimraf": "2.6.3",
"ts-jest": "24.0.2",
"tslint": "5.14.0",
"typescript": "^3.4.1"
Expand Down
46 changes: 46 additions & 0 deletions src/Serverless.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
declare namespace Serverless {
interface Instance {
cli: {
log(str: string): void
}

config: {
servicePath: string
}

service: {
provider: {
name: string
}
functions: {
[key: string]: Serverless.Function
}
package: Serverless.Package
getAllFunctions(): string[]
}

pluginManager: PluginManager
}

interface Options {
function?: string
watch?: boolean
extraServicePath?: string
}

interface Function {
handler: string
package: Serverless.Package
}

interface Package {
include: string[]
exclude: string[]
artifact?: string
individually?: boolean
}

interface PluginManager {
spawn(command: string): Promise<void>
}
}
174 changes: 109 additions & 65 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,64 @@
import * as path from 'path'
import * as fs from 'fs-extra'

import * as _ from 'lodash'
import * as globby from 'globby'

import { ServerlessOptions, ServerlessInstance, ServerlessFunction } from './types'
import * as typescript from './typescript'

import { watchFiles } from './watchFiles'

// Folders
const serverlessFolder = '.serverless'
const buildFolder = '.build'
const SERVERLESS_FOLDER = '.serverless'
const BUILD_FOLDER = '.build'

export class TypeScriptPlugin {

private originalServicePath: string
private isWatching: boolean

serverless: ServerlessInstance
options: ServerlessOptions
commands: { [key: string]: any }
serverless: Serverless.Instance
options: Serverless.Options
hooks: { [key: string]: Function }

constructor(serverless: ServerlessInstance, options: ServerlessOptions) {
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': this.compileTs.bind(this),
'after:package:createDeploymentArtifacts': this.cleanup.bind(this),
'before:deploy:function:packageFunction': this.compileTs.bind(this),
'after:deploy:function:packageFunction': this.cleanup.bind(this),
'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))
Expand All @@ -55,31 +69,42 @@ export class TypeScriptPlugin {
'after:invoke:local:invoke': () => {
if (this.options.watch) {
this.watchFunction()
this.serverless.cli.log('Waiting for changes ...')
this.serverless.cli.log('Waiting for changes...')
}
}
}
}

get functions() {
return this.options.function
? { [this.options.function] : this.serverless.service.functions[this.options.function] }
: this.serverless.service.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)
return typescript.extractFileNames(
this.originalServicePath,
this.serverless.service.provider.name,
this.functions
)
}

prepare() {
// exclude serverless-plugin-typescript
const functions = this.functions
for (const fnName in functions) {
const fn = functions[fnName]
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'])
}
Expand All @@ -106,9 +131,7 @@ export class TypeScriptPlugin {
this.serverless.cli.log(`Watching typescript files...`)

this.isWatching = true
watchFiles(this.rootFileNames, this.originalServicePath, () => {
this.compileTs()
})
watchFiles(this.rootFileNames, this.originalServicePath, this.compileTs)
}

async compileTs(): Promise<string[]> {
Expand All @@ -119,88 +142,113 @@ export class TypeScriptPlugin {
// 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, buildFolder)
this.serverless.config.servicePath = path.join(this.originalServicePath, BUILD_FOLDER)
}

const tsconfig = typescript.getTypescriptConfig(
this.originalServicePath,
this.isWatching ? null : this.serverless.cli
)

tsconfig.outDir = buildFolder
tsconfig.outDir = BUILD_FOLDER

const emitedFiles = await typescript.run(this.rootFileNames, tsconfig)
await this.copyExtras()
this.serverless.cli.log('Typescript compiled.')
return emitedFiles
}

/** Link or copy extras such as node_modules or package.include definitions */
async copyExtras() {
const outPkgPath = path.resolve(path.join(buildFolder, 'package.json'))
const outModulesPath = path.resolve(path.join(buildFolder, 'node_modules'))

// Link or copy node_modules and package.json to .build so Serverless can
// exlcude devDeps during packaging
if (!fs.existsSync(outModulesPath)) {
await this.linkOrCopy(path.resolve('node_modules'), outModulesPath, 'junction')
}

if (!fs.existsSync(outPkgPath)) {
await this.linkOrCopy(path.resolve('package.json'), outPkgPath, 'file')
}
const { service } = this.serverless

// include any "extras" from the "include" section
if (this.serverless.service.package.include && this.serverless.service.package.include.length > 0) {
const files = await globby(this.serverless.service.package.include)
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(buildFolder, filename))
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(buildFolder, filename)))
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<void> {
const { service } = this.serverless

await fs.copy(
path.join(this.originalServicePath, buildFolder, serverlessFolder),
path.join(this.originalServicePath, serverlessFolder)
path.join(this.originalServicePath, BUILD_FOLDER, SERVERLESS_FOLDER),
path.join(this.originalServicePath, SERVERLESS_FOLDER)
)

if (this.options.function) {
const fn = this.serverless.service.functions[this.options.function]
const basename = path.basename(fn.package.artifact)
fn.package.artifact = path.join(
const fn = service.functions[this.options.function]
fn.package.artifact = path.join(
this.originalServicePath,
serverlessFolder,
SERVERLESS_FOLDER,
path.basename(fn.package.artifact)
)
return
}

if (this.serverless.service.package.individually) {
const functionNames = this.serverless.service.getAllFunctions()
if (service.package.individually) {
const functionNames = service.getAllFunctions()
functionNames.forEach(name => {
this.serverless.service.functions[name].package.artifact = path.join(
service.functions[name].package.artifact = path.join(
this.originalServicePath,
serverlessFolder,
path.basename(this.serverless.service.functions[name].package.artifact)
SERVERLESS_FOLDER,
path.basename(service.functions[name].package.artifact)
)
})
return
}

this.serverless.service.package.artifact = path.join(
service.package.artifact = path.join(
this.originalServicePath,
serverlessFolder,
path.basename(this.serverless.service.package.artifact)
SERVERLESS_FOLDER,
path.basename(service.package.artifact)
)
}

Expand All @@ -209,18 +257,14 @@ export class TypeScriptPlugin {
// Restore service path
this.serverless.config.servicePath = this.originalServicePath
// Remove temp build folder
fs.removeSync(path.join(this.originalServicePath, buildFolder))
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?: 'dir' | 'junction' | 'file'
): Promise<void> {
private async linkOrCopy(srcPath: string, dstPath: string, type?: fs.FsSymlinkType): Promise<void> {
return fs.symlink(srcPath, dstPath, type)
.catch(error => {
if (error.code === 'EPERM' && error.errno === -4048) {
Expand Down
Loading

0 comments on commit c99fe30

Please sign in to comment.