Skip to content

Commit c99fe30

Browse files
authored
Merge pull request #159 from prisma/refactor/type-definitions-and-cleanup
Dev deps fix + cleanup + refactor
2 parents 02b1efa + 9286d50 commit c99fe30

10 files changed

+173
-121
lines changed

README.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ Serverless plugin for zero-config Typescript support
1414
## Install
1515

1616
```sh
17-
yarn add --dev serverless-plugin-typescript
17+
yarn add --dev serverless-plugin-typescript typescript
18+
# or
19+
npm install -D serverless-plugin-typescript typescript
1820
```
1921

2022
Add the following plugin to your `serverless.yml`:
@@ -38,6 +40,7 @@ The default `tsconfig.json` file used by the plugin looks like this:
3840
"preserveConstEnums": true,
3941
"strictNullChecks": true,
4042
"sourceMap": true,
43+
"allowJs": true,
4144
"target": "es5",
4245
"outDir": ".build",
4346
"moduleResolution": "node",

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@
3030
"@types/fs-extra": "5.0.5",
3131
"@types/jest": "24.0.12",
3232
"@types/lodash": "4.14.123",
33+
"@types/node": "12.6.2",
3334
"jest": "24.5.0",
3435
"mock-fs": "4.9.0",
35-
"rimraf": "^2.6.3",
36+
"rimraf": "2.6.3",
3637
"ts-jest": "24.0.2",
3738
"tslint": "5.14.0",
3839
"typescript": "^3.4.1"

src/Serverless.d.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
declare namespace Serverless {
2+
interface Instance {
3+
cli: {
4+
log(str: string): void
5+
}
6+
7+
config: {
8+
servicePath: string
9+
}
10+
11+
service: {
12+
provider: {
13+
name: string
14+
}
15+
functions: {
16+
[key: string]: Serverless.Function
17+
}
18+
package: Serverless.Package
19+
getAllFunctions(): string[]
20+
}
21+
22+
pluginManager: PluginManager
23+
}
24+
25+
interface Options {
26+
function?: string
27+
watch?: boolean
28+
extraServicePath?: string
29+
}
30+
31+
interface Function {
32+
handler: string
33+
package: Serverless.Package
34+
}
35+
36+
interface Package {
37+
include: string[]
38+
exclude: string[]
39+
artifact?: string
40+
individually?: boolean
41+
}
42+
43+
interface PluginManager {
44+
spawn(command: string): Promise<void>
45+
}
46+
}

src/index.ts

+109-65
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,64 @@
11
import * as path from 'path'
22
import * as fs from 'fs-extra'
3-
43
import * as _ from 'lodash'
54
import * as globby from 'globby'
65

7-
import { ServerlessOptions, ServerlessInstance, ServerlessFunction } from './types'
86
import * as typescript from './typescript'
9-
107
import { watchFiles } from './watchFiles'
118

12-
// Folders
13-
const serverlessFolder = '.serverless'
14-
const buildFolder = '.build'
9+
const SERVERLESS_FOLDER = '.serverless'
10+
const BUILD_FOLDER = '.build'
1511

