@injectable/webpack-service
Version:
service for compiling strings to js
236 lines (218 loc) • 9.77 kB
text/typescript
import { InjectableContainer, Dependencies, Injectable, ResourceStore } from "@injectable/core"
import { FileHostingService, Encoding, OnRequest } from "@injectable/file-hosting-service"
import { LogService, LogLevel } from "@injectable/log-service"
import webpack, { Configuration } from "webpack"
import MemoryFS from "memory-fs"
import { Union } from "unionfs"
import fs from "fs"
import { resolve } from "path";
export class WebpackService extends Injectable<[ "WebpackService" ], Dependencies<[ FileHostingService, LogService<any, any> ]>> {
private resourceStore = new ResourceStore<{
"compilationRequests": Map<number, Array<CompileRequest>>,
"comilationFileIds": Map<number, Array<number>>
}>()
private counter: number = 0
prepare(): void {
this.start()
}
/**
* registers an id for a compilation
*/
public registerCompilationId(): number {
let id = this.counter
++this.counter
return id
}
/**
* Sets a compilation to be compiled and then hosted.
* @param injectableName creator injectable
* @param compilationId id of the compilation
* @param files files to compile
* @param configuration webpack configuration
* @param baseRoute base route to host the files on
* @param encodings all possible encodings
* @param onRequest callback to verify the file request
*/
public setCompilation(
injectableName: string,
compilationId: number,
files: { [filename: string]: string },
configuration: Configuration,
baseRoute: string,
encodings: Array<Encoding>,
onRequest?: OnRequest
): void {
this.compileFiles(injectableName, compilationId, files, configuration)
.then(files => {
this.dependencies.LogService.log(this.typeNames[0], LogLevel.INFO, `Compiled files for "${baseRoute}".`)
let compilationFileIds = this.resourceStore.getResource(injectableName, "comilationFileIds")
if(compilationFileIds != null) {
let fileIds = compilationFileIds.get(compilationId)
if(fileIds) {
fileIds.forEach(fileId =>
this.dependencies.FileHostingService.deleteFile(this.typeNames[0], fileId)
)
}
} else {
compilationFileIds = new Map()
this.resourceStore.setResource(injectableName, "comilationFileIds", compilationFileIds)
}
compilationFileIds.set(
compilationId,
Object.entries(files)
.map(([fileName, buffer]) => {
let id = this.dependencies.FileHostingService.registerFileId()
this.dependencies.FileHostingService.setFile(
this.typeNames[0],
id,
`${baseRoute}${fileName}`,
buffer,
encodings,
(req, res) => {
res.set("Content-Type", "application/x-javascript")
if(onRequest != null) {
return onRequest(req, res)
} else {
return Promise.resolve(true)
}
}
)
return id
})
)
})
.catch(error => this.dependencies.LogService.log(this.typeNames[0], LogLevel.ERROR, error.toString()))
}
/**
* deletes a compilation and all hosted files assoicated with it
* @param injectableName creator injectable
* @param compilationId id of the compilation
*/
public deleteCompilation(injectableName: string, compilationId: number): void {
let compilationFileIds = this.resourceStore.getResource(injectableName, "comilationFileIds")
if(compilationFileIds != null) {
let fileIds = compilationFileIds.get(compilationId)
if(fileIds != null) {
fileIds.forEach(fileId => this.dependencies.FileHostingService.deleteFile(this.typeNames[0], fileId))
compilationFileIds.delete(compilationId)
}
}
let compileRequests = this.resourceStore.getResource(injectableName, "compilationRequests")
if(compileRequests != null) {
compileRequests.delete(compilationId)
}
}
protected onClose(): void {
}
protected removeResources(injectableName: string): void {
let compilationFileIds = this.resourceStore.getResource(injectableName, "comilationFileIds")
if(compilationFileIds != null) {
compilationFileIds.forEach(fileIds =>
fileIds.forEach(fileId =>
this.dependencies.FileHostingService.deleteFile(this.typeNames[0], fileId)
)
)
}
this.resourceStore.removeResources(injectableName)
}
private compileFiles(
injectableName: string,
compilationId: number,
files: { [filename: string]: string },
configuration: Configuration
): Promise<CompilationResult> {
return new Promise((resolve, reject) => {
let compileRequestMap = this.resourceStore.getResource(injectableName, "compilationRequests")
if(compileRequestMap == null) {
compileRequestMap = new Map<number, Array<CompileRequest>>([])
this.resourceStore.setResource(injectableName, "compilationRequests", compileRequestMap)
}
let compileRequests = compileRequestMap.get(compilationId)
let request: CompileRequest = {
resolve,
reject,
files,
configuration
}
if(compileRequests == null) {
compileRequests = [ request ]
compileRequestMap.set(compilationId, compileRequests)
} else {
//the queue is just 2 long, and the last entry gets overwritten because it got updated
compileRequests[Math.min(compileRequests.length, 1)] = request
}
if(compileRequests.length > 0) {
this.compileQueue(compileRequests)
}
})
}
/**
*
* queue.length must be at least 1
* @param hash
* @param compileRequests
*/
private compileQueue(compileRequests: Array<CompileRequest>): void {
let request = compileRequests.shift()!
this.compile(
request.files,
request.configuration
)
.then(result => {
request.resolve(result)
if(compileRequests.length > 0) {
this.compileQueue(compileRequests)
}
})
.catch(request.reject)
}
private compile(files: { [filename: string]: string }, configuration: Configuration): Promise<CompilationResult> {
//TODO all dynamic imports need to get recompiled even if just one changes
let inputMemFs = new MemoryFS()
let outputMemFs = new MemoryFS()
let unionFs = new Union()
unionFs.use(fs).use(inputMemFs)
Object.entries(files).forEach(([filename, code]) => inputMemFs.writeFileSync(`/${filename}`, code))
let webpackOutput = configuration.output || {}
let webpackResolve = configuration.resolve || {}
let webpackModules = webpackResolve.modules || []
let nodeModulesPath = resolve(process.cwd(), "node_modules")
webpackModules.push(nodeModulesPath)
webpackOutput.publicPath = "/"
webpackOutput.path = "/"
webpackResolve.modules = webpackModules
configuration.output = webpackOutput
configuration.resolve = webpackResolve
return new Promise((resolve, reject) => {
let compiler = webpack(configuration)
compiler.inputFileSystem = <any>unionFs
compiler.outputFileSystem = outputMemFs
compiler.run((_error, stats) => {
if (stats.hasErrors()) {
reject(stats.toString("errors-only"))
} else {
let files = Object.entries<[string, Buffer]>(outputMemFs.data)
.reduce((prev, [filename, code]) => {
prev[filename] = code
return prev
}, <any>{})
resolve(files)
}
})
})
}
}
type CompilationResult = { [filename: string]: Buffer }
type CompileRequest = {
resolve: (result: CompilationResult) => void
reject: (error: any) => void
files: { [filename: string]: string }
configuration: Configuration
}
export { Configuration } from "webpack"
export { Encoding, OnRequest } from "@injectable/file-hosting-service"
export default class WebpackServiceContainer extends InjectableContainer<WebpackService> {
typeNames: ["WebpackService"] = ["WebpackService"]
injectableClass = WebpackService
dependencyTypeNames: [ "FileHostingService", "LogService" ] = [ "FileHostingService", "LogService" ]
}