diff --git a/lib/CompatibilityPlugin.ts b/lib/CompatibilityPlugin.ts index c503ab66812..94b0dc837c5 100644 --- a/lib/CompatibilityPlugin.ts +++ b/lib/CompatibilityPlugin.ts @@ -8,9 +8,12 @@ import Compiler = require('./Compiler') import Compilation = require('./Compilation') import Parser = require('./Parser') import { CallExpression } from 'estree' -import { CompilationParams, ParserOptions } from '../typings/webpack-types' +import { CompilationParams, ParserOptions, NMFAfterResolveResult } from '../typings/webpack-types' import ContextDependency = require('./dependencies/ContextDependency') +const jsonLoaderPath = require.resolve('json-loader'); +const matchJson = /\.json$/i; + class CompatibilityPlugin { apply(compiler: Compiler) { compiler.plugin('compilation', function (compilation: Compilation, params: CompilationParams) { @@ -48,6 +51,16 @@ class CompatibilityPlugin { return true; }); }); + params.normalModuleFactory.plugin('after-resolve', function (data: NMFAfterResolveResult, done) { + // if this is a json file and there are no loaders active, we use the json-loader in order to avoid + // parse errors @see https://github.com/webpack/webpack/issues/3363 + if (matchJson.test(data.request) && data.loaders.length === 0) { + data.loaders.push({ + loader: jsonLoaderPath + }); + } + done(null, data); + }); }); } } diff --git a/lib/Compilation.ts b/lib/Compilation.ts index 3e02a2a71fe..420e8345814 100644 --- a/lib/Compilation.ts +++ b/lib/Compilation.ts @@ -12,7 +12,9 @@ import { ErrCallback, PlainObject, TimeStampMap, - WebpackError, AbstractInputFileSystem + WebpackError, + AbstractInputFileSystem, + PerformanceOptions } from '../typings/webpack-types' import { ResolveError } from 'enhanced-resolve/lib/common-types' import async = require('async'); @@ -36,7 +38,6 @@ import ChunkRenderError = require('./ChunkRenderError'); import NormalModule = require('./NormalModule') import DependenciesBlock = require('./DependenciesBlock') import AsyncDependenciesBlock = require('./AsyncDependenciesBlock') -import * as Resolve from 'enhanced-resolve' interface SlotChunk { name: string @@ -82,6 +83,7 @@ class Compilation extends Tapable { options: WebpackOptions outputOptions: WebpackOutputOptions preparedChunks: SlotChunk[] + performance: PerformanceOptions profile: boolean records: Record resolvers: Compiler.Resolvers @@ -99,6 +101,7 @@ class Compilation extends Tapable { this.outputOptions = options && options.output; this.bail = options && options.bail; this.profile = options && options.profile; + this.performance = options && options.performance; this.mainTemplate = new MainTemplate(this.outputOptions); this.chunkTemplate = new ChunkTemplate(this.outputOptions); @@ -437,12 +440,12 @@ class Compilation extends Tapable { const start = this.profile && +new Date(); const errorAndCallback = this.bail ? function errorAndCallback(err: ModuleNotFoundError) { - callback(err); - } : function errorAndCallback(err: ModuleNotFoundError) { - err.dependencies = [dependency]; - this.errors.push(err); - callback(); - }.bind(this); + callback(err); + } : function errorAndCallback(err: ModuleNotFoundError) { + err.dependencies = [dependency]; + this.errors.push(err); + callback(); + }.bind(this); if (typeof dependency !== 'object' || dependency === null || !dependency.constructor) { throw new Error('Parameter \'dependency\' must be a Dependency'); @@ -1083,7 +1086,7 @@ class Compilation extends Tapable { let source; let file; const filenameTemplate = chunk.filenameTemplate - ? chunk.filenameTemplate as string + ? chunk.filenameTemplate : chunk.isInitial() ? filename : chunkFilename; try { const useChunkHash = !chunk.hasRuntime() || this.mainTemplate.useChunkHash && this.mainTemplate.useChunkHash(chunk); @@ -1156,6 +1159,7 @@ declare namespace Compilation { __SourceMapDevToolData?: Dictionary emitted?: boolean existsAt?: string + isOverSizeLimit?: boolean } } diff --git a/lib/Compiler.ts b/lib/Compiler.ts index 59d5f87b3c8..cb4e4e6f72a 100644 --- a/lib/Compiler.ts +++ b/lib/Compiler.ts @@ -20,7 +20,8 @@ import { TimeStampMap, WatchFileSystem, WatchOptions, - AbstractInputFileSystem, AbstractStats + AbstractInputFileSystem, + AbstractStats } from '../typings/webpack-types' import Parser = require('./Parser') import Stats = require('./Stats') diff --git a/lib/ContextModule.ts b/lib/ContextModule.ts index 4f8fa483f91..e6abe06869c 100644 --- a/lib/ContextModule.ts +++ b/lib/ContextModule.ts @@ -10,7 +10,6 @@ import AsyncDependenciesBlock = require('./AsyncDependenciesBlock'); import ModuleDependency = require('./dependencies/ModuleDependency'); import RequestShortener = require('./RequestShortener') import Compilation = require('./Compilation') -import * as Resolve from 'enhanced-resolve' class ContextModule extends Module { async: boolean @@ -90,7 +89,10 @@ class ContextModule extends Module { super.disconnect(); } - build(options: WebpackOptions, compilation: Compilation, resolver: any, fs: AbstractInputFileSystem, callback: ErrCallback) { + build( + options: WebpackOptions, compilation: Compilation, resolver: any, fs: AbstractInputFileSystem, + callback: ErrCallback + ) { this.built = true; this.builtTime = new Date().getTime(); const addon = this.addon; diff --git a/lib/ContextModuleFactory.ts b/lib/ContextModuleFactory.ts index 204ab29f1cc..6b8fd9a4edd 100644 --- a/lib/ContextModuleFactory.ts +++ b/lib/ContextModuleFactory.ts @@ -8,10 +8,14 @@ import Tapable = require('tapable'); import ContextModule = require('./ContextModule'); import ContextElementDependency = require('./dependencies/ContextElementDependency'); import ContextDependency = require('./dependencies/ContextDependency') -import { CMFBeforeResolveResult, ErrCallback, AlternativeModule, AbstractInputFileSystem } from '../typings/webpack-types' +import { + CMFBeforeResolveResult, + ErrCallback, + AlternativeModule, + AbstractInputFileSystem +} from '../typings/webpack-types' import { Stats } from 'fs' import Compiler = require('./Compiler') -import * as Resolve from 'enhanced-resolve' class ContextModuleFactory extends Tapable { constructor(public resolvers: Compiler.Resolvers) { @@ -126,7 +130,10 @@ class ContextModuleFactory extends Tapable { }); } - resolveDependencies(fs: AbstractInputFileSystem, resource: string, recursive: boolean, regExp: RegExp, callback: ErrCallback) { + resolveDependencies( + fs: AbstractInputFileSystem, resource: string, recursive: boolean, regExp: RegExp, + callback: ErrCallback + ) { if (!regExp || !resource) { return callback(null, []); } diff --git a/lib/ContextReplacementPlugin.ts b/lib/ContextReplacementPlugin.ts index 9819dca5185..41e9df76921 100644 --- a/lib/ContextReplacementPlugin.ts +++ b/lib/ContextReplacementPlugin.ts @@ -8,7 +8,9 @@ import Compiler = require('./Compiler') import ContextModuleFactory = require('./ContextModuleFactory') import ContextDependency = require('./dependencies/ContextDependency') import { - CMFAfterResolveResult, CMFBeforeResolveResult, ErrCallback, + CMFAfterResolveResult, + CMFBeforeResolveResult, + ErrCallback, AbstractInputFileSystem } from '../typings/webpack-types' import * as Resolve from 'enhanced-resolve' diff --git a/lib/Entrypoint.ts b/lib/Entrypoint.ts index 6a0bee11186..9b38768e0c0 100644 --- a/lib/Entrypoint.ts +++ b/lib/Entrypoint.ts @@ -3,6 +3,7 @@ Author Tobias Koppers @sokra */ import Chunk = require('./Chunk') +import Compilation = require('./Compilation') class Entrypoint { chunks: Chunk[] @@ -26,6 +27,28 @@ class Entrypoint { } chunk.entrypoints.push(this); } + + getFiles() { + const files: string[] = []; + + for (let chunk of this.chunks) { + for (let file of chunk.files) { + if (!files.includes(file)) { + files.push(file) + } + } + } + + return files; + } + + getSize(compilation: Compilation) { + const files = this.getFiles(); + + return files + .map(file => compilation.assets[file].size()) + .reduce((currentSize, nextSize) => currentSize + nextSize, 0); + } } export = Entrypoint; diff --git a/lib/JsonpMainTemplatePlugin.ts b/lib/JsonpMainTemplatePlugin.ts index 8f10fb5e830..e2c9393734f 100644 --- a/lib/JsonpMainTemplatePlugin.ts +++ b/lib/JsonpMainTemplatePlugin.ts @@ -142,15 +142,15 @@ class JsonpMainTemplatePlugin { 'while(resolves.length)', this.indent('resolves.shift()();'), this.entryPointInChildren(chunk) ? [ - 'if(executeModules) {', - this.indent([ - 'for(i=0; i < executeModules.length; i++) {', - this.indent(`result = ${this.requireFn}(${this.requireFn}.s = executeModules[i]);`), - '}' - ]), - '}', - 'return result;' - ] : '' + 'if(executeModules) {', + this.indent([ + 'for(i=0; i < executeModules.length; i++) {', + this.indent(`result = ${this.requireFn}(${this.requireFn}.s = executeModules[i]);`), + '}' + ]), + '}', + 'return result;' + ] : '' ]), '};' ]); diff --git a/lib/MainTemplate.ts b/lib/MainTemplate.ts index bbaecbbe26d..b875e720c80 100644 --- a/lib/MainTemplate.ts +++ b/lib/MainTemplate.ts @@ -68,22 +68,22 @@ class MainTemplate extends Template { '};', '', this.asString(outputOptions.strictModuleExceptionHandling ? [ - '// Execute the module function', - 'var threw = true;', - 'try {', - this.indent([ + '// Execute the module function', + 'var threw = true;', + 'try {', + this.indent([ + `modules[moduleId].call(module.exports, module, module.exports, ${this.renderRequireFunctionForModule(hash, chunk, 'moduleId')});`, + 'threw = false;' + ]), + '} finally {', + this.indent([ + 'if(threw) delete installedModules[moduleId];' + ]), + '}' + ] : [ + '// Execute the module function', `modules[moduleId].call(module.exports, module, module.exports, ${this.renderRequireFunctionForModule(hash, chunk, 'moduleId')});`, - 'threw = false;' ]), - '} finally {', - this.indent([ - 'if(threw) delete installedModules[moduleId];' - ]), - '}' - ] : [ - '// Execute the module function', - `modules[moduleId].call(module.exports, module, module.exports, ${this.renderRequireFunctionForModule(hash, chunk, 'moduleId')});`, - ]), '', '// Flag the module as loaded', 'module.l = true;', @@ -113,16 +113,20 @@ class MainTemplate extends Template { buf.push(`${this.requireFn}.c = installedModules;`); buf.push(''); - buf.push('// identity function for calling harmory imports with the correct context'); + buf.push('// identity function for calling harmony imports with the correct context'); buf.push(`${this.requireFn}.i = function(value) { return value; };`); buf.push(''); - buf.push('// define getter function for harmory exports'); + buf.push('// define getter function for harmony exports'); buf.push(`${this.requireFn}.d = function(exports, name, getter) {`); buf.push(this.indent([ - 'Object.defineProperty(exports, name, {', - this.indent(['configurable: false,', 'enumerable: true,', 'get: getter']), - '});' + `if(!${this.requireFn}.o(exports, name)) \{`, + this.indent([ + 'Object.defineProperty(exports, name, {', + this.indent(['configurable: false,', 'enumerable: true,', 'get: getter']), + '});' + ]), + '}' ])); buf.push('};'); diff --git a/lib/Module.ts b/lib/Module.ts index c04fd7e02eb..c3746ff01ba 100644 --- a/lib/Module.ts +++ b/lib/Module.ts @@ -37,6 +37,7 @@ abstract class Module extends DependenciesBlock implements IRemoveAndDo { meta: Module.Meta; optional?: boolean parent: Module + portableId: string prefetched: boolean profile?: Module.Profile providedExports: string[] | boolean; @@ -56,6 +57,7 @@ abstract class Module extends DependenciesBlock implements IRemoveAndDo { this.debugId = debugId++; this.lastId = -1; this.id = null; + this.portableId = null; this.index = null; this.index2 = null; this.used = null; diff --git a/lib/NormalModule.ts b/lib/NormalModule.ts index fd8efcfcbbd..46c6c9fcec8 100644 --- a/lib/NormalModule.ts +++ b/lib/NormalModule.ts @@ -19,7 +19,8 @@ import { WebpackOutputOptions, TimeStampMap, SourceRange, - ErrCallback, AbstractInputFileSystem + ErrCallback, + AbstractInputFileSystem } from '../typings/webpack-types' import crypto = require('crypto') import path = require('path'); @@ -38,7 +39,6 @@ import DependenciesBlockVariable = require('./DependenciesBlockVariable') import DependenciesBlock = require('./DependenciesBlock') import Parser = require('./Parser') import Resolver = require('enhanced-resolve/lib/Resolver') -import * as Resolve from 'enhanced-resolve' function asString(buf: string | Buffer) { if (Buffer.isBuffer(buf)) { @@ -110,7 +110,10 @@ class NormalModule extends Module { return this.resource; } - doBuild(options: WebpackOptions, compilation: Compilation, resolver: Resolver, fs: AbstractInputFileSystem, callback: ErrCallback) { + doBuild( + options: WebpackOptions, compilation: Compilation, resolver: Resolver, fs: AbstractInputFileSystem, + callback: ErrCallback + ) { this.cacheable = false; const self = this; const loaderContext: LoaderContext = { @@ -205,7 +208,10 @@ class NormalModule extends Module { super.disconnect(); } - build(options: WebpackOptions, compilation: Compilation, resolver: Resolver, fs: AbstractInputFileSystem, callback: ErrCallback) { + build( + options: WebpackOptions, compilation: Compilation, resolver: Resolver, fs: AbstractInputFileSystem, + callback: ErrCallback + ) { const self = this; self.buildTimestamp = new Date().getTime(); self.built = true; diff --git a/lib/Parser.ts b/lib/Parser.ts index 0272cd8f255..eb7ba1309d3 100644 --- a/lib/Parser.ts +++ b/lib/Parser.ts @@ -4,19 +4,21 @@ */ import * as ESTree from 'estree' import { PlainObject, SourceRange, ParserState } from '../typings/webpack-types' -import acorn = require('acorn'); +import acorn from 'acorn-dynamic-import' import Tapable = require('tapable'); import BasicEvaluatedExpression = require('./BasicEvaluatedExpression'); import Module = require('./Module') import DependenciesBlock = require('./DependenciesBlock') import Compilation = require('./Compilation') +// todo: once update acorn.d.ts, remove this interface interface ASTOPTION { ecmaVersion: number locations: boolean onComment: any[] ranges: boolean sourceType: string + plugins: any } interface IdentCallback { @@ -28,12 +30,18 @@ const POSSIBLE_AST_OPTIONS = [ ranges: true, locations: true, ecmaVersion: 2017, - sourceType: 'module' + sourceType: 'module', + plugins: { + dynamicImport: true + } }, { ranges: true, locations: true, ecmaVersion: 2017, - sourceType: 'script' + sourceType: 'script', + plugins: { + dynamicImport: true + } } ] as ASTOPTION[]; @@ -41,7 +49,7 @@ interface ParserScope { definitions: string[] inShorthand?: boolean inTry: boolean - renames: Dictionary + renames: Dictionary } //noinspection JSUnusedGlobalSymbols,JSMethodCanBeStatic @@ -1112,6 +1120,16 @@ class Parser extends Tapable { // (function(...) { }(...)) walkIIFE.call(this, expression.callee, expression.arguments); } + else if (expression.callee.type === 'Import') { + const result = this.applyPluginsBailResult1('import-call', expression); + if (result === true) { + return; + } + + if (expression.arguments) { + this.walkExpressions(expression.arguments); + } + } else { const callee = this.evaluateExpression(expression.callee); @@ -1335,6 +1353,9 @@ class Parser extends Tapable { locations: true, ecmaVersion: 2017, sourceType: 'module', + plugins: { + dynamicImport: true + }, onComment: comments }); } @@ -1362,7 +1383,10 @@ class Parser extends Tapable { ranges: true, locations: true, ecmaVersion: 2017, - sourceType: 'module' + sourceType: 'module', + plugins: { + dynamicImport: true + } }); if (!ast || typeof ast !== 'object' || ast.type !== 'Program') { throw new Error('evaluate: Source couldn\'t be parsed'); diff --git a/lib/RecordIdsPlugin.ts b/lib/RecordIdsPlugin.ts index fc0075f7796..4d00fafd1c7 100644 --- a/lib/RecordIdsPlugin.ts +++ b/lib/RecordIdsPlugin.ts @@ -24,7 +24,10 @@ class RecordIdsPlugin { records.modules.usedIds = {}; } modules.forEach(module => { - const identifier = makeRelative(compiler, module.identifier()); + if (!module.portableId) { + module.portableId = makeRelative(compiler, module.identifier()); + } + const identifier = module.portableId; records.modules.byIdentifier[identifier] = module.id; records.modules.usedIds[module.id] = module.id; }); @@ -39,7 +42,10 @@ class RecordIdsPlugin { if (module.id !== null) { return; } - const identifier = makeRelative(compiler, module.identifier()); + if (!module.portableId) { + module.portableId = makeRelative(compiler, module.identifier()); + } + const identifier = module.portableId; const id = records.modules.byIdentifier[identifier]; if (id === undefined) { return; diff --git a/lib/SetVarMainTemplatePlugin.ts b/lib/SetVarMainTemplatePlugin.ts index 9a372a8b81d..5a5be92a6b7 100644 --- a/lib/SetVarMainTemplatePlugin.ts +++ b/lib/SetVarMainTemplatePlugin.ts @@ -27,7 +27,7 @@ class SetVarMainTemplatePlugin { } }); mainTemplate.plugin('global-hash-paths', function (paths: string[]) { - // todo: this is no varExpression in MainTemplate, may refer wrongly + // todo: this is no varExpression in MainTemplate, may refer wrongly, dead code if (this.varExpression) { paths.push(this.varExpression); } diff --git a/lib/SizeFormatHelpers.ts b/lib/SizeFormatHelpers.ts new file mode 100644 index 00000000000..67d2ddf8a1e --- /dev/null +++ b/lib/SizeFormatHelpers.ts @@ -0,0 +1,14 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Sean Larkin @thelarkinn + */ +export function formatSize(size: number) { + if (size <= 0) { + return '0 bytes'; + } + + const abbreviations = ['bytes', 'kB', 'MB', 'GB']; + const index = Math.floor(Math.log(size) / Math.log(1000)); + + return `${+(size / Math.pow(1000, index)).toPrecision(3)} ${abbreviations[index]}`; +} diff --git a/lib/SourceMapDevToolPlugin.ts b/lib/SourceMapDevToolPlugin.ts index 68feaa9e5f5..3c315d204b0 100644 --- a/lib/SourceMapDevToolPlugin.ts +++ b/lib/SourceMapDevToolPlugin.ts @@ -37,7 +37,7 @@ class SourceMapDevToolPlugin { throw new Error('SourceMapDevToolPlugin only takes one argument (pass an options object)'); } if (typeof options === 'string') { - // todo: here may be an error, no usage of options.sourceMapFilename + // todo: here may be an error, no usage of options.sourceMapFilename, dead code options = { sourceMapFilename: options } as SourceMapDevToolPlugin.Option; diff --git a/lib/Stats.ts b/lib/Stats.ts index 5fe980fc730..c55a6439f58 100644 --- a/lib/Stats.ts +++ b/lib/Stats.ts @@ -5,6 +5,7 @@ import RequestShortener = require('./RequestShortener'); import Compilation = require('./Compilation') import { StatsOptions, PlainObject, WebpackError } from '../typings/webpack-types' +import { formatSize } from './SizeFormatHelpers' import Module = require('./Module') import NormalModule = require('./NormalModule') import ModuleNotFoundError = require('./ModuleNotFoundError'); @@ -96,6 +97,7 @@ interface StatsAsset { emitted: boolean name: string size: number + isOverSizeLimit?: boolean } interface StatsEntryPoint { @@ -156,6 +158,7 @@ class Stats { const compilation = this.compilation; const requestShortener = new RequestShortener(d(options.context, process.cwd())); + const showPerformance = d(options.performance, true); const showHash = d(options.hash, true); const showVersion = d(options.version, true); const showTimings = d(options.timings, true); @@ -193,7 +196,7 @@ class Stats { if (excludeModules.length === 0) { return true; } - const ident = module.identifier(); + const ident = module.resource; return !excludeModules.some(regExp => regExp.test(ident)); } @@ -221,14 +224,14 @@ class Stats { function formatError(e: WebpackError | string) { let text = ''; let err: WebpackError = typeof e === 'string' ? { - message: e - } : e; + message: e + } : e; if (err.chunk) { text += `chunk ${err.chunk.name || err.chunk.id}${err.chunk.hasRuntime() ? ' [entry]' : err.chunk.isInitial() - ? ' [initial]' - : ''}\n`; + ? ' [initial]' + : ''}\n`; } if (err.file) { text += `${err.file}\n`; @@ -271,7 +274,6 @@ class Stats { return text; } - // todo: remove any type const obj: StatsJson = { errors: compilation.errors.map(formatError), warnings: compilation.warnings.map(formatError) @@ -317,7 +319,12 @@ class Stats { chunks: [] as number[], chunkNames: [] as string[], emitted: compilation.assets[asset].emitted - }; + } as StatsAsset; + + if (showPerformance) { + obj.isOverSizeLimit = compilation.assets[asset].isOverSizeLimit; + } + assetsByFile[asset] = obj; return obj; }) @@ -357,7 +364,6 @@ class Stats { } function fnModule(module: NormalModule) { - // todo: remove any type const obj: StatsModule = { id: module.id, identifier: module.identifier(), @@ -383,7 +389,6 @@ class Stats { obj.reasons = module.reasons .filter(reason => reason.dependency && reason.module) .map(reason => { - // todo: remove any type const obj: StatsReason = { moduleId: reason.module.id, moduleIdentifier: reason.module.identifier(), @@ -446,9 +451,9 @@ class Stats { moduleName: origin.module ? origin.module.readableIdentifier(requestShortener) : '', loc: typeof origin.loc === 'object' ? ( - obj.loc = `${origin.loc.start.line}:${origin.loc.start.column}-${origin.loc.start.line !== origin.loc.end.line - ? origin.loc.end.line + ':' - : ''}${origin.loc.end.column}`) + obj.loc = `${origin.loc.start.line}:${origin.loc.start.column}-${origin.loc.start.line !== origin.loc.end.line + ? origin.loc.end.line + ':' + : ''}${origin.loc.end.column}`) : '', name: origin.name, reasons: origin.reasons || [] @@ -552,7 +557,16 @@ class Stats { buffer.push('\n'); } - function table(array: string[][], formats: Function[], align: string, splitter = ' ') { + function getText(arr: any[], row: number, col: number) { + return arr[row][col].value; + } + + type TableItem = { + value: string + color: (str: string| number) => void + } + + function table(array: TableItem[][], align: string, splitter = ' ') { let row; const rows = array.length; let col; @@ -564,7 +578,10 @@ class Stats { } for (row = 0; row < rows; row++) { for (col = 0; col < cols; col++) { - value = `${array[row][col]}`; + value = `${getText(array, row, col)}`; + if (value.length === 0) { + colSizes[col] = 0; + } if (value.length > colSizes[col]) { colSizes[col] = value.length; } @@ -572,8 +589,8 @@ class Stats { } for (row = 0; row < rows; row++) { for (col = 0; col < cols; col++) { - const format = row === 0 ? colors.bold : formats[col]; - value = `${array[row][col]}`; + const format = array[row][col].color; + value = `${getText(array, row, col)}`; let l = value.length; if (align[col] === 'l') { format(value); @@ -585,7 +602,7 @@ class Stats { if (align[col] === 'r') { format(value); } - if (col + 1 < cols) { + if (col + 1 < cols && colSizes[col] != 0) { colors.normal(splitter); } } @@ -593,15 +610,12 @@ class Stats { } } - function formatSize(size: number) { - if (size <= 0) { - return '0 bytes'; + function getAssetColor(asset: StatsAsset, defaultColor: (str: string| number) => void) { + if (asset.isOverSizeLimit) { + return colors.yellow; } - const abbreviations = ['bytes', 'kB', 'MB', 'GB']; - const index = Math.floor(Math.log(size) / Math.log(1000)); - - return `${+(size / Math.pow(1000, index)).toPrecision(3)} ${abbreviations[index]}`; + return defaultColor; } if (obj.hash) { @@ -626,17 +640,59 @@ class Stats { newline(); } if (obj.assets && obj.assets.length > 0) { - const t = [['Asset', 'Size', 'Chunks', '', 'Chunk Names']]; - obj.assets.forEach(asset => { + const t = [ + [ + { + value: 'Asset', + color: colors.bold + }, + { + value: 'Size', + color: colors.bold + } + , + { + value: 'Chunks', + color: colors.bold + }, + { + value: '', + color: colors.bold + }, + { + value: '', + color: colors.bold + }, + { + value: 'Chunk Names', + color: colors.bold + } + ] + ]; + obj.assets.forEach(function (asset) { t.push([ - asset.name, - formatSize(asset.size), - asset.chunks.join(', '), - asset.emitted ? '[emitted]' : '', - asset.chunkNames.join(', ') + { + value: asset.name, + color: getAssetColor(asset, colors.green) + }, { + value: formatSize(asset.size), + color: getAssetColor(asset, colors.normal) + }, { + value: asset.chunks.join(', '), + color: colors.bold + }, { + value: asset.emitted ? '[emitted]' : '', + color: colors.green + }, { + value: asset.isOverSizeLimit ? '[big]' : '', + color: getAssetColor(asset, colors.normal) + }, { + value: asset.chunkNames.join(', '), + color: colors.normal + } ]); }); - table(t, [colors.green, colors.normal, colors.bold, colors.green, colors.normal], 'rrrll'); + table(t, 'rrrlll'); } if (obj.entrypoints) { Object.keys(obj.entrypoints).forEach(name => { @@ -725,6 +781,7 @@ class Stats { colors.normal(reason.type); colors.normal(' '); colors.cyan(reason.userRequest); + // todo: dead code if (reason.templateModules) { colors.cyan(reason.templateModules.join(' ')); } diff --git a/lib/UmdMainTemplatePlugin.ts b/lib/UmdMainTemplatePlugin.ts index b9d46e18281..ccd6d2b0c57 100644 --- a/lib/UmdMainTemplatePlugin.ts +++ b/lib/UmdMainTemplatePlugin.ts @@ -147,13 +147,13 @@ class UmdMainTemplatePlugin { ` + (requiredExternals.length > 0 ? (this.name && this.namedDefine === true - ? ` define(${libraryName(this.name)}, ${externalsDepsArray(requiredExternals)}, ${amdFactory});` - : ` define(${externalsDepsArray(requiredExternals)}, ${amdFactory});` - ) + ? ` define(${libraryName(this.name)}, ${externalsDepsArray(requiredExternals)}, ${amdFactory});` + : ` define(${externalsDepsArray(requiredExternals)}, ${amdFactory});` + ) : (this.name && this.namedDefine === true - ? ` define(${libraryName(this.name)}, [], ${amdFactory});` - : ` define([], ${amdFactory});` - ) + ? ` define(${libraryName(this.name)}, [], ${amdFactory});` + : ` define([], ${amdFactory});` + ) ) + (this.name ? ` @@ -167,8 +167,8 @@ class UmdMainTemplatePlugin { : ` else { ${(externals.length > 0 - ? ` var a = typeof exports === "object" ? factory(${externalsRequireArray('commonjs')}) : factory(${externalsRootArray(externals)});` - : ' var a = factory();')} + ? ` var a = typeof exports === "object" ? factory(${externalsRequireArray('commonjs')}) : factory(${externalsRootArray(externals)});` + : ' var a = factory();')} for(var i in a) (typeof exports === "object" ? exports : root)[i] = a[i]; } `) diff --git a/lib/WebpackOptionsApply.ts b/lib/WebpackOptionsApply.ts index 7ad6b8a8e18..35fcff45a5c 100644 --- a/lib/WebpackOptionsApply.ts +++ b/lib/WebpackOptionsApply.ts @@ -24,6 +24,7 @@ import LoaderPlugin = require('./dependencies/LoaderPlugin'); import CommonJsPlugin = require('./dependencies/CommonJsPlugin'); import HarmonyModulesPlugin = require('./dependencies/HarmonyModulesPlugin'); import SystemPlugin = require('./dependencies/SystemPlugin'); +import ImportPlugin = require('./dependencies/ImportPlugin'); import AMDPlugin = require('./dependencies/AMDPlugin'); import RequireContextPlugin = require('./dependencies/RequireContextPlugin'); import RequireEnsurePlugin = require('./dependencies/RequireEnsurePlugin'); @@ -36,6 +37,7 @@ import FlagIncludedChunksPlugin = require('./optimize/FlagIncludedChunksPlugin') import OccurrenceOrderPlugin = require('./optimize/OccurrenceOrderPlugin'); import FlagDependencyUsagePlugin = require('./FlagDependencyUsagePlugin'); import FlagDependencyExportsPlugin = require('./FlagDependencyExportsPlugin'); +import EmittedAssetSizeLimitPlugin = require('./performance/EmittedAssetSizeLimitPlugin'); import { ResolverFactory } from 'enhanced-resolve' import { WebpackOptions } from '../typings/webpack-types' import Compiler = require('./Compiler') @@ -209,10 +211,10 @@ class WebpackOptionsApply extends OptionsApply { let comment = legacy && modern ? '\n/*\n//@ sourceMappingURL=[url]\n//# sourceMappingURL=[url]\n*/' : legacy - ? '\n/*\n//@ sourceMappingURL=[url]\n*/' - : modern - ? '\n//# sourceMappingURL=[url]' - : undefined; + ? '\n/*\n//@ sourceMappingURL=[url]\n*/' + : modern + ? '\n//# sourceMappingURL=[url]' + : undefined; const Plugin = evalWrapped ? EvalSourceMapDevToolPlugin : SourceMapDevToolPlugin; compiler.apply( new Plugin( @@ -235,10 +237,10 @@ class WebpackOptionsApply extends OptionsApply { let comment = legacy && modern ? '\n//@ sourceURL=[url]\n//# sourceURL=[url]' : legacy - ? '\n//@ sourceURL=[url]' - : modern - ? '\n//# sourceURL=[url]' - : undefined; + ? '\n//@ sourceURL=[url]' + : modern + ? '\n//# sourceURL=[url]' + : undefined; compiler.apply(new EvalDevToolModulePlugin(comment, options.output.devtoolModuleFilenameTemplate)); } @@ -259,6 +261,7 @@ class WebpackOptionsApply extends OptionsApply { new AMDPlugin(options.module, options.amd || {}), new CommonJsPlugin(options.module), new HarmonyModulesPlugin(), + new ImportPlugin(options.module), new SystemPlugin(options.module) ); @@ -273,13 +276,15 @@ class WebpackOptionsApply extends OptionsApply { new FlagDependencyUsagePlugin() ); + compiler.apply(new EmittedAssetSizeLimitPlugin(options.performance)); + compiler.apply(new TemplatedPathPlugin()); compiler.apply(new RecordIdsPlugin()); compiler.apply(new WarnCaseSensitiveModulesPlugin()); - if (options.cache === undefined ? options.watch : options.cache) { + if (options.cache) { const CachePlugin = require('./CachePlugin'); compiler.apply(new CachePlugin(typeof options.cache === 'object' ? options.cache : undefined)); } diff --git a/lib/WebpackOptionsDefaulter.ts b/lib/WebpackOptionsDefaulter.ts index 36d026089f7..1b4071fb3e9 100644 --- a/lib/WebpackOptionsDefaulter.ts +++ b/lib/WebpackOptionsDefaulter.ts @@ -71,6 +71,13 @@ class WebpackOptionsDefaulter extends OptionsDefaulter { this.set('node.__filename', 'mock'); this.set('node.__dirname', 'mock'); + this.set('performance.maxAssetSize', 250000); + this.set('performance.maxInitialChunkSize', 250000); + this.set('performance.errorOnHint', false); + this.set('performance.hints', 'make', function (options: WebpackOptions) { + return options.target === 'web'; + }); + this.set('resolve', {}); this.set('resolve.unsafeCache', true); this.set('resolve.modules', ['node_modules']); diff --git a/lib/dependencies/ContextDependencyTemplateAsRequireCall.ts b/lib/dependencies/ContextDependencyTemplateAsRequireCall.ts index d34bbb347ef..be46eae006c 100644 --- a/lib/dependencies/ContextDependencyTemplateAsRequireCall.ts +++ b/lib/dependencies/ContextDependencyTemplateAsRequireCall.ts @@ -6,7 +6,7 @@ import AMDRequireContextDependency = require('./AMDRequireContextDependency') import { ReplaceSource } from 'webpack-sources' import { WebpackOutputOptions } from '../../typings/webpack-types' import RequestShortener = require('../RequestShortener') -import SystemImportContextDependency = require('./SystemImportContextDependency') +import SystemImportContextDependency = require('./ImportContextDependency') import CommonJsRequireContextDependency = require('./CommonJsRequireContextDependency') type RequireContextDependency = AMDRequireContextDependency | SystemImportContextDependency | CommonJsRequireContextDependency diff --git a/lib/dependencies/HarmonyCompatiblilityDependency.ts b/lib/dependencies/HarmonyCompatiblilityDependency.ts new file mode 100644 index 00000000000..250a4063fc3 --- /dev/null +++ b/lib/dependencies/HarmonyCompatiblilityDependency.ts @@ -0,0 +1,33 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra + */ +import NullDependency = require('./NullDependency'); +import Module = require('../Module') +import { ReplaceSource } from 'webpack-sources' + +class Template { + apply( + dep: HarmonyCompatiblilityDependency, + source: ReplaceSource, + ) { + const usedExports = dep.originModule.usedExports; + if (usedExports && !Array.isArray(usedExports)) { + const content = 'Object.defineProperty(exports, "__esModule", { value: true });\n'; + source.insert(-1, content); + } + + } +} + +class HarmonyCompatiblilityDependency extends NullDependency { + constructor(public originModule: Module) { + super(); + } + + static Template = Template +} + +HarmonyCompatiblilityDependency.prototype.type = 'harmony export header'; + +export = HarmonyCompatiblilityDependency; diff --git a/lib/dependencies/HarmonyExportDependencyParserPlugin.ts b/lib/dependencies/HarmonyExportDependencyParserPlugin.ts index 95dd323087a..a1163825789 100644 --- a/lib/dependencies/HarmonyExportDependencyParserPlugin.ts +++ b/lib/dependencies/HarmonyExportDependencyParserPlugin.ts @@ -9,16 +9,27 @@ import HarmonyExportSpecifierDependency = require('./HarmonyExportSpecifierDepen import HarmonyExportImportedSpecifierDependency = require('./HarmonyExportImportedSpecifierDependency'); import HarmonyImportDependency = require('./HarmonyImportDependency'); import HarmonyModulesHelpers = require('./HarmonyModulesHelpers'); -import { Statement, ExportNamedDeclaration, ExportDefaultDeclaration, Expression, Node } from 'estree' +import { Statement, ExportNamedDeclaration, ExportDefaultDeclaration, Expression, Node, SourceLocation } from 'estree' import Parser = require('../Parser') +import HarmonyCompatiblilityDependency = require('./HarmonyCompatiblilityDependency') +import Module = require('../Module') + +function makeHarmonyModule(module: Module, loc: SourceLocation) { + if (!module.meta.harmonyModule) { + const dep = new HarmonyCompatiblilityDependency(module); + dep.loc = loc; + module.addDependency(dep); + module.meta.harmonyModule = true; + module.strict = true; + } +} export = AbstractPlugin.create({ 'export'(this: Parser, statement: ExportNamedDeclaration | ExportDefaultDeclaration) { const dep = new HarmonyExportHeaderDependency(statement.declaration && statement.declaration.range, statement.range); dep.loc = statement.loc; this.state.current.addDependency(dep); - this.state.module.meta.harmonyModule = true; - this.state.module.strict = true; + makeHarmonyModule(this.state.module, statement.loc); return true; }, 'export import'(this: Parser, statement: Statement, source: string) { @@ -27,8 +38,7 @@ export = AbstractPlugin.create({ this.state.current.addDependency(dep); // todo: typo this.state.lastHarmoryImport = dep; - this.state.module.meta.harmonyModule = true; - this.state.module.strict = true; + makeHarmonyModule(this.state.module, statement.loc); return true; }, 'export expression'(this: Parser, statement: Statement, expr: Expression) { @@ -48,7 +58,7 @@ export = AbstractPlugin.create({ let dep; if (rename === 'imported var') { const settings = this.state.harmonySpecifier[`$${id}`]; - dep = new HarmonyExportImportedSpecifierDependency(this.state.module, settings[0], settings[1], settings[2], name, statement.range[1]); + dep = new HarmonyExportImportedSpecifierDependency(this.state.module, settings[0], settings[1], settings[2], name); } else { const immutable = statement.declaration && isImmutableStatement(statement.declaration); @@ -63,7 +73,7 @@ export = AbstractPlugin.create({ }, 'export import specifier'(this: Parser, statement: Statement, source: string, id: string, name: string) { // todo: here has typo - const dep = new HarmonyExportImportedSpecifierDependency(this.state.module, this.state.lastHarmoryImport, HarmonyModulesHelpers.getModuleVar(this.state, source), id, name, 0); + const dep = new HarmonyExportImportedSpecifierDependency(this.state.module, this.state.lastHarmoryImport, HarmonyModulesHelpers.getModuleVar(this.state, source), id, name); dep.loc = statement.loc; this.state.current.addDependency(dep); return true; diff --git a/lib/dependencies/HarmonyExportImportedSpecifierDependency.ts b/lib/dependencies/HarmonyExportImportedSpecifierDependency.ts index f6e96159eec..126193c3e1a 100644 --- a/lib/dependencies/HarmonyExportImportedSpecifierDependency.ts +++ b/lib/dependencies/HarmonyExportImportedSpecifierDependency.ts @@ -51,7 +51,7 @@ class Template { } else if (Array.isArray(dep.originModule.usedExports)) { // we know which exports are used - activeExports = HarmonyModulesHelpers.getActiveExports(dep.originModule); + activeExports = HarmonyModulesHelpers.getActiveExports(dep.originModule, dep); items = dep.originModule.usedExports.map(id => { if (id === 'default') { return; @@ -77,7 +77,7 @@ class Template { } else if (dep.originModule.usedExports && importedModule && Array.isArray(importedModule.providedExports)) { // not sure which exports are used, but we know which are provided - activeExports = HarmonyModulesHelpers.getActiveExports(dep.originModule); + activeExports = HarmonyModulesHelpers.getActiveExports(dep.originModule, dep); items = importedModule.providedExports.map(id => { if (id === 'default') { return; @@ -101,7 +101,7 @@ class Template { } else if (dep.originModule.usedExports) { // not sure which exports are used and provided - activeExports = HarmonyModulesHelpers.getActiveExports(dep.originModule); + activeExports = HarmonyModulesHelpers.getActiveExports(dep.originModule, dep); content = `/* harmony namespace reexport (unknown) */ for(var __WEBPACK_IMPORT_KEY__ in ${name}) `; // Filter out exports which are defined by other exports @@ -117,7 +117,7 @@ class Template { else { content = '/* unused harmony reexport namespace */\n'; } - source.insert(dep.position, content); + source.insert(-1, content); } } @@ -127,8 +127,7 @@ class HarmonyExportImportedSpecifierDependency extends NullDependency { public importDependency: Dependency, public importedVar: string, public id: string, - public name: string, - public position: number + public name: string ) { super(); } @@ -148,7 +147,7 @@ class HarmonyExportImportedSpecifierDependency extends NullDependency { const providedExports = m.providedExports if (Array.isArray(this.originModule.usedExports)) { // reexport * with known used exports - const activeExports = HarmonyModulesHelpers.getActiveExports(this.originModule); + const activeExports = HarmonyModulesHelpers.getActiveExports(this.originModule, this); if (Array.isArray(providedExports)) { return { module: m, @@ -232,6 +231,14 @@ class HarmonyExportImportedSpecifierDependency extends NullDependency { } describeHarmonyExport() { + const importedModule = this.importDependency.module; + if (!this.name && importedModule && Array.isArray(importedModule.providedExports)) { + // for a star export and when we know which exports are provided, we can tell so + return { + exportedName: importedModule.providedExports, + precedence: 3 + } + } return { exportedName: this.name, precedence: this.name ? 2 : 3 diff --git a/lib/dependencies/HarmonyModulesHelpers.ts b/lib/dependencies/HarmonyModulesHelpers.ts index ea1f0622f85..6eba1fc2d33 100644 --- a/lib/dependencies/HarmonyModulesHelpers.ts +++ b/lib/dependencies/HarmonyModulesHelpers.ts @@ -36,7 +36,7 @@ export function checkModuleVar(state: ParserState, request: string) { return getModuleVar(state, request); } -// checks if an harmory dependency is active in a module according to +// checks if an harmony dependency is active in a module according to // precedence rules. export function isActive(module: Module, depInQuestion: HarmonyExportDependency) { const desc = depInQuestion.describeHarmonyExport(); @@ -73,12 +73,17 @@ type HarmonyExportDependency = HarmonyExportSpecifierDependency | HarmonyExportI // get a list of named exports defined in a module // doesn't include * reexports. -export function getActiveExports(module: Module): string[] { - // todo: there is no activeExports assignment at all across the repo - if (module.activeExports) { - return module.activeExports; - } - return module.dependencies.reduce(function (arr, dep: HarmonyExportDependency) { +export function getActiveExports(module: Module, currentDependency: HarmonyExportDependency): string[] { + const desc = currentDependency && currentDependency.describeHarmonyExport(); + const currentIndex = currentDependency ? module.dependencies.indexOf(currentDependency) : -1; + + return module.dependencies.map(function (dep: HarmonyExportDependency, idx) { + return { + dep: dep, + idx: idx + } + }).reduce(function (arr, data) { + const dep = data.dep; if (!dep.describeHarmonyExport) { return arr; } @@ -86,11 +91,17 @@ export function getActiveExports(module: Module): string[] { if (!d) { return arr; } - const name = d.exportedName; - if (!name || arr.includes(name)) { - return arr; + if (!desc + || (d.precedence < desc.precedence) + || (d.precedence === desc.precedence && data.idx < currentIndex) + ) { + const names = [].concat(d.exportedName); + names.forEach(function (name) { + if (name && arr.indexOf(name) < 0) { + arr.push(name); + } + }); } - arr.push(name); return arr; }, []); } diff --git a/lib/dependencies/HarmonyModulesPlugin.ts b/lib/dependencies/HarmonyModulesPlugin.ts index 7e91abcc12c..e477c77f39b 100644 --- a/lib/dependencies/HarmonyModulesPlugin.ts +++ b/lib/dependencies/HarmonyModulesPlugin.ts @@ -17,6 +17,7 @@ import Compiler = require('../Compiler') import Compilation = require('../Compilation') import Parser = require('../Parser') import { CompilationParams, ParserOptions } from '../../typings/webpack-types' +import HarmonyCompatiblilityDependency = require('./HarmonyCompatiblilityDependency') class HarmonyModulesPlugin { apply(compiler: Compiler) { @@ -29,6 +30,10 @@ class HarmonyModulesPlugin { compilation.dependencyFactories.set(HarmonyImportSpecifierDependency, new NullFactory()); compilation.dependencyTemplates.set(HarmonyImportSpecifierDependency, new HarmonyImportSpecifierDependency.Template()); + compilation.dependencyFactories.set(HarmonyCompatiblilityDependency, new NullFactory()); + compilation.dependencyTemplates.set(HarmonyCompatiblilityDependency, new HarmonyCompatiblilityDependency.Template()); + + compilation.dependencyFactories.set(HarmonyExportHeaderDependency, new NullFactory()); compilation.dependencyTemplates.set(HarmonyExportHeaderDependency, new HarmonyExportHeaderDependency.Template()); diff --git a/lib/dependencies/SystemImportContextDependency.ts b/lib/dependencies/ImportContextDependency.ts similarity index 82% rename from lib/dependencies/SystemImportContextDependency.ts rename to lib/dependencies/ImportContextDependency.ts index 21d1db444a1..09a64041963 100644 --- a/lib/dependencies/SystemImportContextDependency.ts +++ b/lib/dependencies/ImportContextDependency.ts @@ -6,7 +6,7 @@ import ContextDependency = require('./ContextDependency'); import CriticalDependencyWarning = require('./CriticalDependencyWarning'); import { SourceRange } from '../../typings/webpack-types' -class SystemImportContextDependency extends ContextDependency { +class ImportContextDependency extends ContextDependency { async: boolean critical: false | string @@ -30,6 +30,6 @@ class SystemImportContextDependency extends ContextDependency { static Template = require('./ContextDependencyTemplateAsRequireCall') } -SystemImportContextDependency.prototype.type = 'System.import context'; +ImportContextDependency.prototype.type = 'System.import context'; -export = SystemImportContextDependency; +export = ImportContextDependency; diff --git a/lib/dependencies/SystemImportDependenciesBlock.ts b/lib/dependencies/ImportDependenciesBlock.ts similarity index 67% rename from lib/dependencies/SystemImportDependenciesBlock.ts rename to lib/dependencies/ImportDependenciesBlock.ts index 2858cbba2c7..201e96dd2fc 100644 --- a/lib/dependencies/SystemImportDependenciesBlock.ts +++ b/lib/dependencies/ImportDependenciesBlock.ts @@ -3,18 +3,18 @@ Author Tobias Koppers @sokra */ import AsyncDependenciesBlock = require('../AsyncDependenciesBlock'); -import SystemImportDependency = require('./SystemImportDependency'); +import ImportDependency = require('./ImportDependency'); import { SourceRange } from '../../typings/webpack-types' import { SourceLocation } from 'estree' import Module = require('../Module') -class SystemImportDependenciesBlock extends AsyncDependenciesBlock { +class ImportDependenciesBlock extends AsyncDependenciesBlock { constructor(request: string, public range: SourceRange, module: Module, loc: SourceLocation) { super(null, module, loc); - const dep = new SystemImportDependency(request, this); + const dep = new ImportDependency(request, this); dep.loc = loc; this.addDependency(dep); } } -export = SystemImportDependenciesBlock; +export = ImportDependenciesBlock; diff --git a/lib/dependencies/SystemImportDependency.ts b/lib/dependencies/ImportDependency.ts similarity index 78% rename from lib/dependencies/SystemImportDependency.ts rename to lib/dependencies/ImportDependency.ts index 8fae25e7ebc..9ec6af81bec 100644 --- a/lib/dependencies/SystemImportDependency.ts +++ b/lib/dependencies/ImportDependency.ts @@ -8,17 +8,17 @@ import WebpackMissingModule = require('./WebpackMissingModule'); import RequestShortener = require('../RequestShortener') import { WebpackOutputOptions } from '../../typings/webpack-types' import { ReplaceSource } from 'webpack-sources' -import SystemImportDependenciesBlock = require('./SystemImportDependenciesBlock') +import ImportDependenciesBlock = require('./ImportDependenciesBlock') class Template { apply( - dep: SystemImportDependency, + dep: ImportDependency, source: ReplaceSource, outputOptions: WebpackOutputOptions, requestShortener: RequestShortener ) { const depBlock = dep.block; - const promise = DepBlockHelpers.getDepBlockPromise(depBlock, outputOptions, requestShortener, 'System.import'); + const promise = DepBlockHelpers.getDepBlockPromise(depBlock, outputOptions, requestShortener, 'import()'); let comment = ''; if (outputOptions.pathinfo) { comment = `/*! ${requestShortener.shorten(dep.request)} */ `; @@ -35,14 +35,14 @@ class Template { } } -class SystemImportDependency extends ModuleDependency { - constructor(request: string, public block: SystemImportDependenciesBlock) { +class ImportDependency extends ModuleDependency { + constructor(request: string, public block: ImportDependenciesBlock) { super(request); } static Template = Template } -SystemImportDependency.prototype.type = 'System.import'; +ImportDependency.prototype.type = 'import()'; -export = SystemImportDependency; +export = ImportDependency; diff --git a/lib/dependencies/SystemImportParserPlugin.ts b/lib/dependencies/ImportParserPlugin.ts similarity index 63% rename from lib/dependencies/SystemImportParserPlugin.ts rename to lib/dependencies/ImportParserPlugin.ts index 06560f3b0a6..8801e1e63a5 100644 --- a/lib/dependencies/SystemImportParserPlugin.ts +++ b/lib/dependencies/ImportParserPlugin.ts @@ -2,32 +2,32 @@ MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ -import SystemImportContextDependency = require('./SystemImportContextDependency'); -import SystemImportDependenciesBlock = require('./SystemImportDependenciesBlock'); +import ImportContextDependency = require('./ImportContextDependency'); +import ImportDependenciesBlock = require('./ImportDependenciesBlock'); import ContextDependencyHelpers = require('./ContextDependencyHelpers'); import Parser = require('../Parser') import { CallExpression } from 'estree' import { ModuleOptions } from '../../typings/webpack-types' -class SystemImportParserPlugin { +class ImportParserPlugin { constructor(public options: ModuleOptions) { } apply(parser: Parser) { const options = this.options; - parser.plugin('call System.import', function (expr: CallExpression) { + parser.plugin(['call System.import', 'import-call'], function (expr: CallExpression) { if (expr.arguments.length !== 1) { - throw new Error('Incorrect number of arguments provided to \'System.import(module: string) -> Promise\'.'); + throw new Error('Incorrect number of arguments provided to \'import(module: string) -> Promise\'.'); } let dep; const param = this.evaluateExpression(expr.arguments[0]); if (param.isString()) { - const depBlock = new SystemImportDependenciesBlock(param.string, expr.range, this.state.module, expr.loc); + const depBlock = new ImportDependenciesBlock(param.string, expr.range, this.state.module, expr.loc); this.state.current.addBlock(depBlock); return true; } else { - dep = ContextDependencyHelpers.create(SystemImportContextDependency, expr.range, param, expr, options); + dep = ContextDependencyHelpers.create(ImportContextDependency, expr.range, param, expr, options); if (!dep) { return; } @@ -40,4 +40,4 @@ class SystemImportParserPlugin { } } -export = SystemImportParserPlugin; +export = ImportParserPlugin; diff --git a/lib/dependencies/ImportPlugin.ts b/lib/dependencies/ImportPlugin.ts new file mode 100644 index 00000000000..2177b56afc3 --- /dev/null +++ b/lib/dependencies/ImportPlugin.ts @@ -0,0 +1,44 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra + */ +import ImportDependency = require('./ImportDependency'); +import ImportContextDependency = require('./ImportContextDependency'); +import UnsupportedFeatureWarning = require('../UnsupportedFeatureWarning'); +import ConstDependency = require('./ConstDependency'); +import BasicEvaluatedExpression = require('../BasicEvaluatedExpression'); +import ImportParserPlugin = require('./ImportParserPlugin'); +import Compiler = require('../Compiler') +import Compilation = require('../Compilation') +import { CompilationParams, ParserOptions, ModuleOptions } from '../../typings/webpack-types' +import Parser = require('../Parser') + +class ImportPlugin { + constructor(public options: ModuleOptions) { + } + + apply(compiler: Compiler) { + const options = this.options; + compiler.plugin('compilation', (compilation: Compilation, params: CompilationParams) => { + const normalModuleFactory = params.normalModuleFactory; + const contextModuleFactory = params.contextModuleFactory; + + compilation.dependencyFactories.set(ImportDependency, normalModuleFactory); + compilation.dependencyTemplates.set(ImportDependency, new ImportDependency.Template()); + + compilation.dependencyFactories.set(ImportContextDependency, contextModuleFactory); + compilation.dependencyTemplates.set(ImportContextDependency, new ImportContextDependency.Template()); + + params.normalModuleFactory.plugin('parser', (parser: Parser, parserOptions: ParserOptions) => { + + if (typeof parserOptions.import !== 'undefined' && !parserOptions.import) { + return; + } + + parser.apply(new ImportParserPlugin(options)); + }); + }); + } +} + +export = ImportPlugin; diff --git a/lib/dependencies/SystemPlugin.ts b/lib/dependencies/SystemPlugin.ts index b19afdf2eb3..55347822c57 100644 --- a/lib/dependencies/SystemPlugin.ts +++ b/lib/dependencies/SystemPlugin.ts @@ -2,12 +2,9 @@ MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ -import SystemImportDependency = require('./SystemImportDependency'); -import SystemImportContextDependency = require('./SystemImportContextDependency'); import UnsupportedFeatureWarning = require('../UnsupportedFeatureWarning'); import ConstDependency = require('./ConstDependency'); import BasicEvaluatedExpression = require('../BasicEvaluatedExpression'); -import SystemImportParserPlugin = require('./SystemImportParserPlugin'); import Compiler = require('../Compiler') import Compilation = require('../Compilation') import Parser = require('../Parser') @@ -23,12 +20,6 @@ class SystemPlugin { const normalModuleFactory = params.normalModuleFactory; const contextModuleFactory = params.contextModuleFactory; - compilation.dependencyFactories.set(SystemImportDependency, normalModuleFactory); - compilation.dependencyTemplates.set(SystemImportDependency, new SystemImportDependency.Template()); - - compilation.dependencyFactories.set(SystemImportContextDependency, contextModuleFactory); - compilation.dependencyTemplates.set(SystemImportContextDependency, new SystemImportContextDependency.Template()); - params.normalModuleFactory.plugin('parser', function (parser: Parser, parserOptions: ParserOptions) { if (typeof parserOptions.system !== 'undefined' && !parserOptions.system) { return; @@ -73,7 +64,6 @@ class SystemPlugin { this.state.current.addDependency(dep); return true; }); - parser.apply(new SystemImportParserPlugin(options)); }); }); } diff --git a/lib/node/NodeOutputFileSystem.ts b/lib/node/NodeOutputFileSystem.ts index 4569c8253c3..37ac42b21d2 100644 --- a/lib/node/NodeOutputFileSystem.ts +++ b/lib/node/NodeOutputFileSystem.ts @@ -23,8 +23,14 @@ interface NodeOutputFileSystem { rmdir(path: string | Buffer, callback?: (err?: NodeJS.ErrnoException) => void): void; unlink(path: string | Buffer, callback?: (err?: NodeJS.ErrnoException) => void): void; writeFile(filename: string, data: any, callback?: (err: NodeJS.ErrnoException) => void): void; - writeFile(filename: string, data: any, options: { encoding?: string; mode?: number; flag?: string; }, callback?: (err: NodeJS.ErrnoException) => void): void; - writeFile(filename: string, data: any, options: { encoding?: string; mode?: string; flag?: string; }, callback?: (err: NodeJS.ErrnoException) => void): void; + writeFile( + filename: string, data: any, options: { encoding?: string; mode?: number; flag?: string; }, + callback?: (err: NodeJS.ErrnoException) => void + ): void; + writeFile( + filename: string, data: any, options: { encoding?: string; mode?: string; flag?: string; }, + callback?: (err: NodeJS.ErrnoException) => void + ): void; join(...paths: string[]): string; } diff --git a/lib/node/NodeWatchFileSystem.ts b/lib/node/NodeWatchFileSystem.ts index 6330ff32190..12907a24dd9 100644 --- a/lib/node/NodeWatchFileSystem.ts +++ b/lib/node/NodeWatchFileSystem.ts @@ -4,7 +4,6 @@ */ import Watchpack = require('watchpack'); import { WatchOptions, ErrCallback, AbstractInputFileSystem } from '../../typings/webpack-types' -import * as Resolve from 'enhanced-resolve' class NodeWatchFileSystem { watcherOptions: { diff --git a/lib/optimize/CommonsChunkPlugin.ts b/lib/optimize/CommonsChunkPlugin.ts index c9834f78d3b..96935bc8792 100644 --- a/lib/optimize/CommonsChunkPlugin.ts +++ b/lib/optimize/CommonsChunkPlugin.ts @@ -33,6 +33,8 @@ The available options are: names: string[] filename: string minChunks: number + chunks: string[] + children: boolean async: boolean minSize: number `); @@ -59,7 +61,7 @@ The available options are: const filenameTemplate = this.filenameTemplate; const minChunks = this.minChunks; const selectedChunks = this.selectedChunks; - const async = this.async; + const asyncOption = this.async; const minSize = this.minSize; const ident = this.ident; compiler.plugin('this-compilation', function (compilation: Compilation) { @@ -71,7 +73,7 @@ The available options are: compilation[ident] = true; let commonChunks: Chunk[]; - if (!chunkNames && (selectedChunks === false || async)) { + if (!chunkNames && (selectedChunks === false || asyncOption)) { commonChunks = chunks; } else if (Array.isArray(chunkNames) || typeof chunkNames === 'string') { @@ -96,9 +98,9 @@ The available options are: return selectedChunks.includes(chunk.name); }); } - else if (selectedChunks === false || async) { + else if (selectedChunks === false || asyncOption) { usedChunks = (commonChunk.chunks || []).filter(chunk => // we can only move modules from this chunk if the "commonChunk" is the only parent - async || chunk.parents.length === 1); + asyncOption || chunk.parents.length === 1); } else { if (commonChunk.parents.length > 0) { @@ -114,8 +116,8 @@ The available options are: }); } let asyncChunk: Chunk - if (async) { - asyncChunk = this.addChunk(typeof async === 'string' ? async : undefined); + if (asyncOption) { + asyncChunk = this.addChunk(typeof asyncOption === 'string' ? asyncOption : undefined); asyncChunk.chunkReason = 'async commons chunk'; asyncChunk.extraAsync = true; asyncChunk.addParent(commonChunk); @@ -170,7 +172,7 @@ The available options are: commonChunk.addModule(module); module.addChunk(commonChunk); }); - if (async) { + if (asyncOption) { reallyUsedChunks.forEach(chunk => { if (chunk.isInitial()) { return; diff --git a/lib/performance/AssetsOverSizeLimitWarning.ts b/lib/performance/AssetsOverSizeLimitWarning.ts new file mode 100644 index 00000000000..f0271eb3c34 --- /dev/null +++ b/lib/performance/AssetsOverSizeLimitWarning.ts @@ -0,0 +1,31 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Sean Larkin @thelarkinn + */ +import SizeFormatHelpers = require('../SizeFormatHelpers'); + +class AssetsOverSizeLimitWarning extends Error { + assets: AssetsOverSizeLimitWarning.OverSizeLimit[] + + constructor(assetsOverSizeLimit: AssetsOverSizeLimitWarning.OverSizeLimit[], assetLimit: number) { + super(); + Error.captureStackTrace(this, AssetsOverSizeLimitWarning); + this.name = 'AssetsOverSizeLimitWarning'; + this.assets = assetsOverSizeLimit; + + const assetLists = this.assets.map(asset => `\n ${asset.name} (${SizeFormatHelpers.formatSize(asset.size)})`) + .join(''); + + this.message = `asset size limit: The following asset(s) exceed the recommended size limit (${SizeFormatHelpers.formatSize(assetLimit)}). \nThis can impact web performance.\nAssets: ${assetLists}`; + } +} + +declare namespace AssetsOverSizeLimitWarning { + interface OverSizeLimit { + name: string + size: number + isOverSizeLimit?: boolean + } +} + +export = AssetsOverSizeLimitWarning; diff --git a/lib/performance/EmittedAssetSizeLimitPlugin.ts b/lib/performance/EmittedAssetSizeLimitPlugin.ts new file mode 100644 index 00000000000..809ac1e216a --- /dev/null +++ b/lib/performance/EmittedAssetSizeLimitPlugin.ts @@ -0,0 +1,95 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Sean Larkin @thelarkinn + */ +import path = require('path'); + +import EntrypointsOverSizeLimitWarning = require('./EntrypointsOverSizeLimitWarning'); +import AssetsOverSizeLimitWarning = require('./AssetsOverSizeLimitWarning'); +import NoAsyncChunksWarning = require('./NoAsyncChunksWarning'); +import Compiler = require('../Compiler') +import Compilation = require('../Compilation') +import { PerformanceOptions } from '../../typings/webpack-types' + +class EmittedAssetSizeLimitPlugin { + maxAssetSize?: number + maxInitialSize?: number + hints? + errorOnHint?: boolean + + constructor(performanceOptions: PerformanceOptions) { + this.maxAssetSize = performanceOptions.maxAssetSize; + this.maxInitialSize = performanceOptions.maxInitialChunkSize; + this.hints = performanceOptions.hints; + this.errorOnHint = performanceOptions.errorOnHint; + } + + apply(compiler: Compiler) { + if (!this.hints) { + return; + } + const entrypointSizeLimit = this.maxInitialSize; + const sizeLimit = this.maxAssetSize; + const hints = this.hints; + const shouldErrorOnHint = this.errorOnHint; + + compiler.plugin('after-emit', (compilation: Compilation, callback) => { + const warnings = []; + + const assetsOverSizeLimit: AssetsOverSizeLimitWarning.OverSizeLimit[] = []; + + Object.keys(compilation.assets).forEach(asset => { + const obj = { + name: asset, + size: compilation.assets[asset].size() + } as AssetsOverSizeLimitWarning.OverSizeLimit; + + if (doesExceedLimit(sizeLimit, obj.size)) { + obj.isOverSizeLimit = true; + assetsOverSizeLimit.push(obj); + compilation.assets[asset].isOverSizeLimit = true; + } + }); + + const hasAsyncChunks = compilation.chunks.filter(chunk => !chunk.isInitial()).length > 0; + + const entrypointsOverLimit = Object.keys(compilation.entrypoints) + .map(key => compilation.entrypoints[key]) + .filter(entry => doesExceedLimit(entrypointSizeLimit, entry.getSize(compilation))); + + // 1. Individual Chunk: Size < 250kb + // 2. Collective Initial Chunks [entrypoint] (Each Set?): Size < 250kb + // 3. No Async Chunks + // if !1, then 2, if !2 return + if (assetsOverSizeLimit.length > 0) { + warnings.push(new AssetsOverSizeLimitWarning(assetsOverSizeLimit, sizeLimit)); + } + if (entrypointsOverLimit.length > 0) { + warnings.push(new EntrypointsOverSizeLimitWarning(entrypointsOverLimit, compilation, entrypointSizeLimit)); + } + + if (warnings.length > 0) { + if (!hasAsyncChunks) { + warnings.push(new NoAsyncChunksWarning()); + } + + if (shouldErrorOnHint) { + Array.prototype.push.apply(compilation.errors, warnings); + } + else { + Array.prototype.push.apply(compilation.warnings, warnings); + } + } + + callback(); + }); + } +} + +export = EmittedAssetSizeLimitPlugin; + +// When using this we should always +// compare byte size and then format later +function doesExceedLimit(limit: number, actualSize: number) { + return limit < actualSize; +} diff --git a/lib/performance/EntrypointsOverSizeLimitWarning.ts b/lib/performance/EntrypointsOverSizeLimitWarning.ts new file mode 100644 index 00000000000..8953168d32a --- /dev/null +++ b/lib/performance/EntrypointsOverSizeLimitWarning.ts @@ -0,0 +1,27 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Sean Larkin @thelarkinn + */ +import SizeFormatHelpers = require('../SizeFormatHelpers'); +import Compilation = require('../Compilation') +import Entrypoint = require('../Entrypoint') + +class EntrypointsOverSizeLimitWarning extends Error { + constructor(public entrypoints: Entrypoint[], compilation: Compilation, entrypointLimit: number) { + super(); + Error.captureStackTrace(this, EntrypointsOverSizeLimitWarning); + this.name = 'EntrypointsOverSizeLimitWarning'; + + const entrypointCompilation = compilation; + const entrypointList = this.entrypoints.map(entrypoint => + `\n ${entrypoint.name} (${SizeFormatHelpers.formatSize(entrypoint.getSize(entrypointCompilation))})\n${entrypoint.getFiles() + .map((filename, index) => ' ' + entrypoint.getFiles()[index] + '\n') + .join('')}` + ) + .join(''); + + this.message = `entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (${SizeFormatHelpers.formatSize(entrypointLimit)}). This can impact web performance.\nEntrypoints:${entrypointList}`; + } +} + +export = EntrypointsOverSizeLimitWarning; diff --git a/lib/performance/NoAsyncChunksWarning.ts b/lib/performance/NoAsyncChunksWarning.ts new file mode 100644 index 00000000000..822f9c9cab7 --- /dev/null +++ b/lib/performance/NoAsyncChunksWarning.ts @@ -0,0 +1,15 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Sean Larkin @thelarkinn + */ +class NoAsyncChunksWarning extends Error { + constructor() { + super(); + Error.captureStackTrace(this, NoAsyncChunksWarning); + this.name = 'NoAsyncChunksWarning'; + + this.message = 'webpack performance recommendations: \n' + 'You can limit the size of your bundles by using System.import() or require.ensure to lazy load some parts of your application.\n' + 'For more info visit https://webpack.js.org/guides/code-splitting/'; + } +} + +export = NoAsyncChunksWarning; diff --git a/tsconfig.json b/tsconfig.json index de773842390..9c3d1ce8246 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ ], "declaration": false, "noImplicitAny": true, - "noImplicitThis": true, + "noImplicitThis": false, "suppressImplicitAnyIndexErrors": true, "pretty": true, "noImplicitReturns": false, diff --git a/typings/webpack-types.d.ts b/typings/webpack-types.d.ts index 6dfa24f4649..4b42d5cddc6 100644 --- a/typings/webpack-types.d.ts +++ b/typings/webpack-types.d.ts @@ -173,6 +173,7 @@ declare interface StatsOptions { modulesSort?: string; // Add public path information publicPath?: boolean; + performance?: boolean; providedExports?: boolean; // Add information about the reasons why modules are included reasons: boolean; @@ -240,6 +241,13 @@ declare interface FilenameTemplateInfo { shortIdentifier: string; } +declare interface PerformanceOptions { + maxAssetSize?: number + maxInitialChunkSize?: number + hints? + errorOnHint?: boolean +} + declare interface WebpackOptions { /** Set the value of require.amd and define.amd. */ amd?: { [moduleName: string]: boolean }; @@ -272,6 +280,7 @@ declare interface WebpackOptions { plugins?: Tapable.Plugin; /** Capture timing information for each module. */ profile?: boolean; + performance?: PerformanceOptions; /** Used for recordsInputPath and recordsOutputPath */ recordsPath?: string; /** Load compiler state from a json file. */ @@ -472,6 +481,7 @@ declare interface ParserOptions { requireJs: boolean; node: false | NodeOption; ident: string + import: boolean } declare interface WatchFileSystem {