UNPKG

@injectable/webpack-service

Version:
236 lines (218 loc) 9.77 kB
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" ] }