1612
export class TypeScriptPlugin {
17-
1813
private originalServicePath: string
1914
private isWatching: boolean
2015

21-
serverless: ServerlessInstance
22-
options: ServerlessOptions
23-
commands: { [key: string]: any }
16+
serverless: Serverless.Instance
17+
options: Serverless.Options
2418
hooks: { [key: string]: Function }
2519

26-
constructor(serverless: ServerlessInstance, options: ServerlessOptions) {
20+
constructor(serverless: Serverless.Instance, options: Serverless.Options) {
2721
this.serverless = serverless
2822
this.options = options
2923

3024
this.hooks = {
3125
'before:run:run': async () => {
3226
await this.compileTs()
27+
await this.copyExtras()
28+
await this.copyDependencies()
3329
},
3430
'before:offline:start': async () => {
3531
await this.compileTs()
32+
await this.copyExtras()
33+
await this.copyDependencies()
3634
this.watchAll()
3735
},
3836
'before:offline:start:init': async () => {
3937
await this.compileTs()
38+
await this.copyExtras()
39+
await this.copyDependencies()
4040
this.watchAll()
4141
},
42-
'before:package:createDeploymentArtifacts': this.compileTs.bind(this),
43-
'after:package:createDeploymentArtifacts': this.cleanup.bind(this),
44-
'before:deploy:function:packageFunction': this.compileTs.bind(this),
45-
'after:deploy:function:packageFunction': this.cleanup.bind(this),
42+
'before:package:createDeploymentArtifacts': async () => {
43+
await this.compileTs()
44+
await this.copyExtras()
45+
await this.copyDependencies(true)
46+
},
47+
'after:package:createDeploymentArtifacts': async () => {
48+
await this.cleanup()
49+
},
50+
'before:deploy:function:packageFunction': async () => {
51+
await this.compileTs()
52+
await this.copyExtras()
53+
await this.copyDependencies(true)
54+
},
55+
'after:deploy:function:packageFunction': async () => {
56+
await this.cleanup()
57+
},
4658
'before:invoke:local:invoke': async () => {
4759
const emitedFiles = await this.compileTs()
60+
await this.copyExtras()
61+
await this.copyDependencies()
4862
if (this.isWatching) {
4963
emitedFiles.forEach(filename => {
5064
const module = require.resolve(path.resolve(this.originalServicePath, filename))
@@ -55,31 +69,42 @@ export class TypeScriptPlugin {
5569
'after:invoke:local:invoke': () => {
5670
if (this.options.watch) {
5771
this.watchFunction()
58-
this.serverless.cli.log('Waiting for changes ...')
72+
this.serverless.cli.log('Waiting for changes...')
5973
}
6074
}
6175
}
6276
}
6377

6478
get functions() {
65-
return this.options.function
66-
? { [this.options.function] : this.serverless.service.functions[this.options.function] }
67-
: this.serverless.service.functions
79+
const { options } = this
80+
const { service } = this.serverless
81+
82+
if (options.function) {
83+
return {
84+
[options.function]: service.functions[this.options.function]
85+
}
86+
}
87+
88+
return service.functions
6889
}
6990

7091
get rootFileNames() {
71-
return typescript.extractFileNames(this.originalServicePath, this.serverless.service.provider.name, this.functions)
92+
return typescript.extractFileNames(
93+
this.originalServicePath,
94+
this.serverless.service.provider.name,
95+
this.functions
96+
)
7297
}
7398

7499
prepare() {
75100
// exclude serverless-plugin-typescript
76-
const functions = this.functions
77-
for (const fnName in functions) {
78-
const fn = functions[fnName]
101+
for (const fnName in this.functions) {
102+
const fn = this.functions[fnName]
79103
fn.package = fn.package || {
80104
exclude: [],
81105
include: [],
82106
}
107+
83108
// Add plugin to excluded packages or an empty array if exclude is undefined
84109
fn.package.exclude = _.uniq([...fn.package.exclude || [], 'node_modules/serverless-plugin-typescript'])
85110
}
@@ -106,9 +131,7 @@ export class TypeScriptPlugin {
106131
this.serverless.cli.log(`Watching typescript files...`)
107132

108133
this.isWatching = true
109-
watchFiles(this.rootFileNames, this.originalServicePath, () => {
110-
this.compileTs()
111-
})
134+
watchFiles(this.rootFileNames, this.originalServicePath, this.compileTs)
112135
}
113136

114137
async compileTs(): Promise<string[]> {
@@ -119,88 +142,113 @@ export class TypeScriptPlugin {
119142
// Save original service path and functions
120143
this.originalServicePath = this.serverless.config.servicePath
121144
// Fake service path so that serverless will know what to zip
122-
this.serverless.config.servicePath = path.join(this.originalServicePath, buildFolder)
145+
this.serverless.config.servicePath = path.join(this.originalServicePath, BUILD_FOLDER)
123146
}
124147

125148
const tsconfig = typescript.getTypescriptConfig(
126149
this.originalServicePath,
127150
this.isWatching ? null : this.serverless.cli
128151
)
129152

130-
tsconfig.outDir = buildFolder
153+
tsconfig.outDir = BUILD_FOLDER
131154

132155
const emitedFiles = await typescript.run(this.rootFileNames, tsconfig)
133-
await this.copyExtras()
134156
this.serverless.cli.log('Typescript compiled.')
135157
return emitedFiles
136158
}
137159

160+
/** Link or copy extras such as node_modules or package.include definitions */
138161
async copyExtras() {
139-
const outPkgPath = path.resolve(path.join(buildFolder, 'package.json'))
140-
const outModulesPath = path.resolve(path.join(buildFolder, 'node_modules'))
141-
142-
// Link or copy node_modules and package.json to .build so Serverless can
143-
// exlcude devDeps during packaging
144-
if (!fs.existsSync(outModulesPath)) {
145-
await this.linkOrCopy(path.resolve('node_modules'), outModulesPath, 'junction')
146-
}
147-
148-
if (!fs.existsSync(outPkgPath)) {
149-
await this.linkOrCopy(path.resolve('package.json'), outPkgPath, 'file')
150-
}
162+
const { service } = this.serverless
151163

152164
// include any "extras" from the "include" section
153-
if (this.serverless.service.package.include && this.serverless.service.package.include.length > 0) {
154-
const files = await globby(this.serverless.service.package.include)
165+
if (service.package.include && service.package.include.length > 0) {
166+
const files = await globby(service.package.include)
155167

156168
for (const filename of files) {
157-
const destFileName = path.resolve(path.join(buildFolder, filename))
169+
const destFileName = path.resolve(path.join(BUILD_FOLDER, filename))
158170
const dirname = path.dirname(destFileName)
159171

160172
if (!fs.existsSync(dirname)) {
161173
fs.mkdirpSync(dirname)
162174
}
163175

164176
if (!fs.existsSync(destFileName)) {
165-
fs.copySync(path.resolve(filename), path.resolve(path.join(buildFolder, filename)))
177+
fs.copySync(path.resolve(filename), path.resolve(path.join(BUILD_FOLDER, filename)))
166178
}
167179
}
168180
}
169181
}
170182

183+
/**
184+
* Copy the `node_modules` folder and `package.json` files to the output
185+
* directory.
186+
* @param isPackaging Provided if serverless is packaging the service for deployment
187+
*/
188+
async copyDependencies(isPackaging = false) {
189+
const outPkgPath = path.resolve(path.join(BUILD_FOLDER, 'package.json'))
190+
const outModulesPath = path.resolve(path.join(BUILD_FOLDER, 'node_modules'))
191+
192+
// copy development dependencies during packaging
193+
if (isPackaging) {
194+
if (fs.existsSync(outModulesPath)) {
195+
fs.unlinkSync(outModulesPath)
196+
}
197+
198+
fs.copySync(
199+
path.resolve('node_modules'),
200+
path.resolve(path.join(BUILD_FOLDER, 'node_modules'))
201+
)
202+
} else {
203+
if (!fs.existsSync(outModulesPath)) {
204+
await this.linkOrCopy(path.resolve('node_modules'), outModulesPath, 'junction')
205+
}
206+
}
207+
208+
// copy/link package.json
209+
if (!fs.existsSync(outPkgPath)) {
210+
await this.linkOrCopy(path.resolve('package.json'), outPkgPath, 'file')
211+
}
212+
}
213+
214+
/**
215+
* Move built code to the serverless folder, taking into account individual
216+
* packaging preferences.
217+
*/
171218
async moveArtifacts(): Promise<void> {
219+
const { service } = this.serverless
220+
172221
await fs.copy(
173-
path.join(this.originalServicePath, buildFolder, serverlessFolder),
174-
path.join(this.originalServicePath, serverlessFolder)
222+
path.join(this.originalServicePath, BUILD_FOLDER, SERVERLESS_FOLDER),
223+
path.join(this.originalServicePath, SERVERLESS_FOLDER)
175224
)
176225

177226
if (this.options.function) {
178-
const fn = this.serverless.service.functions[this.options.function]
179-
const basename = path.basename(fn.package.artifact)
180-
fn.package.artifact = path.join(
227+
const fn = service.functions[this.options.function]
228+
fn.package.artifact = path.join(
181229
this.originalServicePath,
182-
serverlessFolder,
230+
SERVERLESS_FOLDER,
183231
path.basename(fn.package.artifact)
184232
)
185233
return
186234
}
187235

188-
if (this.serverless.service.package.individually) {
189-
const functionNames = this.serverless.service.getAllFunctions()
236+
if (service.package.individually) {
237+
const functionNames = service.getAllFunctions()
190238
functionNames.forEach(name => {
191-
this.serverless.service.functions[name].package.artifact = path.join(
239+
service.functions[name].package.artifact = path.join(
192240
this.originalServicePath,
193-
serverlessFolder,
194-
path.basename(this.serverless.service.functions[name].package.artifact)
241+
SERVERLESS_FOLDER,
242+
path.basename(service.functions[name].package.artifact)
195243
)
196244
})
197245
return
198246
}
199247

200-
this.serverless.service.package.artifact = path.join(
248+
service.package.artifact = path.join(
201249
this.originalServicePath,
202-
serverlessFolder,
203-
path.basename(this.serverless.service.package.artifact)
250+
SERVERLESS_FOLDER,
251+
path.basename(service.package.artifact)
204252
)
205253
}
206254

@@ -209,18 +257,14 @@ export class TypeScriptPlugin {
209257
// Restore service path
210258
this.serverless.config.servicePath = this.originalServicePath
211259
// Remove temp build folder
212-
fs.removeSync(path.join(this.originalServicePath, buildFolder))
260+
fs.removeSync(path.join(this.originalServicePath, BUILD_FOLDER))
213261
}
214262

215263
/**
216264
* Attempt to symlink a given path or directory and copy if it fails with an
217265
* `EPERM` error.
218266
*/
219-
private async linkOrCopy(
220-
srcPath: string,
221-
dstPath: string,
222-
type?: 'dir' | 'junction' | 'file'
223-
): Promise<void> {
267+
private async linkOrCopy(srcPath: string, dstPath: string, type?: fs.FsSymlinkType): Promise<void> {
224268
return fs.symlink(srcPath, dstPath, type)
225269
.catch(error => {
226270
if (error.code === 'EPERM' && error.errno === -4048) {

0 commit comments

Comments
 (0)