UNPKG

werkzeug

Version:

compiles ts, coffee, sass/scss, less/ stylus and packs browser ready bundles

730 lines (570 loc) 22.7 kB
FS = require 'fs' FSE = require 'fs-extra' Path = require 'path' nga = require 'ng-annotate/ng-annotate-main' Dict = require 'jsdictionary' Babel = require 'babel-core' Babel_es2015 = require 'babel-preset-es2015' JMin = require 'jsonminify' FSU = require '../utils/fsu' PH = require '../utils/path-helper' IPC = require '../utils/ipc' Log = require '../utils/log' PROCESS_BASE = Path.join __dirname, '..', '..' PACK_CODE = FS.readFileSync Path.join(__dirname, 'pack.js'), 'utf8' CHUNK_CODE = FS.readFileSync Path.join(__dirname, 'chunk.js'), 'utf8' MULTI_COMMENT_MAP = /\/\*\s*[@#]\s*sourceMappingURL\s*=\s*([^\s]*)\s*\*\//g SINGLE_COMMENT_MAP = /\/\/\s*[@#]\s*sourceMappingURL\s*=\s*([^\s]*)($|\n|\r\n?)/g ENV = 'development' #TODO: make chunk loader mechanism more dynamic (currently only Promis variant available) getPackCode = (p) -> """ (function(pack) { var win = window, process = win.process || (win.process = {}) env = process.env || (process.env = {}) cfg = { index: #{p.index}, total: #{p.total}, startIndex: #{p.file.index}, type: 'register::#{p.id}', path: '#{p.file.path}', pack: pack }; env.NODE_ENV = env.NODE_ENV || '#{p.env}' var packer = #{if p.index == 0 then PACK_CODE else CHUNK_CODE} packer.init(cfg); })({ #{p.code} });""" getChunkCode = (p) -> """ (function(pack) { var cfg = { type: 'register::#{p.id}', path: '#{p.file.path}', chunk: '#{p.chunk}', pack: pack }; var chunk = #{CHUNK_CODE} chunk.init(cfg); })({ #{p.code} });""" class Indexer constructor: () -> @current = -1 @cache = {} get: (path) -> cached = @cache[path] if not isNaN cached return cached ++@current remove: (path, index) -> @cache[path] = index null babelOptions = ast: false compact: false presets: [Babel_es2015] class Packer constructor: () -> @create() create: () -> @indexer = new Indexer() @fileMap = {} # indexing: files mapped by path in tmp or node_modules @nodes = {} # indexing: currently unused @loaders = {} # indexing: map of all parsed loaders by path @openFiles = 0 # indexing: number of currently reading files @openPacks = 0 # packaging: number of currently writing packages @packed = null # packaging: map of already packed files @loaded = null # packaging: map of chunks loaded by a loader @packs = null # packaging: list of current packs @chunks = null # packaging: list of current chunks @ipc = new IPC(process, @) @errors = [] null init: (@cfg) -> @id = Math.random() + '_' + Date.now() @nga = @cfg.packer.nga or false @useBabel = not @cfg.packer.disableBabelFallback @useUglify = @cfg.packer.uglify == true @env = @cfg.packer.env or {} @NODE_ENV = @env.NODE_ENV or 'development' @out = PH.getOut @cfg, 'packer' null readPackages: () -> @errors = [] packages = @cfg.packer.packages or [] for cfg in packages path = Path.join @out, cfg.in @readFile path null update: (files) -> try errors = @errors @errors = [] updated = {} for f in files path = PH.outFromIn @cfg, null, f.path, true file = @fileMap[path] #console.log 'update file: ', path, f.path continue if not file or updated[path] updated[path] = true @clear file if not f.removed @readFile path for error in errors path = error.path file = @fileMap[path] continue if not file or updated[path] updated[path] = true @clear file @readFile path if @openFiles == 0 @completed() catch e console.log 'packer error: ', e.toString() null clear: (file) -> path = file.path for reqPath of file.req req = @fileMap[reqPath] delete req.ref[path] if req delete file.req[reqPath] for loderPath of file.reqAsL loaderRefs = @loaders[loderPath] if loaderRefs delete loaderRefs[path] if not Dict.hasKeys loaderRefs delete @loaders[loderPath] delete file.reqAsL[loderPath] delete @fileMap[path] @indexer.remove path, file.index null isRequired: (file) -> Dict.hasKeys file.ref or @loaders[file.path] removeSources: (path) -> FS.unlinkSync path FS.unlinkSync path + '.map' writePackages: () -> # remove current packs and chunks @removeSources(pack.out) for pack in @packs if @packs @removeSources(chunk.out) for chunk in @chunks if @chunks @totalModules = 0 @packed = {} @loaded = {} @packs = [] @chunks = [] packages = @cfg.packer.packages or [] # clear loader data for path of @fileMap file = @fileMap[path] file.loaders = {} file.parts = {} # create packs and gather all requireds for pack, i in packages by -1 path = Path.join @out, pack.in file = @fileMap[path] p = file: file index: i total: packages.length id: @id out: Path.join @out, pack.out req: {} loaders: {} code: '' env: @NODE_ENV numModules: 0 @packs.push p @gatherReq p, file # gather all modules for each loader for path of @loaders loader = @fileMap[path] @gatherChunks loader, loader # cleanup modules required by each loader for path of @loaded @cleanupChunks @fileMap[path] for p in @packs @writePack p for path of @loaders loader = @fileMap[path] chunk = @getChunkPath loader p = file: loader index: loader.index id: @id out: Path.join @out, chunk chunk: chunk code: '' numModules: 0 @chunks.push p @writeChunk p null getChunkPath: (loader) -> @cfg.packer.chunks + loader.index + '.js' gatherReq: (p, file) -> if @packed[file.index] return null @packed[file.index] = true p.req[file.path] = true for rpath of file.req rfile = @fileMap[rpath] if not rfile @errors.push path: file.path line: -1 col: -1 error: 'required file not found: ' + rpath else if not @packed[rfile.index] # add all loaders to the pack -> used by cleanupChunks for lpath of rfile.reqAsL p.loaders[lpath] = true @gatherReq(p, rfile) null gatherChunks: (loader, file) -> file.loaders[loader.path] = true loader.parts[file.path] = true @loaded[file.path] = true for rpath of file.req rfile = @fileMap[rpath] if not rfile @errors.push path: file.path line: -1 col: -1 error: 'required file not found: ' + rpath else @gatherChunks(loader, rfile) if not loader.parts[rpath] null cleanupChunks: (file) -> # returns a loader, if the file is required by exactly one loader loader = @getLoader file path = file.path packed = @packed[file.index] # remove file from loaders, if already packed # remove also if required by more than one loader in case bigChunks = false if packed or (not @cfg.packer.bigChunks and not loader) for lpath of file.loaders loader = @fileMap[lpath] delete loader.parts[path] delete file.loaders[lpath] # add file to the first matching pack if not already packed if not packed for p in @packs if p.loaders[lpath] p.req[path] = true @packed[file.index] = true packed = true break null getLoader: (file) -> count = 0 for path of file.loaders ++count return null if count > 1 @fileMap[path] initSourceMapping: (pack, type) -> if type == 'pack' if pack.index == 0 @lineOffset = 194 + 4 else @lineOffset = 50 else @lineOffset = 48 origin = Path.relative @out, pack.out @sourceMap = version : 3 file: origin sourceRoot: '' sources: [ origin ] sections: [] null addSourceMap: (pack, file, singleLine) -> @lineOffset += if singleLine then 2 else 3 map = file.sourceMap if map @sourceMap.sections.push offset: line: @lineOffset column: 0 map: map @lineOffset += 1 + (if singleLine then 1 else file.numLines) null writeSourceMap: (pack) -> mapOut = pack.out + '.map' FSE.ensureFileSync mapOut FS.writeFileSync mapOut, JSON.stringify(@sourceMap), 'utf8' pack.code += "\r\n//# sourceMappingURL=#{Path.relative @out, pack.out}.map" writePack: (p) -> @initSourceMapping(p, 'pack') @addPackSrc p, @fileMap[path] for path of p.req p.code = p.code.slice 0, -3 p.code = getPackCode p ++@openPacks @writeSourceMap p #TODO: handle write errors FSE.ensureFileSync p.out FS.writeFile p.out, p.code, 'utf8', (error) => --@openPacks if error console.log 'packer.writePack: pack write error: ', p.out if @openPacks == 0 @completed() null null writeChunk: (p) -> @initSourceMapping(p, 'chunk') @addPackSrc p, @fileMap[path] for path of p.file.parts p.code = p.code.slice 0, -3 p.code = getChunkCode p ++@openPacks @writeSourceMap p #TODO: handle write errors FSE.ensureFileSync p.out FS.writeFile p.out, p.code, 'utf8', (error) => --@openPacks if error console.log 'packer.writeChunk: chunk write error: ', p.out if @openPacks == 0 @completed() null null addPackSrc: (p, file) -> ++@totalModules ++p.numModules source = file.source source = nga(source, add:true).src if @nga moduleId = Path.relative @out, file.path code = "// #{file.path}\r\n#{file.index}: " if /.js$/.test file.path code += "function(module, exports, require) {\r\nmodule.id = '#{moduleId}';\r\n#{source}\r\n},\r\n" @addSourceMap p, file, false else # replace ' with \' source = source.replace /'/g, (args...) -> if args[2][args[1] - 1] != '\\' then "\\'" else "'" #TODO: check, if this causes problems with JMin # surround with quotes source = "'#{source}'" #TODO: maybe do JSON.parse to check for json -> currently json files without extension can't be required if /.json$/.test file.path source = "JSON.parse(#{JMin source})" # replace newlines with \n source = source.replace /\r\n|\n/g, '\\n' # html can have nested requireds: ${require('path/to/html')} if /.html$/.test file.path source = source.replace /\${\s*(require\s*\(\s*\d*?\s*\))\s*}/g, "' + $1 + '" code += "function(module, exports, require) {\r\nmodule.exports = #{source};\r\n},\r\n" @addSourceMap p, file, true p.code += code null readFile: (path, parent) -> file = @fileMap[path] if file if parent parent.req[path] = true file.ref[parent.path] = true return file file = @fileMap[path] = index: @indexer.get path path: path source: '' sourceMap: '' numLines: 0 ref: {} req: {} reqAsL: {} error: false if parent parent.req[path] = true file.ref[parent.path] = true #console.log 'read file: ', path ++@openFiles FS.readFile path, 'utf8', (error, source) => if error file.error = error @errors.push path: path line: -1 col: -1 error: 'file read error' else if /\.js$/.test path # uglify if @cfg.packer.uglify source = source.replace /process\.env\.NODE_ENV/g, 'NODE_ENV' @uglify = @uglify or require 'uglify-js' try result = @uglify.minify source, fromString: true compress: global_defs: 'NODE_ENV': @cfg.packer.env?.NODE_ENV or ENV catch e console.log 'error while uglifying: ', path, e source = result.code if result #TODO: babel should be a compiler # use babel if file is in node-modules and isn't an umd module and has an import statement if @useBabel and /node_modules/.test(path) and not /\.umd\./.test(path) and /((^| )import )|((^| )class )|((^| )let )|((^| )const |((^| )export ))/gm.test(source) result = Babel.transform source, babelOptions source = result.code console.log 'babel: transformed -> ' + Path.relative @cfg.base, path # handle source map if PH.testJS(path) moduleId = Path.relative @out, path mapPath = path + '.map' source = source.replace SINGLE_COMMENT_MAP, '' source = source.replace MULTI_COMMENT_MAP, '' numLines = (source or '').split(/\r\n|\n/).length fixFF = @cfg.options.fffMaps includeExt = @cfg.options.includeExternalMaps if FSU.isFile(mapPath) and (path.indexOf(@out) == 0 or includeExt) map = FSU.require mapPath map.file = Path.basename path file.sourceMap = map # only touch sourcesContent to fix firefox sourcemap bug if fixFF map.sourcesContent = map.sourcesContent or [] # correct paths to original sources # and include sources, if firefox fixing is enabled for sourcePath, i in map.sources absSourcePath = Path.join map.sourceRoot, sourcePath if fixFF and not map.sourcesContent[i] # add all original sources to the map to fix firefox sourcemap bug if FSU.isFile(absSourcePath) map.sourcesContent.push FS.readFileSync(absSourcePath, 'utf8') else map.sourcesContent.push '' #console.log '', absSourcePath map.sources[i] = Path.relative @out, absSourcePath map.sourceRoot = '' file.moduleId = moduleId file.source = source file.numLines = numLines @parseFile file @writePackages() if --@openFiles == 0 null file parseFile: (file) -> path = file.path base = Path.dirname path #regex = /^([^\/]|(\/(?!\/)))*?require\s*\(\s*('|")(.*?)('|")\s*\)/gm regex = /require\s*\(\s*('|")(.*?)('|")\s*\)/gm regPos = 2 loaderRegex = new RegExp('^' + @cfg.packer.loaderPrefix) #while result = regex.exec file.source file.source = file.source.replace regex, (args...) => name = PH.correctOut args[regPos] isLoader = loaderRegex.test name name = name.replace /^es6-promise\!/, '' if isLoader if /\.|\//.test(name[0]) modulePath = @getRelModulePath base, name else modulePath = @getNodeModulePath base, name if modulePath and not @nodes[modulePath] @nodes[modulePath] = true # currently unused if modulePath rfile = @readFile modulePath, file if isLoader rpath = rfile.path loaderRefs = @loaders[rpath] or @loaders[rpath] = {} loaderRefs[path] = true file.reqAsL[rpath] = true # remove linking to enable chunks delete file.req[rpath] delete rfile.ref[path] return "require(#{rfile.index}, '#{@getChunkPath rfile}')" return "require(#{rfile.index})" else if not @isComment file.source, args[4] @errors.push path: path line: -1 col: -1 error: 'packer.parseFile: module "' + name + '" not found' args[0] null isComment: (text, index) -> sameLine = true behindComment = false while --index > -1 char1 = text[index] char2 = text[index + 1] chars = char1 + char2 sameLine = sameLine and char1 != '\n' insideComment = chars == '/*' behindComment = chars == '*/' return true if sameLine and chars == '//' return true if insideComment return false if behindComment return false getRelModulePath: (base, moduleName) -> ext = @testExt moduleName, '.js' path = Path.resolve base, moduleName return file if @isFile file = path + ext # js file found return file if @isFile file = Path.join path, 'index.js' # index.js file found return path if ext and @isFile path # asset file found null getNodeModulePath: (base, moduleName) -> nodePath = Path.join base, 'node_modules' modulePath = Path.join nodePath, moduleName if @isDir nodePath ext = @testExt moduleName, '.js' return file if @isFile file = modulePath + ext # .js file = Path.join modulePath, 'package.json' # package.json try json = FSU.requireJson file main = json?.main catch if main ext = @testExt main, '.js' if @isFile file = Path.join modulePath, main + ext # main return file return file if @isFile file = Path.join modulePath, 'index.js' # index.js if base != @cfg.base and base != PROCESS_BASE and base != '/' # abort, if outside project root return @getNodeModulePath Path.resolve(base, '..'), moduleName # try next dir # try modules shipped with werkzeug if base != PROCESS_BASE return @getNodeModulePath PROCESS_BASE, moduleName null testExt: (name, ext) -> return '' if new RegExp(ext + '$').test name ext isDir: (path) -> stat = @getStat(path) return true if stat?.isDirectory() false isFile: (path) -> stat = @getStat path return true if stat?.isFile() false getStat: (path) -> try return FS.statSync path catch error null completed: () => info = errors: @errors totalModules: @totalModules modules: [] chunks: [] for pack, i in @packs by -1 info.modules.push modules: pack.numModules path: pack.file.path out: pack.out for chunk, i in @chunks info.chunks.push modules: chunk.index path: chunk.file.path out: pack.out @ipc.send 'packed', info null exit: () -> null module.exports = new Packer()