Skip to content

Commit cc11034

Browse files
committed
Major refactoring + support watch mode
1 parent 1608fb6 commit cc11034

File tree

4 files changed

+178
-60
lines changed

4 files changed

+178
-60
lines changed

src/index.ts

Lines changed: 101 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import * as path from 'path'
22
import * as fs from 'fs-p'
3+
34
import * as _ from 'lodash'
45
import * as globby from 'globby'
56

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

10+
import { watchFiles } from './watchFiles'
11+
912
// Folders
1013
const serverlessFolder = '.serverless'
1114
const buildFolder = '.build'
1215

13-
class ServerlessPlugin {
16+
export class ServerlessPlugin {
1417

1518
private originalServicePath: string
16-
private originalFunctions: { [key: string]: ServerlessFunction } | {}
19+
private isWatching: boolean
1720

1821
serverless: ServerlessInstance
1922
options: ServerlessOptions
@@ -25,67 +28,110 @@ class ServerlessPlugin {
2528
this.options = options
2629

2730
this.hooks = {
28-
'before:offline:start:init': this.beforeCreateDeploymentArtifacts.bind(this),
29-
'before:package:createDeploymentArtifacts': this.beforeCreateDeploymentArtifacts.bind(this, 'service'),
30-
'after:package:createDeploymentArtifacts': this.afterCreateDeploymentArtifacts.bind(this, 'service'),
31-
'before:deploy:function:packageFunction': this.beforeCreateDeploymentArtifacts.bind(this, 'function'),
32-
'after:deploy:function:packageFunction': this.afterCreateDeploymentArtifacts.bind(this, 'function'),
33-
'before:invoke:local:invoke': this.beforeCreateDeploymentArtifacts.bind(this),
34-
'after:invoke:local:invoke': this.cleanup.bind(this),
35-
}
36-
this.commands = {
37-
ts: {
38-
commands: {
39-
invoke: {
40-
usage: 'Run a function locally from the tsc output bundle',
41-
lifecycleEvents: [
42-
'invoke',
43-
],
44-
options: {
45-
function: {
46-
usage: 'Name of the function',
47-
shortcut: 'f',
48-
required: true,
49-
},
50-
path: {
51-
usage: 'Path to JSON file holding input data',
52-
shortcut: 'p',
53-
},
54-
},
55-
},
56-
},
31+
'before:offline:start': async () => {
32+
await this.compileTs()
33+
this.watchAll()
5734
},
35+
'before:offline:start:init': async () => {
36+
await this.compileTs()
37+
this.watchAll()
38+
},
39+
'before:package:createDeploymentArtifacts': this.compileTs.bind(this),
40+
'after:package:createDeploymentArtifacts': this.cleanup.bind(this),
41+
'before:deploy:function:packageFunction': this.compileTs.bind(this),
42+
'after:deploy:function:packageFunction': this.cleanup.bind(this),
43+
'before:invoke:local:invoke': async () => {
44+
const emitedFiles = await this.compileTs()
45+
if (this.isWatching) {
46+
emitedFiles.forEach(filename => {
47+
const module = require.resolve(path.resolve(this.originalServicePath, filename))
48+
delete require.cache[module]
49+
})
50+
}
51+
},
52+
'after:invoke:local:invoke': () => {
53+
if (this.options.watch) {
54+
this.watchFunction()
55+
this.serverless.cli.log('Waiting for changes ...')
56+
}
57+
}
5858
}
5959
}
6060

61-
async beforeCreateDeploymentArtifacts(type: string): Promise<void> {
62-
this.serverless.cli.log('Compiling with Typescript...')
63-
64-
// Save original service path and functions
65-
this.originalServicePath = this.serverless.config.servicePath
66-
this.originalFunctions = type === 'function'
67-
? _.pick(this.serverless.service.functions, [this.options.function])
61+
get functions() {
62+
return this.options.function
63+
? { [this.options.function] : this.serverless.service.functions[this.options.function] }
6864
: this.serverless.service.functions
65+
}
6966

70-
// Fake service path so that serverless will know what to zip
71-
this.serverless.config.servicePath = path.join(this.originalServicePath, buildFolder)
72-
73-
const tsFileNames = typescript.extractFileNames(this.originalFunctions)
74-
const tsconfig = typescript.getTypescriptConfig(this.originalServicePath)
67+
get rootFileNames() {
68+
return typescript.extractFileNames(this.functions)
69+
}
7570

76-
for (const fnName in this.originalFunctions) {
77-
const fn = this.originalFunctions[fnName]
71+
prepare() {
72+
// exclude serverless-plugin-typescript
73+
const functions = this.functions
74+
for (const fnName in functions) {
75+
const fn = functions[fnName]
7876
fn.package = fn.package || {
7977
exclude: [],
8078
include: [],
8179
}
8280
fn.package.exclude = _.uniq([...fn.package.exclude, 'node_modules/serverless-plugin-typescript'])
8381
}
82+
}
83+
84+
async watchFunction(): Promise<void> {
85+
if (this.isWatching) {
86+
return
87+
}
88+
89+
this.serverless.cli.log(`Watch function ${this.options.function}...`)
90+
91+
this.isWatching = true
92+
watchFiles(this.rootFileNames, this.originalServicePath, () => {
93+
this.serverless.pluginManager.spawn('invoke:local')
94+
})
95+
}
96+
97+
async watchAll(): Promise<void> {
98+
if (this.isWatching) {
99+
return
100+
}
101+
102+
this.serverless.cli.log(`Watching typescript files...`)
103+
104+
this.isWatching = true
105+
watchFiles(this.rootFileNames, this.originalServicePath, () => {
106+
this.compileTs()
107+
})
108+
}
109+
110+
async compileTs(): Promise<string[]> {
111+
this.prepare()
112+
this.serverless.cli.log('Compiling with Typescript...')
113+
114+
if (!this.originalServicePath) {
115+
// Save original service path and functions
116+
this.originalServicePath = this.serverless.config.servicePath
117+
// Fake service path so that serverless will know what to zip
118+
this.serverless.config.servicePath = path.join(this.originalServicePath, buildFolder)
119+
}
120+
121+
const tsFileNames = typescript.extractFileNames(this.functions)
122+
const tsconfig = typescript.getTypescriptConfig(
123+
this.originalServicePath,
124+
this.isWatching ? null : this.serverless.cli
125+
)
84126

85127
tsconfig.outDir = buildFolder
86128

87-
await typescript.run(tsFileNames, tsconfig)
129+
const emitedFiles = await typescript.run(tsFileNames, tsconfig)
130+
await this.copyExtras()
131+
return emitedFiles
132+
}
88133

134+
async copyExtras() {
89135
// include node_modules into build
90136
if (!fs.existsSync(path.resolve(path.join(buildFolder, 'node_modules')))) {
91137
fs.symlinkSync(path.resolve('node_modules'), path.resolve(path.join(buildFolder, 'node_modules')))
@@ -115,23 +161,21 @@ class ServerlessPlugin {
115161
}
116162
}
117163

118-
async afterCreateDeploymentArtifacts(type: string): Promise<void> {
119-
// Copy .build to .serverless
164+
async cleanup(): Promise<void> {
120165
await fs.copy(
121166
path.join(this.originalServicePath, buildFolder, serverlessFolder),
122167
path.join(this.originalServicePath, serverlessFolder)
123168
)
124169

125-
const basename = type === 'function'
126-
? path.basename(this.originalFunctions[this.options.function].artifact)
127-
: path.basename(this.serverless.service.package.artifact)
128-
this.serverless.service.package.artifact = path.join(this.originalServicePath, serverlessFolder, basename)
129-
130-
// Cleanup after everything is copied
131-
await this.cleanup()
132-
}
170+
if (this.options.function) {
171+
const fn = this.serverless.service.functions[this.options.function]
172+
const basename = path.basename(fn.package.artifact)
173+
fn.package.artifact = path.join(this.originalServicePath, serverlessFolder, basename)
174+
} else {
175+
const basename = path.basename(this.serverless.service.package.artifact)
176+
this.serverless.service.package.artifact = path.join(this.originalServicePath, serverlessFolder, basename)
177+
}
133178

134-
async cleanup(): Promise<void> {
135179
// Restore service path
136180
this.serverless.config.servicePath = this.originalServicePath
137181
// Remove temp build folder

src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ export interface ServerlessInstance {
1010
package: ServerlessPackage
1111
getFunction: (name: string) => any
1212
}
13+
pluginManager: PluginManager
1314
}
1415

1516
export interface ServerlessOptions {
1617
function?: string
18+
watch?: boolean
1719
extraServicePath?: string
1820
}
1921

@@ -26,4 +28,9 @@ export interface ServerlessPackage {
2628
include: string[]
2729
exclude: string[]
2830
artifact?: string
31+
individually?: boolean
32+
}
33+
34+
export interface PluginManager {
35+
spawn(command: string): Promise<void>
2936
}

src/typescript.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export function extractFileNames(functions: { [key: string]: ServerlessFunction
3030
}
3131

3232
export async function run(fileNames: string[], options: ts.CompilerOptions): Promise<string[]> {
33+
options.listEmittedFiles = true
3334
const program = ts.createProgram(fileNames, options)
3435

3536
const emitResult = program.emit()
@@ -49,10 +50,29 @@ export async function run(fileNames: string[], options: ts.CompilerOptions): Pro
4950
throw new Error('Typescript compilation failed')
5051
}
5152

52-
return fileNames.map(f => f.replace(/\.ts$/, '.js'))
53+
return emitResult.emittedFiles.filter(filename => filename.endsWith('.js'))
5354
}
5455

55-
export function getTypescriptConfig(cwd: string): ts.CompilerOptions {
56+
/*
57+
* based on rootFileNames returns list of all related (e.g. imported) source files
58+
*/
59+
export function getSourceFiles(
60+
rootFileNames: string[],
61+
options: ts.CompilerOptions
62+
): string[] {
63+
const program = ts.createProgram(rootFileNames, options)
64+
const programmFiles = program.getSourceFiles()
65+
.map(file => file.fileName)
66+
.filter(file => {
67+
return file.split(path.sep).indexOf('node_modules') < 0
68+
})
69+
return programmFiles
70+
}
71+
72+
export function getTypescriptConfig(
73+
cwd: string,
74+
logger?: { log: (str: string) => void }
75+
): ts.CompilerOptions {
5676
const configFilePath = path.join(cwd, 'tsconfig.json')
5777

5878
if (fs.existsSync(configFilePath)) {
@@ -68,7 +88,12 @@ export function getTypescriptConfig(cwd: string): ts.CompilerOptions {
6888
throw new Error(JSON.stringify(configParseResult.errors))
6989
}
7090

71-
console.log(`Using local tsconfig.json`)
91+
if (logger) {
92+
logger.log(`Using local tsconfig.json`)
93+
}
94+
95+
// disallow overrriding rootDir
96+
configParseResult.options.rootDir = './'
7297

7398
return configParseResult.options
7499
}

src/watchFiles.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as typescript from './typescript'
2+
import * as ts from 'typescript'
3+
import { watchFile, unwatchFile, Stats} from 'fs'
4+
import { ServerlessOptions, ServerlessInstance, ServerlessFunction } from './types'
5+
6+
export function watchFiles(
7+
rootFileNames: string[],
8+
originalServicePath: string,
9+
cb: () => void
10+
) {
11+
const tsConfig = typescript.getTypescriptConfig(originalServicePath)
12+
let watchFiles = typescript.getSourceFiles(rootFileNames, tsConfig)
13+
14+
watchFiles.forEach(fileName => {
15+
watchFile(fileName, { persistent: true, interval: 250 }, watchCallback)
16+
})
17+
18+
function watchCallback(curr: Stats, prev: Stats) {
19+
// Check timestamp
20+
if (+curr.mtime <= +prev.mtime) {
21+
return
22+
}
23+
24+
cb()
25+
26+
// use can reference not watched yet file or remove reference to already watched
27+
const newWatchFiles = typescript.getSourceFiles(rootFileNames, tsConfig)
28+
watchFiles.forEach(fileName => {
29+
if (newWatchFiles.indexOf(fileName) < 0) {
30+
unwatchFile(fileName, watchCallback)
31+
}
32+
})
33+
34+
newWatchFiles.forEach(fileName => {
35+
if (watchFiles.indexOf(fileName) < 0) {
36+
watchFile(fileName, { persistent: true, interval: 250 }, watchCallback)
37+
}
38+
})
39+
40+
watchFiles = newWatchFiles
41+
}
42+
}

0 commit comments

Comments
 (0)