├── mini-pack ├── readme.md ├── cli.js ├── webpack-sources │ ├── RawSource.js │ ├── OriginalSource.js │ ├── CachedSource.js │ ├── PrefixSource.js │ ├── ConcatSource.js │ └── ReplaceSource.js ├── lib │ ├── node │ │ └── NodeEnvironmentPlugin.js │ ├── dependencies │ │ ├── DependencyReference.js │ │ ├── CommonJsPlugin.js │ │ ├── CommonJsRequireDependency.js │ │ └── ModuleDependencyTemplateAsId.js │ ├── Stats.js │ ├── FunctionModulePlugin.js │ ├── web │ │ ├── JsonpChunkTemplatePlugin.js │ │ ├── JsonpMainTemplatePlugin.js │ │ └── JsonpTemplatePlugin.js │ ├── ChunkTemplate.js │ ├── NamedChunksPlugin.js │ ├── TemplatedPathPlugin.js │ ├── EntryOptionPlugin.js │ ├── NamedModulesPlugin.js │ ├── util │ │ └── createHash.js │ ├── Chunk.js │ ├── webpack.js │ ├── JavascriptGenerator.js │ ├── SingleEntryPlugin.js │ ├── ResolverFactory.js │ ├── Parser.js │ ├── Entrypoint.js │ ├── FunctionModuleTemplatePlugin.js │ ├── ModuleTemplate.js │ ├── Template.js │ ├── JavascriptModulesPlugin.js │ ├── RuleSet.js │ ├── WebpackOptionsApply.js │ ├── Compiler.js │ ├── NormalModuleFactory.js │ ├── NormalModule.js │ ├── buildChunkGraph.js │ ├── MainTemplate.js │ ├── Compilation.js │ └── WebpackOptionsDefaulter.js ├── enhanced-resolve │ ├── DescriptionFileUtils.js │ ├── ResultPlugin.js │ ├── AliasFieldPlugin.js │ ├── ParsePlugin.js │ ├── JoinRequestPlugin.js │ ├── UnsafeCachePlugin.js │ ├── ResolverFactory.js │ ├── DescriptionFilePlugin.js │ └── Resolver.js └── loader-runner │ └── LoaderRunner.js ├── src ├── default.css ├── default.less ├── a.js ├── 2.jpg ├── 2.png ├── index.js ├── common.less ├── test.js ├── index.less ├── index.css ├── common.css └── temp.js ├── debugger.js ├── webpack-main.png ├── loaders ├── css-loader │ ├── runtime │ │ ├── getUrl.js │ │ └── api.js │ ├── utils.js │ ├── plugins │ │ ├── postcss-import-parser.js │ │ └── postcss-url-parser.js │ ├── index.js │ └── readme.md ├── loader2.js ├── file-loader │ └── index.js ├── url-loader │ └── index.js ├── babel-loader │ └── index.js ├── less-loader │ └── index.js ├── loader1.js ├── style-loader │ ├── runtime │ │ └── injectStylesIntoStyleTag.js │ └── index.js └── loader指南.md ├── .gitignore ├── plugins ├── MiniCssExtractPlugin │ └── loader.js ├── plugin1.js └── MediaExtractPlugin │ ├── postcss-plugins │ └── postcss-media-parser.js │ ├── index.js │ └── readme.md ├── index.html ├── package.json ├── webpack.config.js └── README.md /mini-pack/readme.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/default.css: -------------------------------------------------------------------------------- 1 | span{ 2 | color: green; 3 | } -------------------------------------------------------------------------------- /src/default.less: -------------------------------------------------------------------------------- 1 | span{ 2 | color: blue; 3 | } -------------------------------------------------------------------------------- /src/a.js: -------------------------------------------------------------------------------- 1 | console.log('a.js') 2 | module.exports = 'a.js file' -------------------------------------------------------------------------------- /debugger.js: -------------------------------------------------------------------------------- 1 | debugger; 2 | require('./node_modules/webpack/bin/webpack') 3 | -------------------------------------------------------------------------------- /src/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizuncong/mini-webpack/HEAD/src/2.jpg -------------------------------------------------------------------------------- /src/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizuncong/mini-webpack/HEAD/src/2.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const test = require('./test.js') 2 | 3 | console.log(test) 4 | -------------------------------------------------------------------------------- /webpack-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizuncong/mini-webpack/HEAD/webpack-main.png -------------------------------------------------------------------------------- /src/common.less: -------------------------------------------------------------------------------- 1 | img { 2 | height: 400px; 3 | overflow: hidden; 4 | } 5 | @import './default.less'; -------------------------------------------------------------------------------- /loaders/css-loader/runtime/getUrl.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (url) { 4 | return url; 5 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | node_modules 4 | 5 | dist 6 | dll 7 | 8 | .npm 9 | stats.json 10 | 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | const a = require('./a.js') 2 | 3 | console.log('a==', a) 4 | console.log('test.js') 5 | module.exports = 'test.js file' -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | @color: red; 2 | @import './common.less'; 3 | body{ 4 | background: @color; 5 | background: url('./2.jpg'); 6 | } -------------------------------------------------------------------------------- /plugins/MiniCssExtractPlugin/loader.js: -------------------------------------------------------------------------------- 1 | function loader(){ 2 | 3 | } 4 | 5 | loader.pitch = function(request){ 6 | 7 | } 8 | 9 | module.exports = loader -------------------------------------------------------------------------------- /mini-pack/cli.js: -------------------------------------------------------------------------------- 1 | const webpack = require('./lib/webpack.js') 2 | const options = require('../webpack.config') 3 | 4 | const compiler = webpack(options) 5 | compiler.run((err, stat) => { 6 | stat.toJson(); 7 | }) -------------------------------------------------------------------------------- /mini-pack/webpack-sources/RawSource.js: -------------------------------------------------------------------------------- 1 | 2 | class RawSource { 3 | constructor(value) { 4 | this._value = value; 5 | } 6 | 7 | source() { 8 | return this._value; 9 | } 10 | } 11 | 12 | module.exports = RawSource; 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mini Webpack 6 | 7 | 8 |
container
9 |
我是红色
10 | 11 | -------------------------------------------------------------------------------- /mini-pack/lib/node/NodeEnvironmentPlugin.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | class NodeEnvironmentPlugin { 3 | apply(compiler){ 4 | compiler.inputFileSystem = fs; 5 | compiler.outputFileSystem = fs 6 | } 7 | } 8 | 9 | module.exports = NodeEnvironmentPlugin -------------------------------------------------------------------------------- /loaders/loader2.js: -------------------------------------------------------------------------------- 1 | function loader(source, ...rest){ 2 | console.log('=====loader2=========') 3 | debugger; 4 | return source 5 | } 6 | 7 | // loader.pitch = function loader(request) { 8 | // console.log('loader 2 pitch', request) 9 | // return 'test' 10 | // }; 11 | 12 | module.exports = loader -------------------------------------------------------------------------------- /mini-pack/lib/dependencies/DependencyReference.js: -------------------------------------------------------------------------------- 1 | 2 | class DependencyReference { 3 | constructor(module, importedNames, weak = false, order = NaN) { 4 | this.module = module; 5 | this.importedNames = importedNames; 6 | this.weak = !!weak; 7 | this.order = order; 8 | } 9 | } 10 | 11 | module.exports = DependencyReference; 12 | -------------------------------------------------------------------------------- /mini-pack/webpack-sources/OriginalSource.js: -------------------------------------------------------------------------------- 1 | 2 | class OriginalSource { 3 | constructor(value, name) { 4 | this._value = value; 5 | this._name = name; 6 | } 7 | source() { 8 | return this._value; 9 | } 10 | updateHash(hash) { 11 | hash.update(this._value); 12 | } 13 | } 14 | 15 | 16 | module.exports = OriginalSource; 17 | -------------------------------------------------------------------------------- /loaders/file-loader/index.js: -------------------------------------------------------------------------------- 1 | const loaderUtils = require('loader-utils') 2 | function loader(source){ 3 | const filename = loaderUtils.interpolateName(this, '[name].[hash].[ext]', { content: source }) 4 | this.emitFile(filename, source) 5 | return `module.exports="${filename}"` 6 | } 7 | 8 | loader.raw = true // 以二进制流读取文件 9 | module.exports = loader -------------------------------------------------------------------------------- /mini-pack/webpack-sources/CachedSource.js: -------------------------------------------------------------------------------- 1 | 2 | class CachedSource { 3 | constructor(source) { 4 | this._source = source; 5 | this._cachedSource = undefined; 6 | this._cachedSize = undefined; 7 | this._cachedMaps = {}; 8 | } 9 | 10 | source() { 11 | return this._source.source(); 12 | } 13 | } 14 | 15 | module.exports = CachedSource; 16 | -------------------------------------------------------------------------------- /mini-pack/enhanced-resolve/DescriptionFileUtils.js: -------------------------------------------------------------------------------- 1 | function cdUp(directory) { 2 | if (directory === "/") return null; 3 | const i = directory.lastIndexOf("/"), 4 | j = directory.lastIndexOf("\\"); 5 | const p = i < 0 ? j : j < 0 ? i : i < j ? j : i; 6 | if (p < 0) return null; 7 | return directory.substr(0, p || 1); 8 | } 9 | 10 | exports.cdUp = cdUp; 11 | -------------------------------------------------------------------------------- /mini-pack/lib/Stats.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Stats { 4 | constructor(compilation) { 5 | this.compilation = compilation; 6 | this.hash = compilation.hash; 7 | this.startTime = undefined; 8 | this.endTime = undefined; 9 | } 10 | 11 | toJson(options, forToString) { 12 | console.log('自定义webpack打包完成!!!') 13 | } 14 | } 15 | 16 | module.exports = Stats; 17 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import './common.css'; 2 | 3 | .container{ 4 | color: red; 5 | background-color: green; 6 | } 7 | 8 | body{ 9 | color: blue; 10 | background: url('./2.png') no-repeat; 11 | } 12 | 13 | 14 | 15 | 16 | @media (max-width: 960px) { 17 | .container{ 18 | color: red; 19 | background-color: blue; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /mini-pack/enhanced-resolve/ResultPlugin.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = class ResultPlugin { 3 | constructor(source) { 4 | this.source = source; 5 | } 6 | 7 | apply(resolver) { 8 | this.source.tapAsync( 9 | "ResultPlugin", 10 | (request, resolverContext, callback) => { 11 | const obj = Object.assign({}, request); 12 | callback(null, obj); 13 | } 14 | ); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /plugins/plugin1.js: -------------------------------------------------------------------------------- 1 | 2 | class MyPlugin1 { 3 | constructor(){ 4 | // console.log('MyPlugin1 construct=====') 5 | } 6 | apply(compiler){ 7 | // console.log('Myplugin1 apply======') 8 | compiler.hooks.emit.tapPromise('MediaExtractPlugin', async (compilation) => { 9 | console.log(); 10 | }) 11 | } 12 | } 13 | 14 | module.exports = MyPlugin1 -------------------------------------------------------------------------------- /mini-pack/lib/FunctionModulePlugin.js: -------------------------------------------------------------------------------- 1 | 2 | const FunctionModuleTemplatePlugin = require("./FunctionModuleTemplatePlugin"); 3 | 4 | class FunctionModulePlugin { 5 | apply(compiler) { 6 | compiler.hooks.compilation.tap("FunctionModulePlugin", compilation => { 7 | new FunctionModuleTemplatePlugin().apply( 8 | compilation.moduleTemplates.javascript 9 | ); 10 | }); 11 | } 12 | } 13 | 14 | module.exports = FunctionModulePlugin; 15 | -------------------------------------------------------------------------------- /mini-pack/lib/web/JsonpChunkTemplatePlugin.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | class JsonpChunkTemplatePlugin { 4 | apply(chunkTemplate) { 5 | chunkTemplate.hooks.hash.tap("JsonpChunkTemplatePlugin", hash => { 6 | hash.update("JsonpChunkTemplatePlugin"); 7 | hash.update("4"); 8 | hash.update(`${chunkTemplate.outputOptions.jsonpFunction}`); 9 | hash.update(`${chunkTemplate.outputOptions.globalObject}`); 10 | }); 11 | } 12 | } 13 | module.exports = JsonpChunkTemplatePlugin; 14 | -------------------------------------------------------------------------------- /loaders/url-loader/index.js: -------------------------------------------------------------------------------- 1 | const loaderUtils = require('loader-utils') 2 | const mime = require('mime') 3 | function loader(source){ 4 | const { limit } = loaderUtils.getOptions(this) || { limit: 20000} 5 | if(limit && limit > source.length){ 6 | return `module.exports="data:${mime.getType(this.resourcePath)};base64,${source.toString('base64')}"` 7 | } 8 | return require('../file-loader').call(this, source) 9 | } 10 | 11 | loader.raw = true 12 | module.exports = loader -------------------------------------------------------------------------------- /mini-pack/lib/web/JsonpMainTemplatePlugin.js: -------------------------------------------------------------------------------- 1 | const Template = require('../Template') 2 | class JsonpMainTemplatePlugin { 3 | apply(mainTemplate) { 4 | mainTemplate.hooks.hash.tap("JsonpMainTemplatePlugin", hash => { 5 | hash.update("jsonp"); 6 | hash.update("6"); 7 | }); 8 | 9 | mainTemplate.hooks.localVars.tap( 10 | "JsonpMainTemplatePlugin", 11 | (source, chunk, hash) => { 12 | return source; 13 | } 14 | ); 15 | } 16 | } 17 | module.exports = JsonpMainTemplatePlugin; 18 | -------------------------------------------------------------------------------- /src/common.css: -------------------------------------------------------------------------------- 1 | div { 2 | height: 200px; 3 | color: red; 4 | overflow: hidden; 5 | background-color: aqua; 6 | margin-bottom: 20px; 7 | } 8 | 9 | @media (max-width: 480px) { 10 | div { 11 | background-color: lightgreen; 12 | } 13 | div { 14 | color: green; 15 | } 16 | } 17 | 18 | @media (max-width: 960px) { 19 | div { 20 | background-color: aliceblue; 21 | } 22 | div { 23 | color: blue; 24 | } 25 | } 26 | 27 | 28 | -------------------------------------------------------------------------------- /mini-pack/webpack-sources/PrefixSource.js: -------------------------------------------------------------------------------- 1 | 2 | var REPLACE_REGEX = /\n(?=.|\s)/g; 3 | 4 | class PrefixSource { 5 | constructor(prefix, source) { 6 | this._source = source; 7 | this._prefix = prefix; 8 | } 9 | source() { 10 | var node = typeof this._source === "string" ? this._source : this._source.source(); 11 | var prefix = this._prefix; 12 | const source = prefix + node.replace(REPLACE_REGEX, "\n" + prefix); 13 | return source 14 | } 15 | } 16 | 17 | 18 | module.exports = PrefixSource; 19 | -------------------------------------------------------------------------------- /src/temp.js: -------------------------------------------------------------------------------- 1 | // const img = require('./2.jpg') 2 | // const css = require('./index.css') 3 | // console.log('css...module', css) 4 | // const ele = document.createElement('div') 5 | // ele.innerHTML = 'hello world'; 6 | // document.body.appendChild(ele) 7 | 8 | // const styleEle = document.createElement('style') 9 | // styleEle.appendChild(document.createTextNode(css.default.toString())) 10 | 11 | // document.head.appendChild(styleEle) 12 | 13 | const test = require('./test.js') 14 | 15 | console.log(test) 16 | -------------------------------------------------------------------------------- /mini-pack/lib/ChunkTemplate.js: -------------------------------------------------------------------------------- 1 | const { 2 | Tapable, 3 | SyncHook, 4 | SyncBailHook, 5 | SyncWaterfallHook, 6 | AsyncSeriesHook 7 | } = require("tapable"); 8 | module.exports = class ChunkTemplate extends Tapable { 9 | constructor(outputOptions) { 10 | super(); 11 | this.outputOptions = outputOptions || {}; 12 | this.hooks = { 13 | hash: new SyncHook(["hash"]), 14 | }; 15 | } 16 | 17 | updateHash(hash) { 18 | hash.update("ChunkTemplate"); 19 | hash.update("2"); 20 | this.hooks.hash.call(hash); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /loaders/babel-loader/index.js: -------------------------------------------------------------------------------- 1 | const babel = require('@babel/core') 2 | const loaderUtils = require('loader-utils') 3 | function loader(source){ 4 | const options = loaderUtils.getOptions(this); 5 | const cb = this.async(); 6 | babel.transform(source, { 7 | ...options, 8 | sourceMap: true, 9 | filename: this.resourcePath.split('/').pop(), // 文件名,也用于sourcemap,如果不设置这个选项,那么在浏览器控制台sources tab下看到源码文件名称为unknown 10 | }, function(err, result){ 11 | cb(err, result.code, result.map) 12 | }) 13 | } 14 | 15 | module.exports = loader -------------------------------------------------------------------------------- /mini-pack/lib/NamedChunksPlugin.js: -------------------------------------------------------------------------------- 1 | 2 | class NamedChunksPlugin { 3 | nameResolver(chunk) { 4 | return chunk.name || null; 5 | } 6 | 7 | apply(compiler) { 8 | compiler.hooks.compilation.tap("NamedChunksPlugin", compilation => { 9 | compilation.hooks.beforeChunkIds.tap("NamedChunksPlugin", chunks => { 10 | for (const chunk of chunks) { 11 | if (chunk.id === null) { 12 | chunk.id = this.nameResolver(chunk); 13 | } 14 | } 15 | }); 16 | }); 17 | } 18 | } 19 | 20 | module.exports = NamedChunksPlugin; 21 | -------------------------------------------------------------------------------- /mini-pack/lib/web/JsonpTemplatePlugin.js: -------------------------------------------------------------------------------- 1 | 2 | const JsonpMainTemplatePlugin = require("./JsonpMainTemplatePlugin"); 3 | const JsonpChunkTemplatePlugin = require('./JsonpChunkTemplatePlugin') 4 | class JsonpTemplatePlugin { 5 | apply(compiler) { 6 | compiler.hooks.thisCompilation.tap("JsonpTemplatePlugin", compilation => { 7 | new JsonpMainTemplatePlugin().apply(compilation.mainTemplate); 8 | new JsonpChunkTemplatePlugin().apply(compilation.chunkTemplate); 9 | }); 10 | } 11 | } 12 | 13 | module.exports = JsonpTemplatePlugin; 14 | -------------------------------------------------------------------------------- /mini-pack/lib/dependencies/CommonJsPlugin.js: -------------------------------------------------------------------------------- 1 | 2 | const CommonJsRequireDependency = require("./CommonJsRequireDependency"); 3 | 4 | class CommonJsPlugin { 5 | constructor(options) { 6 | this.options = options; 7 | } 8 | 9 | apply(compiler) { 10 | compiler.hooks.compilation.tap( 11 | "CommonJsPlugin", 12 | (compilation, { normalModuleFactory }) => { 13 | compilation.dependencyFactories.set( 14 | CommonJsRequireDependency, 15 | normalModuleFactory 16 | ); 17 | } 18 | ); 19 | } 20 | } 21 | module.exports = CommonJsPlugin; 22 | -------------------------------------------------------------------------------- /mini-pack/lib/TemplatedPathPlugin.js: -------------------------------------------------------------------------------- 1 | const REGEXP_NAME = /\[name\]/gi; 2 | 3 | class TemplatedPathPlugin { 4 | apply(compiler) { 5 | compiler.hooks.compilation.tap("TemplatedPathPlugin", compilation => { 6 | const mainTemplate = compilation.mainTemplate; 7 | mainTemplate.hooks.assetPath.tap( 8 | "TemplatedPathPlugin", 9 | (path, data, assetInfo) => { 10 | if(data && data.chunk){ 11 | return path.replace(REGEXP_NAME, data.chunk.name) 12 | } 13 | return path; 14 | } 15 | ); 16 | }); 17 | } 18 | } 19 | 20 | module.exports = TemplatedPathPlugin; 21 | -------------------------------------------------------------------------------- /loaders/css-loader/runtime/api.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // cssWithMappingToString就是一个函数: function(item){ return item[1] } 4 | module.exports = function (cssWithMappingToString) { 5 | var list = []; 6 | 7 | list.toString = function toString() { 8 | return this.map(function (item) { 9 | var content = cssWithMappingToString(item); 10 | return content; 11 | }).join(""); 12 | }; 13 | 14 | list.i = function (modules) { 15 | for (var _i = 0; _i < modules.length; _i++) { 16 | var item = [].concat(modules[_i]); 17 | list.push(item); 18 | } 19 | }; 20 | 21 | return list; 22 | }; -------------------------------------------------------------------------------- /mini-pack/lib/EntryOptionPlugin.js: -------------------------------------------------------------------------------- 1 | const SingleEntryPlugin = require("./SingleEntryPlugin") 2 | 3 | // 在WebpackOptionsApply.js中初始化 4 | // webpack可以打包多页和单页应用,通过在webpack.config中配置entry 5 | // EntryOptionPlugin主要是处理webpack.config中的entry配置, 6 | // 然后为每个entry初始化一个SingleEntryPlugin实例 7 | class EntryOptionPlugin{ 8 | apply(compiler){ 9 | compiler.hooks.entryOption.tap('EntryOptionPlugin', (context, entry)=>{ 10 | if(typeof entry === 'string'){ 11 | new SingleEntryPlugin(context, entry,'main').apply(compiler) 12 | } 13 | }) 14 | } 15 | } 16 | 17 | module.exports = EntryOptionPlugin -------------------------------------------------------------------------------- /mini-pack/lib/NamedModulesPlugin.js: -------------------------------------------------------------------------------- 1 | class NamedModulesPlugin { 2 | constructor(options) { 3 | this.options = options || {}; 4 | } 5 | 6 | apply(compiler) { 7 | compiler.hooks.compilation.tap("NamedModulesPlugin", compilation => { 8 | compilation.hooks.beforeModuleIds.tap("NamedModulesPlugin", modules => { 9 | const context = this.options.context || compiler.options.context; 10 | for (const module of modules) { 11 | if (module.id === null && module.libIdent) { 12 | module.id = module.libIdent({ context }); 13 | } 14 | } 15 | }); 16 | }); 17 | } 18 | } 19 | 20 | module.exports = NamedModulesPlugin; 21 | -------------------------------------------------------------------------------- /mini-pack/lib/util/createHash.js: -------------------------------------------------------------------------------- 1 | class BulkUpdateDecorator { 2 | constructor(hash) { 3 | this.hash = hash; 4 | this.buffer = ""; 5 | } 6 | update(data, inputEncoding) { 7 | this.buffer += data; 8 | return this; 9 | } 10 | digest(encoding) { 11 | if (this.buffer.length > 0) { 12 | this.hash.update(this.buffer); 13 | } 14 | var digestResult = this.hash.digest(encoding); 15 | return typeof digestResult === "string" 16 | ? digestResult 17 | : digestResult.toString(); 18 | } 19 | } 20 | 21 | module.exports = (algorithm) => { 22 | return new BulkUpdateDecorator(require("crypto").createHash(algorithm)); 23 | }; 24 | -------------------------------------------------------------------------------- /mini-pack/webpack-sources/ConcatSource.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | class ConcatSource { 4 | constructor() { 5 | this.children = []; 6 | } 7 | 8 | add(item) { 9 | if(item instanceof ConcatSource) { 10 | var children = item.children; 11 | for(var j = 0; j < children.length; j++) 12 | this.children.push(children[j]); 13 | } else { 14 | this.children.push(item); 15 | } 16 | } 17 | source() { 18 | let source = ""; 19 | const children = this.children; 20 | for(let i = 0; i < children.length; i++) { 21 | const child = children[i]; 22 | source += typeof child === "string" ? child : child.source(); 23 | } 24 | return source; 25 | } 26 | } 27 | 28 | 29 | module.exports = ConcatSource; 30 | -------------------------------------------------------------------------------- /mini-pack/enhanced-resolve/AliasFieldPlugin.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | module.exports = class AliasFieldPlugin { 3 | constructor(source, field, target) { 4 | this.source = source; 5 | this.field = field; 6 | this.target = target; 7 | } 8 | 9 | apply(resolver) { 10 | const target = resolver.ensureHook(this.target); 11 | resolver 12 | .getHook(this.source) 13 | .tapAsync("AliasFieldPlugin", (request, resolveContext, callback) => { 14 | request.__innerRequest_request = request.request; 15 | request.__innerRequest_relativePath = request.relativePath; 16 | request.__innerRequest = resolver.join(request.relativePath, request.request) 17 | callback(); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /mini-pack/lib/dependencies/CommonJsRequireDependency.js: -------------------------------------------------------------------------------- 1 | 2 | const DependencyReference = require('./DependencyReference') 3 | class CommonJsRequireDependency { 4 | constructor(request, range, reqRange) { 5 | this.request = request; 6 | this.userRequest = request; 7 | this.range = range || []; 8 | this.reqRange = reqRange || []; // webpack源码中为require构造了一个独立的依赖,这里为了方便 9 | // 我直接存储在CommonJsRequireDependency中 10 | this.module = null; 11 | } 12 | getReference() { 13 | if (!this.module) return null; 14 | return new DependencyReference(this.module, true, this.weak); 15 | } 16 | updateHash(hash) { 17 | hash.update((this.module && this.module.id) + ""); 18 | } 19 | } 20 | 21 | 22 | module.exports = CommonJsRequireDependency; 23 | -------------------------------------------------------------------------------- /loaders/less-loader/index.js: -------------------------------------------------------------------------------- 1 | const loaderUtils = require('loader-utils') 2 | const less = require('less') 3 | function loader(source){ 4 | const options = loaderUtils.getOptions(this) || {} 5 | const done = this.async(); 6 | // 必须的,不然无法解析index.less中 @import "./common.less" 7 | options.filename = this.resource; // '/Users/lizc/Documents/MYProjects/mini-webpack/src/index.less' 8 | less.render(source, options, (err, { css, imports } = {} ) => { 9 | if(err){ 10 | done(err) 11 | return; 12 | } 13 | console.log('imports...', imports) 14 | // 必须的,收集.less文件中@import引入的文件 15 | imports.forEach(this.addDependency, this); 16 | done(err, css) 17 | }) 18 | } 19 | 20 | module.exports = loader -------------------------------------------------------------------------------- /mini-pack/enhanced-resolve/ParsePlugin.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = class ParsePlugin { 3 | constructor(source, target) { 4 | this.source = source; // newResolve 钩子 5 | this.target = target; // parsedResolve 钩子 6 | } 7 | 8 | apply(resolver) { 9 | const target = resolver.ensureHook(this.target); 10 | resolver 11 | .getHook(this.source) 12 | .tapAsync("ParsePlugin", (request, resolveContext, callback) => { 13 | // 解析request.request文件路径,判断是否是目录、文件或者模块。 14 | const parsed = { 15 | directory: false, 16 | file: false, 17 | module: false, 18 | query: '', 19 | request: request.request 20 | } 21 | const obj = Object.assign({}, request, parsed) 22 | resolver.doResolve(target, obj, resolveContext, callback) 23 | }); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /mini-pack/lib/dependencies/ModuleDependencyTemplateAsId.js: -------------------------------------------------------------------------------- 1 | 2 | const Template = require('../Template') 3 | class ModuleDependencyTemplateAsId { 4 | apply(dep, source, runtime) { 5 | const content = `${Template.toComment(dep.request)}${JSON.stringify(dep.module.id)}` 6 | // 在webpack源码中,ReplaceSource.replacements数组中存放的是 7 | // 要替换的标志。以:const test = require('./test.js')\n\nconsole.log(test)\n为例, 8 | // 数组第一项存放的是'./test.js'的起始位置,这个会被替换成content 9 | // 数组第二项存放的是require的起始位置,require会被替换成__webpack_require__ 10 | source.replace(dep.range[0], dep.range[1] - 1, content); 11 | source.replace(dep.reqRange[0], dep.reqRange[1] - 1, '__webpack_require__'); 12 | } 13 | } 14 | module.exports = ModuleDependencyTemplateAsId; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-webpack", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server", 8 | "build": "webpack", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@babel/core": "^7.15.8", 16 | "@babel/preset-env": "^7.15.8", 17 | "css-loader": "^5.2.7", 18 | "html-webpack-plugin": "^4.5.2", 19 | "less": "^4.1.2", 20 | "less-loader": "^5.0.0", 21 | "mime": "^2.5.2", 22 | "style-loader": "^2.0.0", 23 | "webpack": "^4.46.0", 24 | "webpack-cli": "^3.3.12", 25 | "webpack-dev-server": "^3.11.2" 26 | }, 27 | "devDependencies": { 28 | "mini-css-extract-plugin": "^0.9.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /mini-pack/lib/Chunk.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A Chunk is a unit of encapsulation for Modules. 3 | * Chunks are "rendered" into bundles that get emitted when the build completes. 4 | */ 5 | class Chunk { 6 | constructor(name) { 7 | this.id = null; 8 | this.name = name; 9 | this.entryModule = undefined; 10 | this._groups = new Set(); 11 | this._modules = new Set(); 12 | } 13 | addGroup(chunkGroup) { 14 | this._groups.add(chunkGroup); 15 | return true; 16 | } 17 | addModule(module) { 18 | this._modules.add(module); 19 | return true; 20 | } 21 | getModules(){ 22 | return Array.from(this._modules) 23 | } 24 | updateHash(hash) { 25 | hash.update(`${this.id} `); 26 | hash.update(`${this.name || ""} `); 27 | for (const m of this._modules) { 28 | hash.update(m.hash); 29 | } 30 | } 31 | } 32 | 33 | 34 | module.exports = Chunk; 35 | -------------------------------------------------------------------------------- /mini-pack/enhanced-resolve/JoinRequestPlugin.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path') 3 | module.exports = class JoinRequestPlugin { 4 | constructor(source, target) { 5 | this.source = source; 6 | this.target = target; 7 | } 8 | 9 | apply(resolver) { 10 | const target = resolver.ensureHook(this.target); 11 | resolver 12 | .getHook(this.source) 13 | .tapAsync("JoinRequestPlugin", (request, resolveContext, callback) => { 14 | const obj = Object.assign({}, request, { 15 | // webpack使用的是resolver.join,其实就是memory-fs/lib下的join方法 16 | path: path.join(request.path, request.request), 17 | // relativePath: request.request, 18 | relativePath: 19 | request.relativePath && 20 | resolver.join(request.relativePath, request.request), 21 | request: undefined 22 | }); 23 | resolver.doResolve(target, obj, resolveContext, callback); 24 | }); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /mini-pack/enhanced-resolve/UnsafeCachePlugin.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = class UnsafeCachePlugin { 3 | constructor(source, target) { 4 | this.source = source; // 当前钩子 resolve 5 | this.target = target; // 下一个钩子 'newResolve' 6 | this.cache = {} 7 | } 8 | 9 | apply(resolver) { 10 | const target = resolver.ensureHook(this.target); 11 | resolver 12 | .getHook(this.source) 13 | .tapAsync("UnsafeCachePlugin", (request, resolveContext, callback) => { 14 | const cacheEntry = this.cache[request.request] 15 | if(cacheEntry){ 16 | return callback(null, cacheEntry) 17 | } 18 | // 如果缓存没有,则交由newResove钩子继续进行解析 19 | resolver.doResolve( 20 | target, 21 | request, 22 | resolveContext, 23 | (err, result) => { 24 | if (err) return callback(err); 25 | if (result) return callback(null, (this.cache[request.request] = result)); 26 | callback(); 27 | } 28 | ) 29 | }); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /mini-pack/lib/webpack.js: -------------------------------------------------------------------------------- 1 | const NodeEnvironmentPlugin = require('./node/NodeEnvironmentPlugin') 2 | const Compiler = require('./Compiler') 3 | const WebpackOptionsApply = require('./WebpackOptionsApply') 4 | const WebpackOptionsDefaulter = require('./WebpackOptionsDefaulter') 5 | 6 | const webpack = options => { 7 | // 处理webpack options默认值 8 | options = new WebpackOptionsDefaulter().process(options) 9 | const compiler = new Compiler(options.context); 10 | compiler.options = options; 11 | // 给comlier设置读写文件系统,日志输出属性值 12 | new NodeEnvironmentPlugin().apply(compiler) 13 | // 注册插件 14 | if(options.plugins && Array.isArray(options.plugins)){ 15 | options.plugins.forEach(plugin => { 16 | plugin.apply(compiler) 17 | }); 18 | } 19 | 20 | // 主要是注册插件,其中最主要是给compiler.hooks.compilation注册插件 21 | compiler.options = new WebpackOptionsApply().process(options, compiler) 22 | 23 | return compiler 24 | } 25 | 26 | module.exports = webpack; 27 | -------------------------------------------------------------------------------- /loaders/loader1.js: -------------------------------------------------------------------------------- 1 | // function loader(source, ...rest){ 2 | // const callback = this.async(); 3 | // console.log('=====loader1=========') 4 | // console.log(source) 5 | // console.log(rest) 6 | // console.log('=====loader1=========') 7 | // setTimeout(() => { 8 | // // source = "import ___CSS_LOADER_API_IMPORT___ from \"../src/test.js\";\n" + source 9 | // // if(rest[0].sources[0] === 'index.js'){ 10 | // // source = "import ___CSS_LOADER_API_IMPORT___ from \"../src/index.css\";\n" + source 11 | // // } 12 | // callback(null, source) 13 | // }, 3000); 14 | // } 15 | 16 | // loader.pitch = function loader(request) { 17 | // console.log('loader 1 pitch', request) 18 | // }; 19 | // module.exports = loader 20 | 21 | 22 | function loader(source, ...rest){ 23 | console.log('=====loader1 执行了=========\n\n') 24 | debugger; 25 | return source 26 | } 27 | 28 | // loader.pitch = function loader(request) { 29 | // console.log('loader 1 pitch', request) 30 | // return 'test' 31 | // }; 32 | 33 | module.exports = loader -------------------------------------------------------------------------------- /loaders/style-loader/runtime/injectStylesIntoStyleTag.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function modulesToDom(list, options) { 4 | for (var i = 0; i < list.length; i++) { 5 | var item = list[i]; 6 | var obj = { 7 | css: item[1] 8 | }; 9 | addStyle(obj, options) 10 | } 11 | } 12 | 13 | function insertStyleElement(options) { 14 | var style = document.createElement('style'); 15 | var attributes = options.attributes || {}; 16 | 17 | Object.keys(attributes).forEach(function (key) { 18 | style.setAttribute(key, attributes[key]); 19 | }); 20 | 21 | // 查找head 22 | var target = document.querySelector(options.insert) 23 | 24 | target.appendChild(style); 25 | 26 | return style; 27 | } 28 | 29 | function applyToTag(style, options, obj) { 30 | var css = obj.css; 31 | style.appendChild(document.createTextNode(css)); 32 | } 33 | 34 | 35 | function addStyle(obj, options) { 36 | var style; 37 | var update; 38 | style = insertStyleElement(options); 39 | update = applyToTag.bind(null, style, options); 40 | 41 | update(obj); 42 | } 43 | 44 | module.exports = function (list, options) { 45 | modulesToDom(list, options); 46 | }; -------------------------------------------------------------------------------- /loaders/css-loader/utils.js: -------------------------------------------------------------------------------- 1 | exports.getImportCode = function(imports) { 2 | let code = ""; 3 | 4 | for (const item of imports) { 5 | const { importName, url } = item; 6 | code += `import ${importName} from ${url};\n`; 7 | } 8 | 9 | return code ? `// Imports\n${code}` : ""; 10 | } 11 | 12 | 13 | exports.getModuleCode = function(result, api, replacements) { 14 | let code = JSON.stringify(result.css); 15 | let beforeCode = `var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(function(i){return i[1]});\n`; 16 | 17 | for (const item of api) { 18 | beforeCode += `___CSS_LOADER_EXPORT___.i(${item.importName});\n`; 19 | } 20 | for (const item of replacements) { 21 | const { replacementName, importName } = item; 22 | beforeCode += `var ${replacementName} = ___CSS_LOADER_GET_URL_IMPORT___(${importName});\n`; 23 | code = code.replace(new RegExp(replacementName, "g"), () => `" + ${replacementName} + "`); 24 | } 25 | return `${beforeCode}// Module\n___CSS_LOADER_EXPORT___.push([module.id, ${code}, ""]);\n`; 26 | } 27 | 28 | exports.getExportCode = function() { 29 | let code = "// Exports\n"; 30 | 31 | code += `export default ___CSS_LOADER_EXPORT___;\n`; 32 | return code; 33 | } 34 | -------------------------------------------------------------------------------- /mini-pack/lib/JavascriptGenerator.js: -------------------------------------------------------------------------------- 1 | 2 | const ReplaceSource = require('../webpack-sources/ReplaceSource') 3 | const ModuleDependencyTemplateAsId = require('./dependencies/ModuleDependencyTemplateAsId') 4 | const moduleTemplate = new ModuleDependencyTemplateAsId() 5 | class JavascriptGenerator { 6 | constructor(){ 7 | 8 | } 9 | 10 | generate(module, dependencyTemplates, runtimeTemplate){ 11 | const originalSource = module.originalSource(); 12 | const source = new ReplaceSource(originalSource); 13 | this.sourceBlock( 14 | module, 15 | module, 16 | [], 17 | dependencyTemplates, 18 | source, 19 | runtimeTemplate 20 | ); 21 | return source 22 | } 23 | sourceBlock( 24 | module, 25 | block, 26 | availableVars, 27 | dependencyTemplates, 28 | source, 29 | runtimeTemplate 30 | ) { 31 | for (const dependency of block.dependencies) { 32 | this.sourceDependency( 33 | dependency, 34 | dependencyTemplates, 35 | source, 36 | runtimeTemplate 37 | ); 38 | } 39 | } 40 | sourceDependency(dependency, dependencyTemplates, source, runtimeTemplate) { 41 | moduleTemplate.apply(dependency, source, runtimeTemplate); 42 | } 43 | } 44 | 45 | module.exports = JavascriptGenerator; 46 | -------------------------------------------------------------------------------- /mini-pack/lib/SingleEntryPlugin.js: -------------------------------------------------------------------------------- 1 | class SingleEntryDependency { 2 | constructor(request) { 3 | this.module = null; 4 | this.weak = false; 5 | this.optional = false; 6 | this.loc = undefined; 7 | this.request = request; 8 | this.userRequest = request; 9 | } 10 | } 11 | 12 | // 在EntryOptionPlugin中初始化 13 | class SingleEntryPlugin{ 14 | constructor(context, entry, name){ 15 | this.context = context; 16 | this.entry = entry // 入口文件 17 | this.name = name // 打包后的dist文件名称,默认main.js 18 | } 19 | apply(compiler){ 20 | // 在compiler.newCompilation方法中触发 21 | compiler.hooks.compilation.tap( 22 | "SingleEntryPlugin", 23 | (compilation, { normalModuleFactory }) => { 24 | compilation.dependencyFactories.set( 25 | SingleEntryDependency, 26 | normalModuleFactory 27 | ); 28 | } 29 | ); 30 | compiler.hooks.make.tapAsync('SingleEntryPlugin', (compilation, callback) => { 31 | const { context, entry, name } = this; 32 | const dep = new SingleEntryDependency(entry); 33 | dep.loc = { name }; 34 | // 开始从入口文件进行递归编译 35 | compilation.addEntry(context, dep, name, callback) 36 | }) 37 | } 38 | } 39 | 40 | module.exports = SingleEntryPlugin -------------------------------------------------------------------------------- /mini-pack/loader-runner/LoaderRunner.js: -------------------------------------------------------------------------------- 1 | function createLoaderObject({loader}) { 2 | var obj = { 3 | path: loader, 4 | request: loader, 5 | query: "", 6 | options: null, 7 | ident: null, 8 | normal: null, 9 | pitch: null, 10 | raw: null, 11 | data: null, 12 | pitchExecuted: false, 13 | normalExecuted: false 14 | }; 15 | obj.request = loader; 16 | return obj; 17 | } 18 | exports.runLoaders = function runLoaders(options, callback) { 19 | const loaderContext = options.context || {}; 20 | const loaders = (options.loaders || []).map(createLoaderObject) 21 | let resourceBuffer = null 22 | loaders.forEach(loader => { 23 | loader.module = require(loader.path) 24 | }) 25 | options.readResource(options.resource, (err, buffer) => { 26 | resourceBuffer = buffer; 27 | const resourceStr = resourceBuffer.toString('utf-8'); 28 | let result = [resourceStr]; 29 | loaders.forEach(loader => { 30 | const fn = loader.module; 31 | result = [(function LOADER_EXECUTION() { 32 | return fn.apply(loaderContext, result); 33 | }())]; 34 | }) 35 | callback(null, { 36 | result, 37 | resourceBuffer, 38 | }) 39 | }) 40 | 41 | }; 42 | -------------------------------------------------------------------------------- /mini-pack/lib/ResolverFactory.js: -------------------------------------------------------------------------------- 1 | const { 2 | Tapable, 3 | SyncWaterfallHook, 4 | HookMap 5 | } = require("tapable"); 6 | const Factory = require("../enhanced-resolve/ResolverFactory"); 7 | 8 | 9 | module.exports = class ResolverFactory extends Tapable { 10 | constructor() { 11 | super(); 12 | this.hooks = { 13 | // 在WebpackOptionsApply.js中注册resolveOptions插件 14 | resolveOptions: new HookMap( 15 | () => new SyncWaterfallHook(["resolveOptions"]) 16 | ), 17 | }; 18 | } 19 | 20 | get(type, resolveOptions = {}){ 21 | const newResolver = this._create(type, resolveOptions); 22 | return newResolver; 23 | } 24 | // 根据type创建对应的resolver实例. 25 | // 在webpack中,解析模块,比如require('./test.js'),查找test.js的路径 26 | // 使用的就是enhanced-resolver实例。webpack支持解析不同的文件类型 27 | // 使用不同的resolveOptions选项,目前支持的type有 “loader” 和 “normal” 28 | _create(type, resolveOptions){ 29 | // 触发在WebpackOptionsApply.js中注册的resolveOptions插件执行 30 | const hook = this.hooks.resolveOptions.for(type); 31 | resolveOptions = hook.call(resolveOptions); 32 | const resolver = Factory.createResolver(resolveOptions) 33 | resolver.withOptions = options => { 34 | return resolver 35 | } 36 | return resolver 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /mini-pack/lib/Parser.js: -------------------------------------------------------------------------------- 1 | 2 | const { Tapable, } = require("tapable"); 3 | const babelParser = require('@babel/parser') 4 | const types = require('@babel/types') 5 | const generator = require('@babel/generator').default 6 | const traverse = require('@babel/traverse').default 7 | const CommonJsRequireDependency = require('./dependencies/CommonJsRequireDependency') 8 | class Parser extends Tapable { 9 | constructor(options = {}, sourceType = "auto") { 10 | super(); 11 | this.hooks = {}; 12 | this.options = options; 13 | this.sourceType = sourceType; 14 | this.scope = undefined; 15 | this.state = undefined; 16 | this.comments = undefined; 17 | } 18 | parse(source, initialState){ 19 | const ast = babelParser.parse(source) 20 | const { module } = initialState 21 | traverse(ast, { 22 | CallExpression: (nodePath) => { 23 | if(nodePath.node.callee.name === 'require'){ 24 | // 获取当前的节点对象 25 | const node = nodePath.node; 26 | const req = node.arguments[0] 27 | const callee = node.callee; 28 | const dep = new CommonJsRequireDependency(node.arguments[0].value, [req.start, req.end], [callee.start, callee.end]) 29 | module.dependencies.push(dep) 30 | } 31 | } 32 | }) 33 | return initialState; 34 | } 35 | } 36 | 37 | 38 | module.exports = Parser; 39 | -------------------------------------------------------------------------------- /loaders/style-loader/index.js: -------------------------------------------------------------------------------- 1 | const loaderUtils = require('loader-utils') 2 | const path = require('path') 3 | function loader(source){ 4 | console.log('style..loader', source) 5 | return source 6 | } 7 | 8 | loader.pitch = function loader(request) { 9 | const options = {} 10 | const insert = '"head"' 11 | const injectType = 'styleTag' 12 | const runtimeOptions = { 13 | injectType: options.injectType, 14 | attributes: options.attributes, 15 | insert: options.insert, 16 | base: options.base 17 | }; 18 | switch (injectType) { 19 | case 'styleTag': 20 | case 'singletonStyleTag': 21 | default: 22 | const isSingleton = injectType === 'singletonStyleTag'; 23 | const str = ` 24 | import api from ${loaderUtils.stringifyRequest(this, `!${path.join(__dirname, 'runtime/injectStylesIntoStyleTag.js')}`)}; 25 | import content from ${loaderUtils.stringifyRequest(this, `!!${request}`)}; 26 | 27 | var options = ${JSON.stringify(runtimeOptions)}; 28 | 29 | options.insert = ${insert}; 30 | options.singleton = ${isSingleton}; 31 | api(content, options); 32 | export default content.locals || {};`; 33 | console.log('my style loader =======\n', str) 34 | return str 35 | } 36 | } 37 | 38 | module.exports = loader -------------------------------------------------------------------------------- /mini-pack/webpack-sources/ReplaceSource.js: -------------------------------------------------------------------------------- 1 | 2 | class Replacement { 3 | constructor(start, end, content, insertIndex, name) { 4 | this.start = start; 5 | this.end = end; 6 | this.content = content; 7 | this.insertIndex = insertIndex; 8 | this.name = name; 9 | } 10 | } 11 | class ReplaceSource { 12 | constructor(source, name) { 13 | this._source = source; 14 | this._name = name; 15 | /** @type {Replacement[]} */ 16 | this.replacements = []; 17 | } 18 | replace(start, end, newValue, name) { 19 | this.replacements.push(new Replacement(start, end, newValue, this.replacements.length, name)); 20 | } 21 | source(options) { 22 | return this._replaceString(this._source.source()); 23 | } 24 | _splitString(str, position) { 25 | return position <= 0 ? ["", str] : [str.substr(0, position), str.substr(position)]; 26 | } 27 | _replaceString(str) { 28 | var result = [str]; 29 | this.replacements.forEach(function(repl) { 30 | var remSource = result.pop(); 31 | var splitted1 = this._splitString(remSource, Math.floor(repl.end + 1)); 32 | var splitted2 = this._splitString(splitted1[0], Math.floor(repl.start)); 33 | result.push(splitted1[1], repl.content, splitted2[0]); 34 | }, this); 35 | // write out result array in reverse order 36 | let resultStr = ""; 37 | for(let i = result.length - 1; i >= 0; --i) { 38 | resultStr += result[i]; 39 | } 40 | return resultStr; 41 | } 42 | } 43 | 44 | module.exports = ReplaceSource; 45 | -------------------------------------------------------------------------------- /mini-pack/lib/Entrypoint.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Entrypoint serves as an encapsulation primitive for chunks that are 4 | * a part of a single ChunkGroup. They represent all bundles that need to be loaded for a 5 | * single instance of a page. Multi-page application architectures will typically yield multiple Entrypoint objects 6 | * inside of the compilation, whereas a Single Page App may only contain one with many lazy-loaded chunks. 7 | */ 8 | class Entrypoint { 9 | constructor(name) { 10 | this.runtimeChunk = undefined; 11 | this.origins = []; 12 | this.chunks = []; 13 | // 索引,从上到下 14 | this._moduleIndices = new Map(); 15 | // 索引,从下到上 16 | this._moduleIndices2 = new Map(); 17 | } 18 | pushChunk(chunk) { 19 | this.chunks.push(chunk); 20 | return true; 21 | } 22 | addOrigin(module, loc, request) { 23 | this.origins.push({ 24 | module, 25 | loc, 26 | request 27 | }); 28 | } 29 | setModuleIndex(module, index) { 30 | this._moduleIndices.set(module, index); 31 | } 32 | getModuleIndex(module) { 33 | return this._moduleIndices.get(module); 34 | } 35 | setModuleIndex2(module, index) { 36 | this._moduleIndices2.set(module, index); 37 | } 38 | getModuleIndex2(module) { 39 | return this._moduleIndices2.get(module); 40 | } 41 | setRuntimeChunk(chunk) { 42 | this.runtimeChunk = chunk; 43 | } 44 | 45 | getRuntimeChunk() { 46 | return this.runtimeChunk || this.chunks[0]; 47 | } 48 | } 49 | 50 | module.exports = Entrypoint; 51 | -------------------------------------------------------------------------------- /mini-pack/lib/FunctionModuleTemplatePlugin.js: -------------------------------------------------------------------------------- 1 | 2 | const ConcatSource = require('../webpack-sources/ConcatSource') 3 | const Template = require("./Template"); 4 | 5 | class FunctionModuleTemplatePlugin { 6 | apply(moduleTemplate) { 7 | moduleTemplate.hooks.render.tap( 8 | "FunctionModuleTemplatePlugin", 9 | (moduleSource, module) => { 10 | const source = new ConcatSource(); 11 | const args = ['module', 'exports']; 12 | if(module.dependencies.length){ 13 | args.push("__webpack_require__"); 14 | } 15 | source.add("/***/ (function(" + args.join(", ") + ") {\n\n"); 16 | source.add(moduleSource); 17 | source.add("\n\n/***/ })"); 18 | return source; 19 | } 20 | ); 21 | moduleTemplate.hooks.package.tap( 22 | "FunctionModuleTemplatePlugin", 23 | (moduleSource, module) => { 24 | const source = new ConcatSource(); 25 | const req = module.id; 26 | const reqStr = req.replace(/\*\//g, "*_/"); 27 | const reqStrStar = "*".repeat(reqStr.length); 28 | source.add("/*!****" + reqStrStar + "****!*\\\n"); 29 | source.add(" !*** " + reqStr + " ***!\n"); 30 | source.add(" \\****" + reqStrStar + "****/\n"); 31 | source.add(Template.toComment("no static exports found") + "\n"); 32 | source.add(moduleSource); 33 | return source; 34 | } 35 | ); 36 | moduleTemplate.hooks.hash.tap("FunctionModuleTemplatePlugin", hash => { 37 | hash.update("FunctionModuleTemplatePlugin"); 38 | hash.update("2"); 39 | }); 40 | } 41 | } 42 | module.exports = FunctionModuleTemplatePlugin; 43 | -------------------------------------------------------------------------------- /plugins/MediaExtractPlugin/postcss-plugins/postcss-media-parser.js: -------------------------------------------------------------------------------- 1 | var valueParser = require('postcss-value-parser'); 2 | 3 | const plugin = (options = {}) => { 4 | return { 5 | postcssPlugin: "postcss-media-parser", 6 | 7 | prepare(result) { 8 | const parsedAtRules = []; 9 | return { 10 | AtRule: { 11 | media(atRule) { 12 | const parsedAtRule = { 13 | atRule, 14 | params: atRule.params, // (max-width: 480px) 15 | width: valueParser(atRule.params).nodes.map(n => n.nodes)[0][2].value.replace('px', ''), // 480px 16 | ruleStr: atRule.nodes.map(n => n.toString()).join('') 17 | }; 18 | parsedAtRules.push(parsedAtRule); 19 | } 20 | }, 21 | 22 | async OnceExit() { 23 | if (parsedAtRules.length === 0) { 24 | return; 25 | } 26 | const {mediaRules} = options; 27 | const widthMap = new Map(); 28 | 29 | for(let index = 0; index < parsedAtRules.length; index ++){ 30 | const { atRule, params, width, ruleStr } = parsedAtRules[index] 31 | atRule.remove(); 32 | let obj = widthMap.get(width); 33 | if(!obj){ 34 | obj = { 35 | params: params, 36 | width: width, 37 | ruleStr: ruleStr 38 | } 39 | } else { 40 | obj.ruleStr = obj.ruleStr + ruleStr 41 | } 42 | widthMap.set(width, obj); 43 | } 44 | 45 | [...widthMap].forEach(item => { 46 | mediaRules.push(item[1]) 47 | }) 48 | } 49 | 50 | }; 51 | } 52 | 53 | }; 54 | }; 55 | plugin.postcss = true 56 | 57 | module.exports = plugin; 58 | -------------------------------------------------------------------------------- /mini-pack/lib/ModuleTemplate.js: -------------------------------------------------------------------------------- 1 | const { Tapable, SyncWaterfallHook, SyncHook } = require("tapable"); 2 | module.exports = class ModuleTemplate extends Tapable { 3 | constructor(runtimeTemplate, type) { 4 | super(); 5 | this.runtimeTemplate = runtimeTemplate; 6 | this.type = type; 7 | this.hooks = { 8 | hash: new SyncHook(["hash"]), 9 | render: new SyncWaterfallHook([ 10 | "source", 11 | "module", 12 | "options", 13 | "dependencyTemplates" 14 | ]), 15 | package: new SyncWaterfallHook([ 16 | "source", 17 | "module", 18 | "options", 19 | "dependencyTemplates" 20 | ]), 21 | }; 22 | } 23 | 24 | updateHash(hash) { 25 | hash.update("1"); 26 | this.hooks.hash.call(hash); 27 | } 28 | 29 | // /***/ "./src/index.js": ====1 30 | // /*!**********************!*\ ====1 31 | // !*** ./src/index.js ***! ====1 32 | // \**********************/ ====1 33 | // /*! no static exports found */ =====1 34 | // /***/ (function(module, exports, __webpack_require__) { ====2 35 | 36 | // const test = __webpack_require__(/*! ./test.js */"./src/test.js") 37 | 38 | // console.log(test) 39 | 40 | 41 | // /***/ }) ====2 42 | render(module, dependencyTemplates, options) { 43 | // 生成源码主体,以上面注释的为例,===2之间的由module.source生成 44 | const moduleSource = module.source( 45 | dependencyTemplates, 46 | this.runtimeTemplate, 47 | this.type 48 | ); 49 | 50 | // ===2部分的代码由this.hooks.render生成 51 | // 在FunctionModuleTemplatePlugin.js中注册this.hooks.render钩子 52 | const moduleSourcePostRender = this.hooks.render.call( 53 | moduleSource, 54 | module, 55 | options, 56 | dependencyTemplates 57 | ); 58 | 59 | // ===1部分的代码由this.hooks.package生成 60 | // 在FunctionModuleTemplatePlugin.js中注册this.hooks.package钩子 61 | return this.hooks.package.call( 62 | moduleSourcePostRender, 63 | module, 64 | options, 65 | dependencyTemplates 66 | ); 67 | } 68 | 69 | }; 70 | -------------------------------------------------------------------------------- /mini-pack/enhanced-resolve/ResolverFactory.js: -------------------------------------------------------------------------------- 1 | const Resolver = require("./Resolver"); 2 | const UnsafeCachePlugin = require('./UnsafeCachePlugin') 3 | const ParsePlugin = require('./ParsePlugin') 4 | const DescriptionFilePlugin = require('./DescriptionFilePlugin') 5 | const AliasFieldPlugin = require('./AliasFieldPlugin') 6 | const JoinRequestPlugin = require('./JoinRequestPlugin') 7 | const ResultPlugin = require('./ResultPlugin') 8 | exports.createResolver = function(options) { 9 | let modules = ["node_modules"]; 10 | const descriptionFiles = ["package.json"]; 11 | const plugins = []; 12 | let mainFields = options.mainFields; // ["loader", "main"]; 13 | const aliasFields = options.aliasFields || []; 14 | const mainFiles = options.mainFiles; // ["index"] 15 | const preferAbsolute = options.preferAbsolute || false; 16 | let extensions = options.extensions; // ['.js', '.json'] 17 | const cachePredicate = function() { return true } 18 | const fileSystem = options.fileSystem; 19 | const resolver = new Resolver(fileSystem) 20 | 21 | // 为resolver增加钩子 22 | resolver.ensureHook("resolve"); 23 | resolver.ensureHook("resolved"); 24 | 25 | plugins.push(new UnsafeCachePlugin( 26 | "resolve", // 当前钩子 27 | "newResolve" // 下一个钩子 28 | ) 29 | ) 30 | plugins.push(new ParsePlugin("newResolve", "parsedResolve")); 31 | 32 | plugins.push( 33 | new DescriptionFilePlugin( 34 | "parsedResolve", 35 | descriptionFiles, // ['package.json'] 36 | "describedResolve" 37 | ) 38 | ); 39 | 40 | aliasFields.forEach(item => { 41 | plugins.push(new AliasFieldPlugin("describedResolve", item, "resolve")); 42 | }); 43 | if (preferAbsolute) { 44 | plugins.push(new JoinRequestPlugin("describedResolve", "relative")); 45 | } 46 | plugins.push( 47 | new DescriptionFilePlugin( 48 | "relative", 49 | descriptionFiles, 50 | "resolved" 51 | ) 52 | ); 53 | plugins.push(new ResultPlugin(resolver.hooks.resolved)); 54 | 55 | 56 | plugins.forEach(plugin => { 57 | plugin.apply(resolver); 58 | }); 59 | return resolver; 60 | }; 61 | 62 | -------------------------------------------------------------------------------- /mini-pack/lib/Template.js: -------------------------------------------------------------------------------- 1 | const ConcatSource = require('../webpack-sources/ConcatSource') 2 | const COMMENT_END_REGEX = /\*\//g; 3 | 4 | class Template { 5 | 6 | static asString(str) { 7 | if (Array.isArray(str)) { 8 | return str.join("\n"); 9 | } 10 | return str; 11 | } 12 | 13 | static indent(s) { 14 | if (Array.isArray(s)) { 15 | return s.map(Template.indent).join("\n"); 16 | } else { 17 | const str = s.trimRight(); 18 | if (!str) return ""; 19 | const ind = str[0] === "\n" ? "" : "\t"; 20 | return ind + str.replace(/\n([^\n])/g, "\n\t$1"); 21 | } 22 | } 23 | static toComment(str) { 24 | if (!str) return ""; 25 | return `/*! ${str.replace(COMMENT_END_REGEX, "* /")} */`; 26 | } 27 | static prefix(s, prefix) { 28 | const str = Template.asString(s).trim(); 29 | if (!str) return ""; 30 | const ind = str[0] === "\n" ? "" : prefix; 31 | return ind + str.replace(/\n([^\n])/g, "\n" + prefix + "$1"); 32 | } 33 | 34 | /** 35 | * @param {Chunk} chunk chunk whose modules will be rendered 36 | * @param {ModuleFilterPredicate} filterFn function used to filter modules from chunk to render 37 | * @param {ModuleTemplate} moduleTemplate ModuleTemplate instance used to render modules 38 | * @param {TODO | TODO[]} dependencyTemplates templates needed for each module to render dependencies 39 | * @param {string=} prefix applying prefix strings 40 | * @returns {ConcatSource} rendered chunk modules in a Source object 41 | */ 42 | static renderChunkModules( 43 | chunk, 44 | filterFn, 45 | moduleTemplate, 46 | dependencyTemplates, 47 | prefix = "" 48 | ) { 49 | const source = new ConcatSource(); 50 | const modules = chunk.getModules(); 51 | const allModules = modules.map(module => { 52 | return { 53 | id: module.id, 54 | source: moduleTemplate.render(module, dependencyTemplates, { 55 | chunk 56 | }) 57 | }; 58 | }); 59 | // Render an object 60 | source.add("{\n"); 61 | allModules.forEach((module, idx) => { 62 | if (idx !== 0) { 63 | source.add(",\n"); 64 | } 65 | source.add(`\n/***/ ${JSON.stringify(module.id)}:\n`); 66 | source.add(module.source); 67 | }); 68 | source.add(`\n\n${prefix}}`); 69 | 70 | return source; 71 | } 72 | } 73 | 74 | module.exports = Template; 75 | -------------------------------------------------------------------------------- /mini-pack/lib/JavascriptModulesPlugin.js: -------------------------------------------------------------------------------- 1 | 2 | const Parser = require('./Parser') 3 | const JavascriptGenerator = require('./JavascriptGenerator') 4 | const Template = require('./Template') 5 | class JavascriptModulesPlugin { 6 | apply(compiler) { 7 | compiler.hooks.compilation.tap( 8 | "JavascriptModulesPlugin", 9 | (compilation, { normalModuleFactory }) => { 10 | normalModuleFactory.hooks.createParser 11 | .tap("JavascriptModulesPlugin", options => { 12 | return new Parser(options, "auto"); 13 | }) 14 | normalModuleFactory.hooks.createGenerator 15 | .tap("JavascriptModulesPlugin", () => { 16 | return new JavascriptGenerator(); 17 | }); 18 | 19 | compilation.mainTemplate.hooks.renderManifest.tap( 20 | "JavascriptModulesPlugin", 21 | (result, options) => { 22 | const chunk = options.chunk; 23 | const hash = options.hash; 24 | const fullHash = options.fullHash; 25 | const outputOptions = options.outputOptions; 26 | const moduleTemplates = options.moduleTemplates; 27 | const dependencyTemplates = options.dependencyTemplates; 28 | 29 | const filenameTemplate = 30 | chunk.filenameTemplate || outputOptions.filename; 31 | 32 | const useChunkHash = true 33 | result.push({ 34 | render: () => { 35 | return compilation.mainTemplate.render( 36 | hash, 37 | chunk, 38 | moduleTemplates.javascript, 39 | dependencyTemplates 40 | ) 41 | }, 42 | filenameTemplate, 43 | pathOptions: { 44 | noChunkHash: !useChunkHash, 45 | contentHashType: "javascript", 46 | chunk 47 | }, 48 | identifier: `chunk${chunk.id}`, 49 | hash: useChunkHash ? chunk.hash : fullHash 50 | }); 51 | return result; 52 | } 53 | ); 54 | 55 | compilation.mainTemplate.hooks.modules.tap( 56 | "JavascriptModulesPlugin", 57 | (source, chunk, hash, moduleTemplate, dependencyTemplates) => { 58 | return Template.renderChunkModules( 59 | chunk, 60 | m => typeof m.source === "function", 61 | moduleTemplate, 62 | dependencyTemplates, 63 | "/******/ " 64 | ); 65 | } 66 | ); 67 | } 68 | ); 69 | } 70 | } 71 | 72 | module.exports = JavascriptModulesPlugin; 73 | -------------------------------------------------------------------------------- /loaders/css-loader/plugins/postcss-import-parser.js: -------------------------------------------------------------------------------- 1 | const plugin = (options = {}) => { 2 | return { 3 | postcssPlugin: "postcss-import-parser", 4 | 5 | prepare(result) { 6 | const parsedAtRules = []; 7 | return { 8 | AtRule: { 9 | import(atRule) { 10 | const parsedAtRule = { 11 | atRule, 12 | url: atRule.params.replace(/['""]/g, ""), //atRule.params的值为"'./common.css'",因此需要将引号去掉 './common.css' 13 | }; 14 | parsedAtRules.push(parsedAtRule); 15 | } 16 | }, 17 | 18 | async OnceExit() { 19 | if (parsedAtRules.length === 0) { 20 | return; 21 | } 22 | 23 | const resolvedAtRules = await Promise.all(parsedAtRules.map(async parsedAtRule => { 24 | const { 25 | atRule, 26 | url, 27 | } = parsedAtRule; 28 | // 不考虑绝对路径,只考虑相对路径的引用情况,比如: @import "./common.css" 29 | const { 30 | resolver, 31 | context 32 | } = options; 33 | const resolvedUrl = await resolver(context, url); 34 | 35 | atRule.remove(); // 将@import语句移除 36 | 37 | return { 38 | url: resolvedUrl, 39 | }; 40 | })) 41 | 42 | const urlToNameMap = new Map(); 43 | for (let index = 0; index <= resolvedAtRules.length - 1; index++) { 44 | const resolvedAtRule = resolvedAtRules[index]; 45 | 46 | const { 47 | url, 48 | } = resolvedAtRule; 49 | const newUrl = url; 50 | let importName = urlToNameMap.get(newUrl); 51 | 52 | if (!importName) { 53 | importName = `___CSS_LOADER_AT_RULE_IMPORT_${urlToNameMap.size}___`; 54 | urlToNameMap.set(newUrl, importName); 55 | options.imports.push({ 56 | importName, 57 | url: options.urlHandler(newUrl), 58 | index 59 | }); 60 | } 61 | options.api.push({ 62 | importName, 63 | index 64 | }); 65 | } 66 | } 67 | 68 | }; 69 | } 70 | 71 | }; 72 | }; 73 | plugin.postcss = true 74 | 75 | module.exports = plugin; 76 | -------------------------------------------------------------------------------- /mini-pack/enhanced-resolve/DescriptionFilePlugin.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path') 3 | const { cdUp } = require('./DescriptionFileUtils') 4 | module.exports = class DescriptionFilePlugin { 5 | constructor(source, filenames, target) { 6 | this.source = source; // parsedResolve 钩子 7 | this.filenames = [].concat(filenames); // ["package.json"] 8 | this.target = target; // describedResolve 钩子 9 | } 10 | 11 | apply(resolver) { 12 | const target = resolver.ensureHook(this.target); 13 | resolver 14 | .getHook(this.source) 15 | .tapAsync( 16 | "DescriptionFilePlugin", 17 | (request, resolveContext, callback) => { 18 | let directory = request.path; 19 | const pkgFilename = this.filenames[0] 20 | let descriptionFilePath = resolver.join(directory, pkgFilename) 21 | const readPkgJSONFile = (filePath, callback) => { 22 | resolver.fileSystem.readFile(filePath, (err, buffer) => { 23 | if (err) { 24 | // console.error('读取package.json文件失败', err.code) 25 | directory = cdUp(directory) 26 | descriptionFilePath = path.join(directory, pkgFilename) 27 | readPkgJSONFile(descriptionFilePath, callback) 28 | return; 29 | }; 30 | callback(buffer) 31 | }) 32 | } 33 | 34 | readPkgJSONFile(descriptionFilePath, buffer => { 35 | const data = JSON.parse(buffer.toString("utf-8")); 36 | const obj = { 37 | ...request, 38 | descriptionFilePath: descriptionFilePath, 39 | descriptionFileData: data, 40 | descriptionFileRoot: directory, 41 | relativePath: "." + request.path 42 | .substr(directory.length) 43 | .replace(/\\/g, "/") 44 | } 45 | // 交给describedResolve钩子处理,这个钩子会按顺序跑下面的钩子 46 | // 1.先跑AliasFieldPlugin, 经过aliasFieldPlugin处理后 47 | // 2.再跑40个AliasPlugin,这个对主流程没什么处理 48 | // 3.其次跑ModuleKindPlugin,这个对主流程没什么处理 49 | // 4.完了跑JoinRequestPlugin,这个主要是处理path,relativePath,并调用resolver.doResolve继续解析 50 | // console.log('DescriptionFilePlugin===', target) 51 | // 5.最后跑RootPlugin 52 | resolver.doResolve( 53 | target, 54 | obj, 55 | resolveContext, 56 | (err, result) => { 57 | if(err) return callback(err); 58 | callback(null, result) 59 | } 60 | ) 61 | }) 62 | } 63 | ); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /mini-pack/lib/RuleSet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * rules: [ 3 | { type: 'javascript/auto', resolve: {} }, 4 | { 5 | test: /\.mjs$/i, 6 | type: 'javascript/esm', 7 | resolve: { mainFields: [Array] } 8 | }, 9 | { test: /\.json$/i, type: 'json' }, 10 | { test: /\.wasm$/i, type: 'webassembly/experimental' }, 11 | { 12 | test: /\.js$/, 13 | use: { 14 | loader: '/Users/lizuncong/Documents/手写源码系列/mini-webpack/loaders/loader1' 15 | } 16 | } 17 | ] 18 | * **/ 19 | 20 | module.exports = class RuleSet { 21 | constructor(rules) { 22 | this.references = Object.create(null); 23 | const ident = "ref-" 24 | this.rules = rules.map((rule, idx) => { 25 | const id = `${ident}-${idx}` 26 | const newRule = {}; 27 | const condition = rule.test; 28 | if(condition){ 29 | newRule.resource = condition.test.bind(condition) 30 | } 31 | const item = rule.use; 32 | if(item){ 33 | const newItem = {} 34 | Object.keys(item).filter(function(key) { 35 | return !["options", "query"].includes(key); 36 | }).forEach(key => { 37 | newItem[key] = item[key] 38 | }) 39 | newRule.use = [newItem] 40 | } 41 | Object.keys(rule).filter(key => { 42 | return !['test', 'use'].includes(key) 43 | }).forEach(key => { 44 | newRule[key] = rule[key] 45 | }) 46 | return newRule; 47 | }) 48 | } 49 | exec(data){ 50 | const result = []; 51 | this.rules.forEach(rule => { 52 | if (rule.resource && !rule.resource(data.resource)) return false; 53 | const keys = Object.keys(rule).filter(key => { 54 | return ![ 55 | "resource", 56 | "rules", 57 | "use", 58 | ].includes(key); 59 | }); 60 | for (const key of keys) { 61 | result.push({ 62 | type: key, 63 | value: rule[key] 64 | }); 65 | } 66 | if(rule.use){ 67 | rule.use.forEach(use => { 68 | result.push({ 69 | type: 'use', 70 | value: use, 71 | }) 72 | }) 73 | } 74 | }) 75 | return result 76 | } 77 | 78 | }; 79 | -------------------------------------------------------------------------------- /mini-pack/lib/WebpackOptionsApply.js: -------------------------------------------------------------------------------- 1 | const EntryOptionPlugin = require('./EntryOptionPlugin') 2 | const JavascriptModulesPlugin = require('./JavascriptModulesPlugin') 3 | const CommonJsPlugin = require("./dependencies/CommonJsPlugin"); 4 | const NamedModulesPlugin = require('./NamedModulesPlugin') 5 | const NamedChunksPlugin = require('./NamedChunksPlugin') 6 | const FunctionModulePlugin = require('./FunctionModulePlugin') 7 | const TemplatedPathPlugin = require("./TemplatedPathPlugin"); 8 | 9 | // 在webpack.js中初始化 10 | class WebpackOptionsApply{ 11 | process(options, compiler){ 12 | compiler.outputPath = options.output.path; 13 | compiler.name = options.name; 14 | 15 | 16 | const JsonpTemplatePlugin = require("./web/JsonpTemplatePlugin"); 17 | new JsonpTemplatePlugin().apply(compiler); 18 | new FunctionModulePlugin().apply(compiler); 19 | new TemplatedPathPlugin().apply(compiler); 20 | 21 | new JavascriptModulesPlugin().apply(compiler); 22 | 23 | // 挂载入口点,注册compiler.hooks.entryOption插件 24 | new EntryOptionPlugin().apply(compiler) 25 | // 触发插件执行,此时初始化SingleEntryPlugin初始化并注册插件, 26 | // 监听compiler.hooks.make钩子 27 | compiler.hooks.entryOption.call(options.context, options.entry) 28 | 29 | 30 | 31 | new CommonJsPlugin(options.module).apply(compiler); 32 | new NamedModulesPlugin().apply(compiler); 33 | new NamedChunksPlugin().apply(compiler) 34 | 35 | 36 | // compiler.resolverFactory主要用于创建解析文件的resolver实例 37 | // 创建resolver实例时需要传递options选项,这里就是用钩子注册 38 | // 创建resolver实例需要的选项 39 | compiler.resolverFactory.hooks.resolveOptions 40 | .for("loader") 41 | .tap("WebpackOptionsApply", resolveOptions => { 42 | return Object.assign( 43 | { 44 | fileSystem: compiler.inputFileSystem 45 | }, 46 | options.resolveLoader, 47 | resolveOptions 48 | ); 49 | }); 50 | compiler.resolverFactory.hooks.resolveOptions 51 | .for("normal") 52 | .tap("WebpackOptionsApply", resolveOptions => { 53 | return Object.assign( 54 | { 55 | fileSystem: compiler.inputFileSystem 56 | }, 57 | options.resolve, 58 | resolveOptions 59 | ); 60 | }); 61 | 62 | return options; 63 | } 64 | } 65 | 66 | module.exports = WebpackOptionsApply 67 | 68 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | // const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | // const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | // const MediaExtractPlugin = require('./plugins/MediaExtractPlugin') 5 | const Plugin1 = require('./plugins/plugin1') 6 | module.exports = { 7 | mode: "development", 8 | entry: './src/index.js', 9 | // devtool: "source-map", 10 | devtool: "none", 11 | output: { 12 | filename: '[name].js', 13 | path: path.resolve(__dirname, 'dist') 14 | }, 15 | // resolveLoader: { 16 | // modules: ['node_modules', path.resolve(__dirname, 'loaders')], 17 | // }, 18 | module: { 19 | rules: [ 20 | // { 21 | // test: /\.js$/, 22 | // use: ['loader2', 'loader1'] 23 | // }, 24 | { 25 | test: /\.js$/, 26 | use: { 27 | loader: path.resolve(__dirname, 'loaders', 'loader1'), 28 | } 29 | }, 30 | // { 31 | // test: /\.js$/, 32 | // use: { 33 | // loader: path.resolve(__dirname, 'loaders', 'babel-loader'), 34 | // options: { 35 | // presets: [ 36 | // '@babel/preset-env' 37 | // ] 38 | // } 39 | // } 40 | // }, 41 | // { 42 | // test: /\.(png|jpg)$/, 43 | // use: path.resolve(__dirname, 'loaders', 'file-loader') 44 | // }, 45 | // { 46 | // test: /\.less$/, 47 | // use: [ 48 | // 'style-loader', 49 | // { 50 | // loader: 'css-loader', 51 | // options: { 52 | // importLoaders: 1, 53 | // sourceMap: false 54 | // } 55 | // }, 56 | // 'less-loader' 57 | // // path.resolve(__dirname, 'loaders', 'less-loader') 58 | // ] 59 | // }, 60 | // { 61 | // test: /\.css$/, 62 | // use: [ 63 | // // path.resolve(__dirname, 'loaders', 'style-loader'), 64 | // // 'style-loader', 65 | // // MiniCssExtractPlugin.loader, 66 | // // path.resolve(__dirname, 'loaders', 'css-loader'), 67 | // { 68 | // loader: 'css-loader', 69 | // options: { 70 | // sourceMap: false, 71 | // // modules: true, 72 | // } 73 | // }, 74 | // ] 75 | // } 76 | ] 77 | }, 78 | plugins: [ 79 | // new MiniCssExtractPlugin(), 80 | // new MediaExtractPlugin(), 81 | new Plugin1(), 82 | // new HtmlWebpackPlugin({ 83 | // template: "./index.html" 84 | // }) 85 | ] 86 | }; 87 | -------------------------------------------------------------------------------- /mini-pack/enhanced-resolve/Resolver.js: -------------------------------------------------------------------------------- 1 | const Tapable = require("tapable/lib/Tapable"); 2 | const SyncHook = require("tapable/lib/SyncHook"); 3 | const AsyncSeriesBailHook = require("tapable/lib/AsyncSeriesBailHook"); 4 | const AsyncSeriesHook = require("tapable/lib/AsyncSeriesHook"); 5 | const memoryFsJoin = require("memory-fs/lib/join"); 6 | 7 | function withName(name, hook) { 8 | hook.name = name; 9 | return hook; 10 | } 11 | 12 | class Resolver extends Tapable { 13 | constructor(fileSystem) { 14 | super(); 15 | this.fileSystem = fileSystem; 16 | this.hooks = { 17 | resolve: withName( 18 | "resolve", 19 | new AsyncSeriesBailHook(["request", "resolveContext"]) 20 | ), 21 | } 22 | } 23 | // 通过该方法注册的钩子都是 异步串行保险式钩子 24 | ensureHook(name){ 25 | const hook = this.hooks[name]; 26 | if (!hook) { 27 | return (this.hooks[name] = withName( 28 | name, 29 | new AsyncSeriesBailHook(["request", "resolveContext"]) 30 | )); 31 | } 32 | return hook; 33 | } 34 | getHook(name){ 35 | const hook = this.hooks[name]; 36 | if (!hook) { 37 | throw new Error(`Hook ${name} doesn't exist`); 38 | } 39 | return hook; 40 | } 41 | // 解析模块 42 | resolve(context, path, request, resolveContext, callback){ 43 | return this.doResolve( 44 | this.hooks.resolve, 45 | { 46 | context: context, 47 | path: path, 48 | request: request 49 | }, 50 | {}, 51 | (err, result) => { 52 | if(err){ 53 | console.error('resolve.error==:', err) 54 | return; 55 | } 56 | result.__innerRequest_relativePath = result.relativePath = result.__innerRequest_request; 57 | result.__innerRequest_request = undefined 58 | callback( 59 | null, 60 | result.path, 61 | result 62 | ) 63 | } 64 | ) 65 | } 66 | 67 | // request用于收集在解析过程中存储文件信息 68 | doResolve(hook, request, resolveContext, callback){ 69 | // if(request.request && request.request.indexOf('./test') > -1){ 70 | // console.log('target===', hook) 71 | // } 72 | const stackLine = `${hook.name}: (${request.path}) ${request.request || ''}` 73 | const innerContext = { 74 | stack: resolveContext.stack ? resolveContext.stack.concat([stackLine]) : [stackLine] 75 | } 76 | // console.log('doResolve===', hook, request, innerContext, resolveContext) 77 | return hook.callAsync(request, innerContext, (err, result) => { 78 | if(err) return callback(err); 79 | if(result) return callback(null, result); 80 | callback() 81 | }) 82 | } 83 | 84 | join(path, request) { 85 | return memoryFsJoin(path, request); 86 | } 87 | 88 | } 89 | 90 | module.exports = Resolver; 91 | -------------------------------------------------------------------------------- /plugins/MediaExtractPlugin/index.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const postcssMediaParser = require('./postcss-plugins/postcss-media-parser') 4 | class Index { 5 | constructor(){ 6 | } 7 | async extraMediaCss(source){ 8 | const mediaRules = []; 9 | const result = await postcss([ 10 | postcssMediaParser({ 11 | mediaRules, 12 | }) 13 | ]) 14 | .process(source, { 15 | hideNothingWarning: true, 16 | // from: resourcePath, 17 | // to: resourcePath, 18 | map: false 19 | }) 20 | return { 21 | css: result.css, 22 | mediaRules, 23 | } 24 | } 25 | generateMediaCssFile(filename, mediaRules, assets){ 26 | const prefix = filename.split(/\.css$/)[0] 27 | const filenames = []; 28 | mediaRules.forEach(mediaRule => { 29 | const { width, ruleStr, params } = mediaRule 30 | const name = `${prefix}@${width}.css` 31 | filenames.push({name, params}) 32 | assets[name] = { 33 | source(){ 34 | return ruleStr 35 | }, 36 | size(){ 37 | return ruleStr.length 38 | } 39 | } 40 | }); 41 | return filenames 42 | } 43 | async addLinkTag(data, mediaCssFileNames){ 44 | const headTags = data.headTags; 45 | mediaCssFileNames.forEach(file => { 46 | const linkTag = { 47 | tagName: 'link', 48 | voidTag: true, 49 | attributes: { href: file.name, media: file.params, rel: 'stylesheet' } 50 | } 51 | headTags.push(linkTag) 52 | }) 53 | 54 | return { ...data, ...headTags } 55 | } 56 | apply(compiler){ 57 | const reg = /\.css$/; 58 | compiler.hooks.emit.tapPromise('MediaExtractPlugin', async (compilation) => { 59 | let mediaCssFileNames = []; 60 | HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapPromise('alterPlugin', async (data) => { 61 | return await this.addLinkTag(data, mediaCssFileNames); 62 | }) 63 | const assets = compilation.assets; 64 | await Object.entries(assets).map(async ([filename, statObj]) => { 65 | if(!reg.test(filename)) return 66 | const { css, mediaRules } = await this.extraMediaCss(assets[filename].source()) 67 | mediaCssFileNames = await this.generateMediaCssFile(filename, mediaRules, assets) 68 | assets[filename] = { 69 | source() { 70 | return css 71 | }, 72 | size() { 73 | return css.length 74 | } 75 | } 76 | }) 77 | }) 78 | } 79 | } 80 | 81 | module.exports = Index -------------------------------------------------------------------------------- /plugins/MediaExtractPlugin/readme.md: -------------------------------------------------------------------------------- 1 | ### 目标 2 | 提取css文件中多媒体查询css样式,并打包成单独的文件,提高css的加载效率 3 | 4 | ### 场景 5 | 假设有main.css 6 | ```css 7 | div { 8 | height: 200px; 9 | color: red; 10 | overflow: hidden; 11 | background-color: aqua; 12 | margin-bottom: 20px; 13 | } 14 | 15 | @media (max-width: 480px) { 16 | div { 17 | background-color: lightgreen; 18 | } 19 | div { 20 | color: green; 21 | } 22 | } 23 | 24 | @media (max-width: 960px) { 25 | div { 26 | background-color: aliceblue; 27 | } 28 | div { 29 | color: blue; 30 | } 31 | } 32 | 33 | 34 | 35 | .container{ 36 | color: red; 37 | background-color: green; 38 | } 39 | 40 | body{ 41 | color: blue; 42 | background: url(2.131f3c4fe7dda6ea7686ecff63b51db8.png) no-repeat; 43 | } 44 | 45 | 46 | 47 | 48 | @media (max-width: 960px) { 49 | .container{ 50 | color: red; 51 | background-color: blue; 52 | } 53 | } 54 | 55 | 56 | /*# sourceMappingURL=main.css.map*/ 57 | ``` 58 | main.css包含一个 `@media (max-width: 480px)`,两个 `@media (max-width: 960px)`,插件在打包时,会从 `main.css` 文件中分别提取 59 | `@media (max-width: 480px)`并单独打包成 `main@480.css`文件,通过 `link` 标签引入。 60 | 61 | 由于两个 `@media (max-width: 960px)` 规则相同,因此插件会合并这两个多媒体实例,并打包成一个 `main@960.css` 文件,通过 `link` 标签引入。 62 | 63 | 最终的结果: 64 | 65 | main.css: 66 | ```css 67 | div { 68 | height: 200px; 69 | color: red; 70 | overflow: hidden; 71 | background-color: aqua; 72 | margin-bottom: 20px; 73 | } 74 | 75 | 76 | .container{ 77 | color: red; 78 | background-color: green; 79 | } 80 | 81 | body{ 82 | color: blue; 83 | background: url(2.131f3c4fe7dda6ea7686ecff63b51db8.png) no-repeat; 84 | } 85 | 86 | 87 | /*# sourceMappingURL=main.css.map*/ 88 | ``` 89 | 90 | main@480.css: 91 | ```css 92 | div { 93 | background-color: lightgreen; 94 | } 95 | div { 96 | color: green; 97 | } 98 | ``` 99 | 100 | main@960.css: 101 | ```css 102 | div { 103 | background-color: aliceblue; 104 | } 105 | div { 106 | color: blue; 107 | } 108 | .container{ 109 | color: red; 110 | background-color: blue; 111 | } 112 | ``` 113 | 114 | index.html: 115 | ```html 116 | 117 | 118 | 119 | 120 | Mini Webpack 121 | 122 | 123 | 124 | 125 | 126 |
container 127 |
我是红色
128 | 129 | 130 | ``` 131 | 132 | 133 | ### 优点 134 | 在重度响应式设计系统中,能够减少css文件大小,并且根据多媒体类型应用有效的css样式,优先加载有效的css资源,提高css下载效率,解析之行效率,减少首屏时间 135 | 136 | ### todo 137 | - 目前场景比较单一,只针对了 @media (max-width: ...px) 做处理。实际项目中需要设置几个断点,比如 138 | + 移动端: max-width: 760px 139 | + ipad: max-width: 960px 140 | + 大于960px的视为pc端 141 | 142 | - 需要支持sourcemap能力 143 | - 需要支持代码分割懒加载能力。 -------------------------------------------------------------------------------- /loaders/css-loader/plugins/postcss-url-parser.js: -------------------------------------------------------------------------------- 1 | const needParseDeclaration = /(?:url|(?:-webkit-)?image-set)\(/i; 2 | 3 | const plugin = (options = {}) => { 4 | return { 5 | postcssPlugin: "postcss-url-parser", 6 | 7 | prepare(result) { 8 | const parsedDeclarations = []; 9 | return { 10 | Declaration(declaration) { 11 | // 解析值为 url(./2.png)的声明 12 | if (!needParseDeclaration.test(declaration.value)) { 13 | return; 14 | } 15 | const reg = /url\((.+?)\)/g; 16 | const res = reg.exec(declaration.value) 17 | parsedDeclarations.push({ 18 | declaration, 19 | url: res[1].replace(/['""]/g, "") // './2.png' 20 | }) 21 | }, 22 | 23 | async OnceExit() { 24 | if (parsedDeclarations.length === 0) { 25 | return; 26 | } 27 | 28 | const resolvedDeclarations = await Promise.all(parsedDeclarations.map(async parsedDeclaration => { 29 | const { 30 | url 31 | } = parsedDeclaration; 32 | 33 | const { 34 | resolver, 35 | context 36 | } = options; 37 | const resolvedUrl = await resolver(context, url); 38 | 39 | if (!resolvedUrl) { 40 | return; 41 | } 42 | 43 | 44 | return { 45 | ...parsedDeclaration, 46 | url: resolvedUrl, // '/Users/lizuncong/Documents/手写源码系列/mini-webpack/src/2.png' 47 | }; 48 | })); 49 | 50 | 51 | 52 | const urlToNameMap = new Map(); 53 | const urlToReplacementMap = new Map(); 54 | let hasUrlImportHelper = false; 55 | 56 | for (let index = 0; index <= resolvedDeclarations.length - 1; index++) { 57 | const item = resolvedDeclarations[index]; 58 | if (!hasUrlImportHelper) { 59 | options.imports.push({ 60 | importName: "___CSS_LOADER_GET_URL_IMPORT___", 61 | url: options.urlHandler(require.resolve("../runtime/getUrl.js")), 62 | index: -1 63 | }); 64 | hasUrlImportHelper = true; 65 | } 66 | 67 | const { url } = item; // url: '/Users/lizuncong/Documents/手写源码系列/mini-webpack/src/2.png' 68 | const newUrl = url; 69 | let importName = urlToNameMap.get(newUrl); 70 | 71 | if (!importName) { 72 | importName = `___CSS_LOADER_URL_IMPORT_${urlToNameMap.size}___`; 73 | urlToNameMap.set(newUrl, importName); 74 | options.imports.push({ 75 | importName, // '___CSS_LOADER_URL_IMPORT_0___' 76 | url: options.urlHandler(newUrl), // '"./2.png"' 77 | index 78 | }); 79 | } 80 | 81 | const replacementKey = JSON.stringify({ 82 | newUrl 83 | }); 84 | let replacementName = urlToReplacementMap.get(replacementKey); 85 | 86 | if (!replacementName) { 87 | replacementName = `___CSS_LOADER_URL_REPLACEMENT_${urlToReplacementMap.size}___`; 88 | urlToReplacementMap.set(replacementKey, replacementName); 89 | options.replacements.push({ 90 | replacementName, // '___CSS_LOADER_URL_REPLACEMENT_0___' 91 | importName, // '___CSS_LOADER_URL_IMPORT_0___' 92 | }); 93 | } 94 | 95 | item.declaration.value = `url(${replacementName})`; 96 | } 97 | } 98 | 99 | }; 100 | } 101 | 102 | }; 103 | }; 104 | 105 | plugin.postcss = true; 106 | 107 | module.exports = plugin; 108 | -------------------------------------------------------------------------------- /loaders/css-loader/index.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss') 2 | const loaderUtils = require('loader-utils') 3 | const postcssImportParser = require('./plugins/postcss-import-parser') 4 | const postcssUrlParser = require('./plugins/postcss-url-parser') 5 | const { getImportCode, getModuleCode, getExportCode } = require('./utils') 6 | 7 | 8 | async function loader(source){ 9 | const callback = this.async(); 10 | const options = { 11 | import:true, 12 | esModule:true, 13 | importLoaders:undefined, 14 | modules:false, 15 | sourceMap:false, 16 | url:true 17 | } 18 | 19 | // 处理 @import './common.css' 20 | const importPluginImports = []; 21 | const importPluginApi = []; 22 | const resolver = this.getResolve({}); 23 | // 处理url(./2.png) 24 | const urlPluginImports = []; 25 | const replacements = []; 26 | 27 | const { 28 | resourcePath 29 | } = this; 30 | const result = await postcss([ 31 | postcssImportParser({ 32 | imports: importPluginImports, 33 | api: importPluginApi, 34 | resolver, 35 | context: this.context, 36 | // urlHandler: url => loaderUtils.stringifyRequest(this, url) 37 | urlHandler: url => { 38 | const loadersRequest = this.loaders.slice(this.loaderIndex, this.loaderIndex + 1).map(x => x.request).join("!"); 39 | const req = `-!${loadersRequest}!` 40 | const comReq = req + url 41 | return loaderUtils.stringifyRequest(this, comReq) 42 | } 43 | }), 44 | postcssUrlParser({ 45 | imports: urlPluginImports, 46 | replacements, 47 | context: this.context, 48 | resolver, 49 | urlHandler: url => loaderUtils.stringifyRequest(this, url) 50 | }) 51 | ]) 52 | .process(source, { 53 | hideNothingWarning: true, 54 | from: resourcePath, 55 | to: resourcePath, 56 | map: false 57 | }) 58 | // importPluginImports是个数组,数组里的元素为: 59 | // importName:'___CSS_LOADER_AT_RULE_IMPORT_0___' 60 | // index:0 61 | // url:'"-!../node_modules/css-loader/dist/cjs.js??ref--7-0!./common.css"' 62 | const imports = [].concat(importPluginImports).concat(urlPluginImports); 63 | // importPluginApi是个数组,数组里的元素为: 64 | // importName:'___CSS_LOADER_AT_RULE_IMPORT_0___' 65 | // index:0 66 | const api = [].concat(importPluginApi); 67 | imports.unshift({ 68 | importName: "___CSS_LOADER_API_IMPORT___", 69 | url: loaderUtils.stringifyRequest(this, require.resolve("./runtime/api")) 70 | }); 71 | 72 | // getImportCode 输出: 73 | // Imports 74 | // import ___CSS_LOADER_API_IMPORT___ from "../node_modules/css-loader/dist/runtime/api.js"; 75 | // import ___CSS_LOADER_AT_RULE_IMPORT_0___ from "-!../node_modules/css-loader/dist/cjs.js??ref--7-0!./common.css"; 76 | const importCode = getImportCode(imports) 77 | // console.log('==============importCode==============') 78 | // console.log(importCode) 79 | // getModuleCode输出: 80 | // var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(function(i){return i[1]}); 81 | // ___CSS_LOADER_EXPORT___.i(___CSS_LOADER_AT_RULE_IMPORT_0___); 82 | // // Module 83 | // ___CSS_LOADER_EXPORT___.push([module.id, "body{\n color: blue;\n background: yellow;\n}\n\n\n.container{\n color: red;\n}\n\n", ""]); 84 | const moduleCode = getModuleCode(result, api, replacements) 85 | // console.log('==============moduleCode==============') 86 | // console.log(moduleCode) 87 | // getExportCode输出: 88 | // Exports 89 | // export default ___CSS_LOADER_EXPORT___; 90 | const exportCode = getExportCode(options) 91 | // console.log('==============exportCode==============') 92 | // console.log(exportCode) 93 | 94 | const str = `${importCode}${moduleCode}${exportCode}` 95 | console.log('===========my css loader==========') 96 | console.log(str) 97 | console.log('===========my css loader 2222==========') 98 | callback(null, str); 99 | 100 | } 101 | 102 | module.exports = loader 103 | 104 | -------------------------------------------------------------------------------- /mini-pack/lib/Compiler.js: -------------------------------------------------------------------------------- 1 | const { 2 | Tapable, 3 | SyncHook, 4 | SyncBailHook, 5 | AsyncParallelHook, 6 | AsyncSeriesHook 7 | } = require("tapable"); 8 | const ResolverFactory = require("./ResolverFactory"); 9 | const NormalModuleFactory = require('./NormalModuleFactory') 10 | const Compilation = require('./Compilation') 11 | const Stats = require('./Stats') 12 | const path = require('path') 13 | class Compiler extends Tapable { 14 | constructor(context){ 15 | super(); 16 | // 这些钩子大部分在WebpackOptionsApply实例中注册插件 17 | this.hooks = { 18 | afterCompile: new AsyncSeriesHook(["compilation"]), 19 | afterResolvers: new SyncHook(["compiler"]), 20 | beforeRun: new AsyncSeriesHook(["compiler"]), 21 | run: new AsyncSeriesHook(["compiler"]), 22 | watchRun: new AsyncSeriesHook(["compiler"]), 23 | thisCompilation: new SyncHook(["compilation", "params"]), 24 | entryOption: new SyncBailHook(["context", "entry"]), 25 | make: new AsyncParallelHook(["compilation"]), 26 | compilation: new SyncHook(["compilation", "params"]), 27 | emit: new AsyncSeriesHook(["compilation"]), 28 | } 29 | this.name = undefined; 30 | 31 | // 用于创建resolver实例 32 | this.resolverFactory = new ResolverFactory(); 33 | this.outputPath = ""; 34 | this.outputFileSystem = null; 35 | this.inputFileSystem = null; 36 | this.options = {} // webpack options 37 | this.context = context 38 | } 39 | emitAssets(compilation, callback){ 40 | let outputPath; 41 | const emitFiles = err => { 42 | const assets = compilation.getAssets() 43 | for(const { name: targetFile, source } of assets){ 44 | const targetPath = path.posix.join(outputPath, targetFile) 45 | let content = source.source(); 46 | this.outputFileSystem.writeFileSync(targetPath, content) 47 | } 48 | callback() 49 | } 50 | this.hooks.emit.callAsync(compilation, (err) => { 51 | // 在简单的主流程中,其实outputPath可以直接赋值this.outputPath 52 | outputPath = compilation.getPath(this.outputPath); 53 | this.outputFileSystem.mkdir(outputPath, emitFiles); 54 | }) 55 | } 56 | 57 | run(finalCallback){ 58 | const onCompiled = (err, compilation) => { // 编译完成后的回调 59 | this.emitAssets(compilation, (err) => { 60 | const stats = new Stats(compilation) 61 | finalCallback(null, stats) 62 | }) 63 | } 64 | this.compile(onCompiled) 65 | } 66 | 67 | newCompilation(params){ 68 | const compilation = new Compilation(this) 69 | this.hooks.thisCompilation.call(compilation, params) 70 | // 主要是为compilation.dependencyFactories设置值以及 71 | // normalModuleFactory.hooks注册钩子 72 | // 触发SingleEntryPlugin中注册的插件执行,从而为compilation.dependencyFactories 73 | // 设置对应的params.normalModuleFactory值 74 | this.hooks.compilation.call(compilation, params) 75 | return compilation 76 | } 77 | 78 | 79 | compile(onCompiled){ 80 | const params = { 81 | // normalModuleFactory负责调用compiler.resolverFactory.create方法创建 82 | // enhanced-resolver实例。normalModuleFactory也是负责解析模块的工厂实例 83 | normalModuleFactory: new NormalModuleFactory( 84 | this.options.context, 85 | this.resolverFactory, 86 | this.options.module || {} 87 | ), 88 | } 89 | const compilation = this.newCompilation(params) 90 | // 创建完compilation后,触发compilation的addEntry方法。 91 | // SingleEntryPlugin 监听了make事件,触发compilation.addEntry方法 92 | this.hooks.make.callAsync(compilation, err => { 93 | compilation.finish(err => { 94 | compilation.seal(err => { // 通过模块生成代码块 95 | onCompiled(err, compilation) 96 | }) 97 | }) 98 | }) 99 | } 100 | } 101 | 102 | module.exports = Compiler -------------------------------------------------------------------------------- /mini-pack/lib/NormalModuleFactory.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const path = require("path"); 4 | const { 5 | Tapable, 6 | SyncBailHook 7 | } = require("tapable"); 8 | const RuleSet = require("./RuleSet"); 9 | const NormalModule = require('./NormalModule') 10 | 11 | // 负责以下事情 12 | // 1. 解析模块 13 | // 2. 为模块匹配wepback loaders并执行 14 | class NormalModuleFactory extends Tapable { 15 | constructor(context, resolverFactory, options) { 16 | super(); 17 | this.hooks = { 18 | createParser: new SyncBailHook(["parserOptions"]), 19 | createGenerator: new SyncBailHook(["generatorOptions"]) 20 | } 21 | this.context = context || ""; 22 | // resolverFactory负责创建对应的moduleFactory实例 23 | this.resolverFactory = resolverFactory; 24 | this.ruleSet = new RuleSet(options.defaultRules.concat(options.rules)); 25 | } 26 | create(data, callback){ 27 | const dependencies = data.dependencies; 28 | const context = data.context || this.context; 29 | const resolveOptions = data.resolveOptions || {}; 30 | const request = dependencies[0].request; 31 | const contextInfo = data.contextInfo || {}; 32 | // 调用this.resolverFactory创建对应的resolver实例 33 | const normalResolver = this.getResolver("normal", data.resolveOptions); 34 | // normalResolver.resolve仅仅用于解析模块,获取文件路径等信息,而不是读取源码 35 | normalResolver.resolve( 36 | contextInfo, 37 | context, 38 | request, 39 | {}, 40 | (err, resource, resourceResolveData) => { 41 | // resource, 文件的绝对路径,比如'/Users/lizc/Documents/MYProjects/mini-webpack/src/index.js' 42 | // resourceResolveData,包含文件的路径信息,以及package.json内容 43 | 44 | // 为模块 './src/index.js' 匹配对应的webpack loader 45 | const result = this.ruleSet.exec({ 46 | resource, 47 | realResource: resource, 48 | resourceQuery: "", 49 | issuer: contextInfo.issuer, 50 | compiler: contextInfo.compiler 51 | }) 52 | const useLoaders = []; 53 | for(const r of result){ 54 | if(r.type === 'use'){ 55 | useLoaders.push(r.value) 56 | } 57 | } 58 | const parser = this.createParser(); 59 | const generator = this.createGenerator(); 60 | const data = { 61 | context: context, 62 | dependencies: dependencies, 63 | generator: generator, 64 | loaders: useLoaders, // webpack loaders数组 65 | parser: parser, 66 | rawRequest: request, // './src/index.js' 67 | request: useLoaders // './src/index.js' 68 | .map(l => l.loader) 69 | .concat([resource]) 70 | .join("!"), 71 | resolveOptions: {}, 72 | resource, // '/Users/lizc/Documents/MYProjects/mini-webpack/src/index.js' 73 | userRequest: resource, // '/Users/lizc/Documents/MYProjects/mini-webpack/src/index.js' 74 | resourceResolveData, 75 | settings: { 76 | resolve: {}, 77 | type: 'javascript/auto' 78 | }, 79 | type: 'javascript/auto', 80 | } 81 | // 为每一个文件创建一个对应的NormalModule实例 82 | const createdModule = new NormalModule(data); 83 | callback(null, createdModule); 84 | } 85 | ) 86 | } 87 | createParser(parserOptions = {}) { 88 | const parser = this.hooks.createParser.call(parserOptions); 89 | return parser; 90 | } 91 | 92 | createGenerator() { 93 | const generator = this.hooks.createGenerator.call(); 94 | return generator; 95 | } 96 | 97 | getResolver(type, resolveOptions) { 98 | return this.resolverFactory.get( 99 | type, 100 | resolveOptions || {} 101 | ); 102 | } 103 | 104 | } 105 | 106 | module.exports = NormalModuleFactory; 107 | -------------------------------------------------------------------------------- /mini-pack/lib/NormalModule.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path') 3 | const { runLoaders } =require('../loader-runner/LoaderRunner') 4 | const OriginalSource = require('../webpack-sources/OriginalSource') 5 | const CachedSource = require('../webpack-sources/CachedSource') 6 | const createHash = require('./util/createHash') 7 | function dirname(path) { 8 | if(path === "/") return "/"; 9 | var i = path.lastIndexOf("/"); 10 | var j = path.lastIndexOf("\\"); 11 | var i2 = path.indexOf("/"); 12 | var j2 = path.indexOf("\\"); 13 | var idx = i > j ? i : j; 14 | var idx2 = i > j ? i2 : j2; 15 | if(idx < 0) return path; 16 | if(idx === idx2) return path.substr(0, idx + 1); 17 | return path.substr(0, idx); 18 | } 19 | 20 | // 本质上通过require()引入的文件对应一个NormalModule实例 21 | class NormalModule { 22 | constructor({ 23 | type, 24 | request, 25 | userRequest, 26 | rawRequest, 27 | loaders, 28 | resource, 29 | matchResource, 30 | parser, 31 | generator, 32 | resolveOptions, 33 | context 34 | }) { 35 | this.context = dirname(resource); 36 | 37 | // Info from Factory 38 | this.request = request; 39 | this.userRequest = userRequest; 40 | this.rawRequest = rawRequest; 41 | this.parser = parser; 42 | this.generator = generator; 43 | this.resource = resource; 44 | this.matchResource = matchResource; 45 | this.loaders = loaders; // 模块的loaders 46 | this.dependencies = []; // 模块的依赖 47 | this._source = null; // OriginalSource的实例,保存的是模块经过loader处理后的源码 48 | this._chunks = new Set(); 49 | this.index = null; 50 | this.index2 = null; 51 | this.id = null; 52 | if (resolveOptions !== undefined) this.resolveOptions = resolveOptions; 53 | } 54 | identifier() { 55 | return this.request; 56 | } 57 | libIdent(options) { 58 | return "./" + path.posix.relative(options.context, this.userRequest); 59 | } 60 | addChunk(chunk) { 61 | this._chunks.add(chunk); 62 | return true; 63 | } 64 | // 1.执行loader,获取模块经过loader处理后的源码 65 | // 2.解析源码得到对应的模块依赖 66 | build(options, compilation, resolver, fs, callback){ 67 | const loaderContext = { 68 | rootContext: options.context, 69 | _compilation: compilation, 70 | _compiler: compilation.compiler, 71 | fs, 72 | } 73 | runLoaders( 74 | { 75 | resource: this.resource, 76 | loaders: this.loaders, 77 | context: loaderContext, 78 | readResource: fs.readFile.bind(fs) 79 | }, 80 | (err, result) => { 81 | const resourceBuffer = result.resourceBuffer; 82 | const source = result.result[0]; 83 | this._source = new OriginalSource(source, this.request) 84 | this._sourceSize = null; 85 | this._ast = null; 86 | 87 | const parseResult = this.parser.parse( 88 | this._source.source(), 89 | { 90 | current: this, 91 | module: this, 92 | compilation, 93 | options 94 | } 95 | ) 96 | this._initBuildHash(compilation); 97 | callback(); 98 | } 99 | ) 100 | } 101 | _initBuildHash(compilation) { 102 | const hash = createHash(compilation.outputOptions.hashFunction); 103 | if (this._source) { 104 | hash.update("source"); 105 | this._source.updateHash(hash); 106 | } 107 | hash.update("meta"); 108 | hash.update(JSON.stringify({})); 109 | this._buildHash = hash.digest("hex"); 110 | } 111 | updateHash(hash) { 112 | hash.update(this._buildHash); 113 | hash.update(`${this.id}`); 114 | for (const dep of this.dependencies) dep.updateHash(hash); 115 | } 116 | originalSource() { 117 | return this._source; 118 | } 119 | source(dependencyTemplates, runtimeTemplate, type = "javascript") { 120 | 121 | const source = this.generator.generate( 122 | this, 123 | dependencyTemplates, 124 | runtimeTemplate, 125 | type 126 | ); 127 | const cachedSource = new CachedSource(source); 128 | return cachedSource; 129 | } 130 | } 131 | 132 | module.exports = NormalModule; 133 | -------------------------------------------------------------------------------- /loaders/loader指南.md: -------------------------------------------------------------------------------- 1 | ### loader的引入方式 2 | loader只能是普通函数,不能使用箭头函数,因为loader中,this提供了整个上下文的环境 3 | #### 第一种引入方式:使用resolveLoader.modules告诉webpack在哪里查找loader 4 | ```js 5 | module.exports = { 6 | resolveLoader: { 7 | modules: ['node_modules', path.resolve(__dirname, 'loaders')], 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.js$/, 13 | use: 'loader1', 14 | }, 15 | ] 16 | } 17 | }; 18 | ``` 19 | #### 第二种引入方式:使用resolveLoader.alias别名方式 20 | ```js 21 | module.exports = { 22 | resolveLoader: { 23 | alias: { 24 | loader1: path.resolve(__dirname, 'loaders', 'loader1.js') 25 | }, 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.js$/, 31 | use: 'loader1', 32 | }, 33 | ] 34 | } 35 | }; 36 | ``` 37 | #### 第三种引入方式:直接使用path.resolve指定loader位置 38 | ```js 39 | module.exports = { 40 | module: { 41 | rules: [ 42 | { 43 | test: /\.js$/, 44 | use: path.resolve(__dirname, 'loaders', 'loader1.js'), 45 | }, 46 | ] 47 | } 48 | }; 49 | ``` 50 | 51 | ### loader执行时机 52 | #### 默认情况下,loader从下往上,从右到左依次执行 53 | 从右往左依次执行,先loader1,其次loade2,最后loader3 54 | ```js 55 | module.exports = { 56 | resolveLoader: { 57 | modules: ['node_modules', path.resolve(__dirname, 'loaders')], 58 | }, 59 | module: { 60 | rules: [ 61 | { 62 | test: /\.js$/, 63 | use: ['loader3', 'loader2', 'loader1'], 64 | }, 65 | ] 66 | } 67 | }; 68 | ``` 69 | 从下往上依次执行,先loader3,其次loader2,最后loader1 70 | ```js 71 | module.exports = { 72 | resolveLoader: { 73 | modules: ['node_modules', path.resolve(__dirname, 'loaders')], 74 | }, 75 | module: { 76 | rules: [ 77 | { 78 | test: /\.js$/, 79 | use: 'loader1', 80 | }, 81 | { 82 | test: /\.js$/, 83 | use: 'loader2', 84 | }, 85 | { 86 | test: /\.js$/, 87 | use: 'loader3', 88 | }, 89 | ] 90 | } 91 | }; 92 | ``` 93 | #### 可以通过enforce改变loader的执行顺序 94 | loader将按照如下方式依次执行 95 | 96 | `pre loader`(配置了enforce: pre的loader) -> `normal loader` -> `inline loader` -> `post loader`(配置了enforce: post的loader)。 97 | 98 | `loader1`以及`loader3`没有配置`enforce`属性,因此是`normal loader`。 99 | 100 | 下面loader的执行顺序是:loader2 -> loader3 -> loader1 -> loader4 101 | ```js 102 | module.exports = { 103 | resolveLoader: { 104 | modules: ['node_modules', path.resolve(__dirname, 'loaders')], 105 | }, 106 | module: { 107 | rules: [ 108 | { 109 | test: /\.js$/, 110 | use: 'loader1', 111 | }, 112 | { 113 | test: /\.js$/, 114 | use: 'loader2', 115 | enforce: "pre" 116 | }, 117 | { 118 | test: /\.js$/, 119 | use: 'loader3', 120 | }, 121 | { 122 | test: /\.js$/, 123 | use: 'loader4', 124 | enforce: "post" 125 | }, 126 | ] 127 | } 128 | }; 129 | ``` 130 | 131 | #### [inline loader执行顺序](https://webpack.js.org/concepts/loaders/#inline) 132 | 通过`import Styles from '!style-loader!css-loader?modules!./styles.css';`方式引入的就是`inline-loader` 133 | 134 | `inline loader`的使用方式不同,也会改变loader的顺序: 135 | 136 | - 如果`inline loader`前面只有`!`号,则文件不会再通过配置的`normal loader`解析 137 | ```javascript 138 | import Styles from '!style-loader!css-loader?modules!./styles.css'; 139 | ``` 140 | - 如果`inline loader`前面有`!!`号,则表示文件不再通过其他loader处理,只经过inline loader处理。 141 | ```javascript 142 | import Styles from '!!style-loader!css-loader?modules!./styles.css'; 143 | ``` 144 | - 如果`inline-loader`前面有`-!`,则不会让文件再去通过`pre loader` 以及 `normal loader`解析,但还是会经过`post loader`解析 145 | ```javascript 146 | import Styles from '-!style-loader!css-loader?modules!./styles.css'; 147 | ``` 148 | 149 | #### [loader的组成](https://webpack.js.org/api/loaders/#pitching-loader) 150 | loader包含两部分,pitchLoader和normalLoader,pitch和normal的执行顺序正好相反 151 | - 当pitch没有定义或者没有返回值时,会先依次执行pitch在获取资源执行loader 152 | - 如果定义的某个pitch有返回值则会跳过读取资源和自己的loader。假设有use: [loader1,loader2,loader3],三个loader都包含pitchloader和normal loader。 153 | + 第一种情况,三个loader的pitch loader都没有返回值,那么执行顺序为:pitch loader1 -> pitch loader2 -> pitch loader3 -> 获取资源 -> normal loader3 -> 154 | normal loader2 -> normal loader1 155 | + 第二种情况,pitch loader有返回值,假设pitch loader2有返回值,则执行顺序为:pitch loader1 -> pitch loader2 -> noraml loader1 156 | 157 | ```javascript 158 | function loader(source){ 159 | console.log('pitchLoader...', source) 160 | } 161 | 162 | loader.raw = true // 如果设置了raw = true,说明当前loader接受的source是个二进制流 163 | 164 | loader.pitch = function(){ 165 | console.log('pitch...') 166 | } 167 | 168 | 169 | module.exports = loader 170 | ``` 171 | 172 | 173 | ### 异步loader 174 | 同步loader执行完就会继续执行下一个loader 175 | ```javascript 176 | function loader(source, ...rest){ 177 | console.log(source) 178 | return source 179 | } 180 | ``` 181 | 可以通过在loader中调用 182 | ```javascript 183 | const callback = this.async(); 184 | ``` 185 | 将loader变成异步loader。异步loader必须手动调用callback才会执行下一个loader 186 | ```javascript 187 | function loader(source){ 188 | const callback = this.async(); // 将loader变成异步的 189 | console.log(source) 190 | // 3秒后交给下一个loader处理 191 | setTimeout(() => { 192 | callback(null, source) // 第一个参数用于暴露错误 193 | }, 3000); 194 | return source // return都没用,必须手动调用callback 195 | } 196 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 源码调试 2 | - 新建debugger.js文件,文件名称可以自定义。引入node_modules/webpack/bin/webpack,使用debug模式运行debugger.js即可 3 | 4 | ### mini-pack目录 5 | 这个目录下的代码就是手写的webpack主流程源码。对应的webpack版本为4.46.0,webpack-cli版本为3.3.12。 6 | 7 | 直接在控制台运行 `node ./mini-pack/cli` 即可。 8 | 9 | 注意,mini-pack中的源码只是webpack主流程的源码,webpack源码本身除了`架构设计`复杂外,还有`业务层面`更加复杂。主流程源码只是针对简单的 10 | 打包场景进行处理,如果遇到打包出错的情况,那么也毫不意外。 11 | 12 | 一般来说,梳理清楚webpack主流程后,基本上对webpack整体架构都会有个清晰的认识。剩下的就是业务复杂度。如果仅仅是对源码感兴趣,大可不必 13 | 深究webpack自身的业务。举个例子,在我手写的过程中,我只是使用了 `index.js`,`test.js`,`a.js`这三个简单的文件进行打包,在真实的webpack 14 | 业务中,光是模块解析,即`require('./test.js')`,为了解析 `test.js` 文件的位置,就已经很复杂了。webpack将解析模块的代码单独封装成一个工具包, 15 | 即`enhanced-resolve`。 16 | 17 | ### loaders目录 18 | loaders目录里面都是各种手写loader,目前手写的 loader 有: 19 | - babel-loader 20 | - file-loader 21 | - url-loader 22 | - less-loader 对应的官方less-loader版本5.0.0 23 | - css-loader 对应的官方css-loader版本5.2.7。其中css loader开发稍稍复杂,可以参考 [css loader原理指南](https://github.com/lizuncong/mini-webpack/blob/master/loaders/css-loader/readme.md) 24 | - style-loader 对应的官方style-loader版本2.0.0 25 | - 如果需要查看 loader 基础知识,可以查看[这篇文章](https://github.com/lizuncong/mini-webpack/blob/master/loaders/loader%E6%8C%87%E5%8D%97.md) 26 | - 如果需要了解 plugin 和 loader 的区别,可以查看[这篇文章](https://github.com/lizuncong/Front-End-Development-Notes/blob/master/%E5%89%8D%E7%AB%AF%E7%9F%A5%E8%AF%86%E4%BD%93%E7%B3%BB(%E5%85%A8%E9%9D%A2)/webpack/loader%E5%92%8Cplugin%E7%9A%84%E5%8C%BA%E5%88%AB.md) 27 | 28 | ### less loader 29 | index.less 30 | ```less 31 | @color: red; 32 | @import './common.less'; 33 | body{ 34 | background: @color; 35 | background: url('./2.jpg'); 36 | } 37 | ``` 38 | common.less 39 | ```less 40 | img { 41 | height: 400px; 42 | overflow: hidden; 43 | } 44 | ``` 45 | 从打包后的源码可以看出,通过 @import xxx.less 引入的less文件,经过less loader打包,不会单独打包成一个独立的xxx.css module。但是通过 @import xxx.css引入的css文件,则会被打包成一个独立的xxx.css module 46 | 47 | ### plugins目录 48 | plugins目录里面都是各种手写常见的webpack plugin,目前手写的plugin有: 49 | - MediaExtractPlugin。提取CSS文件中多媒体查询CSS并单独打包,在响应式设计系统中能够减少css文件大小,提高加载效率。 50 | - mini-css-extract-plugin 对应的官方版本0.9.0。进行中 51 | 52 | 53 | ### webpack主流程 54 | 在手写webpack主流程源码时,主要以下面的demo为例,在src目录下新建三个文件index.j,test.js,a.js 55 | index.js 56 | ```javascript 57 | const test = require('./test.js') 58 | 59 | console.log(test) 60 | ``` 61 | test.js 62 | ```javascript 63 | const a = require('./a.js') 64 | 65 | console.log('a==', a) 66 | console.log('test.js') 67 | module.exports = 'test.js file' 68 | ``` 69 | a.js 70 | ```javascript 71 | console.log('a.js') 72 | module.exports = 'a.js file' 73 | ``` 74 | 75 | webpack.config.js如下: 76 | ```javascript 77 | const path = require('path'); 78 | module.exports = { 79 | mode: "development", 80 | entry: './src/index.js', 81 | devtool: "none", 82 | output: { 83 | filename: '[name].js', 84 | path: path.resolve(__dirname, 'dist') 85 | }, 86 | module: { 87 | rules: [ 88 | { 89 | test: /\.js$/, 90 | use: { 91 | loader: path.resolve(__dirname, 'loaders', 'loader1'), 92 | } 93 | }, 94 | ] 95 | }, 96 | plugins: [] 97 | }; 98 | ``` 99 | 100 | #### 理论知识 101 | 在阅读webpack源码前,一定要对 `webpack/tapable` 用法非常熟悉。熟悉了 `tapable` 用法后,对于阅读webpack源码难度降低了一半。如果对 `tapable` 102 | 不熟,可以看我手写的 [mini-tapable](https://github.com/lizuncong/mini-tapable) 103 | 104 | #### 思考以下几个问题 105 | - webpack options默认值是什么时候设置的 106 | - webpack 插件初始化是什么时候设置的 107 | - chunk 和 module的关联是咋样的 108 | - webpack loader的执行时机 109 | - webpack plugin的执行时机 110 | - webpack是如何解析模块的,即当我们通过require('./test.js')引入一个模块后,webpack是如何查找对应的文件的 111 | - webpack如何解析模块依赖的 112 | - webpack如何构建模块 113 | - webpack模版代码如何生成 114 | 115 | #### webpack主流程图 116 | 当我们运行 `npm run build` 的时候,npm 会从 `./node_modules/webpack/bin/webpack` 开始,找到并执行 `webpack-cli/bin/cli.js`,这就是webpack编译的入口文件 117 | ![image](https://github.com/lizuncong/mini-webpack/blob/master/webpack-main.png) 118 | 119 | #### 打包阶段 120 | webpack打包可以分为几个阶段 121 | - 初始化options默认值及钩子 122 | - 模块解析 123 | - 打包后的代码生成 124 | ### webpack.js 125 | webpack.js主要做的事情如下,其中重点关注下WebpackOptionsApply.js,webpack整个生命周期内的插件基本都在这里初始化 126 | - new WebpackOptionsDefaulter().process(options)。初始化options默认值 127 | - new Compiler(options.context)。初始化compiler实例 128 | - new NodeEnvironmentPlugin().apply(compiler)。为compiler实例设置读写文件系统,这个读写文件系统贯穿整个webpack生命周期 129 | - options.plugins.forEach(plugin => { plugin.apply(compiler) })。注册用户通过webpack.config.js引入的插件 130 | - new WebpackOptionsApply().process(options, compiler)。初始化默认的webpack插件。其中以下插件对于webpack主流程比较重要 131 | + new EntryOptionPlugin().apply(compiler)。***这是挂载webpack编译的入口点!!!*** 132 | + new SingleEntryPlugin()。*** 监听compiler.hooks.make事件 !!! ***,并且给compilation.dependencyFactories设置对应的moduleFactory 133 | + 其余插件,可以在阅读webpack源码过程中再慢慢回头看这个文件里面的逻辑。 134 | 135 | 136 | ### compiler.js 137 | 逻辑从compiler.js run方法开始看起 138 | - new ResolverFactory() 139 | - new NormalModuleFactory()。这个实例用来解析模块!!所有的模块都会经过这个实例解析 140 | - const compilation = this.newCompilation(params)。初始化compilation实例,为compilation添加normalModuleFactory,这个主要用来创建enhanced-resolver实例,解析模块 141 | - 调用this.hooks.make钩子开始从入口模块编译。这里会从 `SingleEntryPlugin` 注册的插件开始执行,调用 `compilation.addEntry(context, dep, name, callback)` 142 | 143 | ### compilation.js 144 | webpack在打包时,会从webpack.config.js指定的entry开始构建模块,对每一个entry,调用compilation.entry方法开始构建。 145 | - addEntry()。添加入口点 146 | + 往this._preparedEntrypoints中添加entry,_preparedEntrypoints后面生成chunk时会用到. 147 | - _addModuleChain()。 -------------------------------------------------------------------------------- /mini-pack/lib/buildChunkGraph.js: -------------------------------------------------------------------------------- 1 | 2 | // 从modules及其依赖中提取简化信息 3 | const extraceBlockInfoMap = compilation => { 4 | const blockInfoMap = new Map(); 5 | let blockInfoModules; 6 | 7 | // 遍历compilation.modules,对每一个module,遍历module.dependencies 8 | // 获取每个依赖对应的module,构造module和module所依赖的模块的对应关系 9 | for (const module of compilation.modules) { 10 | blockInfoModules = new Set(); 11 | 12 | for (const dep of module.dependencies) { 13 | blockInfoModules.add(dep.module) 14 | }; 15 | 16 | const blockInfo = { 17 | modules: blockInfoModules, // 是一个集合,保存着模块所依赖的模块 18 | blocks: [] // 对主流程来讲是个空数组 19 | }; 20 | blockInfoMap.set(module, blockInfo); 21 | 22 | } 23 | return blockInfoMap; 24 | }; 25 | 26 | 27 | // 对每一个入口entry对应的入口模块,从入口模块开始, 28 | // 递归处理依赖模块 29 | // 1.关联入口chunk和module。遍历每一个module,将其依赖模块添加进入口chunk中 30 | // 2. 31 | const visitModules = ( 32 | compilation, 33 | inputChunkGroups, 34 | chunkGroupInfoMap, 35 | ) => { 36 | 37 | const blockInfoMap = extraceBlockInfoMap(compilation); 38 | 39 | const chunkGroupCounters = new Map(); 40 | for (const chunkGroup of inputChunkGroups) { 41 | chunkGroupCounters.set(chunkGroup, { 42 | index: 0, 43 | index2: 0 44 | }); 45 | } 46 | 47 | let nextFreeModuleIndex = 0; 48 | let nextFreeModuleIndex2 = 0; 49 | 50 | 51 | // 添加模块,进入模块,处理模块,离开模块 52 | const ADD_AND_ENTER_MODULE = 0; 53 | const ENTER_MODULE = 1; 54 | const PROCESS_BLOCK = 2; 55 | const LEAVE_MODULE = 3; 56 | 57 | const reduceChunkGroupToQueueItem = (queue, chunkGroup) => { 58 | for (const chunk of chunkGroup.chunks) { 59 | const module = chunk.entryModule; 60 | queue.push({ 61 | action: ENTER_MODULE, 62 | block: module, 63 | module, 64 | chunk, 65 | chunkGroup 66 | }); 67 | } 68 | chunkGroupInfoMap.set(chunkGroup, { 69 | chunkGroup, 70 | minAvailableModules: new Set(), 71 | minAvailableModulesOwned: true, 72 | availableModulesToBeMerged: [], 73 | skippedItems: [], 74 | resultingAvailableModules: undefined, 75 | children: undefined 76 | }); 77 | return queue; 78 | }; 79 | // 将inputChunkGroups里面的chunks扁平化 80 | let queue = inputChunkGroups 81 | .reduce(reduceChunkGroupToQueueItem, []) 82 | .reverse(); 83 | 84 | let module; 85 | let chunk; 86 | let block; 87 | let chunkGroup; 88 | let chunkGroupInfo; 89 | 90 | while (queue.length) { 91 | while(queue.length){ 92 | const queueItem = queue.pop(); 93 | // block和module引用的都是module对象 94 | module = queueItem.module; 95 | block = queueItem.block; 96 | chunk = queueItem.chunk; 97 | if (chunkGroup !== queueItem.chunkGroup) { 98 | chunkGroup = queueItem.chunkGroup; 99 | chunkGroupInfo = chunkGroupInfoMap.get(chunkGroup); 100 | } 101 | 102 | switch (queueItem.action) { 103 | case ADD_AND_ENTER_MODULE: { 104 | 105 | // We connect Module and Chunk when not already done 106 | if (chunk.addModule(module)) { 107 | module.addChunk(chunk); 108 | } else { 109 | // already connected, skip it 110 | break; 111 | } 112 | } 113 | // fallthrough 114 | case ENTER_MODULE: { 115 | if (chunkGroup !== undefined) { 116 | const index = chunkGroup.getModuleIndex(module); 117 | if (index === undefined) { 118 | chunkGroup.setModuleIndex( 119 | module, 120 | chunkGroupCounters.get(chunkGroup).index++ 121 | ); 122 | } 123 | } 124 | if (module.index === null) { 125 | module.index = nextFreeModuleIndex++; 126 | } 127 | 128 | queue.push({ 129 | action: LEAVE_MODULE, 130 | block, 131 | module, 132 | chunk, 133 | chunkGroup 134 | }); 135 | } 136 | // fallthrough 137 | case PROCESS_BLOCK: { 138 | // get prepared block info 139 | const blockInfo = blockInfoMap.get(block); 140 | 141 | // Buffer items because order need to be reverse to get indicies correct 142 | const skipBuffer = []; 143 | const queueBuffer = []; 144 | // Traverse all referenced modules 145 | for (const refModule of blockInfo.modules) { 146 | // enqueue the add and enter to enter in the correct order 147 | // this is relevant with circular dependencies 148 | queueBuffer.push({ 149 | action: ADD_AND_ENTER_MODULE, 150 | block: refModule, 151 | module: refModule, 152 | chunk, 153 | chunkGroup 154 | }); 155 | } 156 | 157 | for (let i = queueBuffer.length - 1; i >= 0; i--) { 158 | queue.push(queueBuffer[i]); 159 | } 160 | 161 | break; 162 | } 163 | case LEAVE_MODULE: { 164 | if (chunkGroup !== undefined) { 165 | const index = chunkGroup.getModuleIndex2(module); 166 | if (index === undefined) { 167 | chunkGroup.setModuleIndex2( 168 | module, 169 | chunkGroupCounters.get(chunkGroup).index2++ 170 | ); 171 | } 172 | } 173 | 174 | if (module.index2 === null) { 175 | module.index2 = nextFreeModuleIndex2++; 176 | } 177 | 178 | break; 179 | } 180 | } 181 | } 182 | } 183 | }; 184 | 185 | 186 | const buildChunkGraph = (compilation, inputChunkGroups) => { 187 | const chunkGroupInfoMap = new Map(); 188 | 189 | 190 | // PART ONE, 为inputChunkGroups即entry point的_moduleIndices,_moduleIndices2 191 | // 赋值 192 | visitModules( 193 | compilation, 194 | inputChunkGroups, 195 | chunkGroupInfoMap 196 | ); 197 | }; 198 | 199 | module.exports = buildChunkGraph; 200 | -------------------------------------------------------------------------------- /loaders/css-loader/readme.md: -------------------------------------------------------------------------------- 1 | ### 前置知识 2 | - [postcss插件开发基础](https://github.com/postcss/postcss/blob/main/docs/writing-a-plugin.md)。不需要了解太深入,了解基础便于阅读源码。 3 | - [webpack loader开发基础](https://webpack.js.org/contribute/writing-a-loader/) 4 | 5 | ### 示例代码 6 | 本篇所有讲解基于下面的代码文件: 7 | 8 | index.css: 9 | ```css 10 | body{ 11 | color: blue; 12 | background: yellow; 13 | background: url('./2.png'); 14 | } 15 | @import './common.css'; 16 | .container{ 17 | color: red; 18 | background: url(./2.jpg); 19 | } 20 | ``` 21 | common.css: 22 | ```css 23 | div { 24 | height: 200px; 25 | color: red; 26 | overflow: hidden; 27 | } 28 | ``` 29 | webpack.config.js文件: 30 | ```javascript 31 | const path = require('path'); 32 | const HtmlWebpackPlugin = require('html-webpack-plugin') 33 | module.exports = { 34 | mode: "development", 35 | entry: './src/index.js', 36 | devtool: "source-map", 37 | output: { 38 | filename: '[name].js', 39 | path: path.resolve(__dirname, 'dist') 40 | }, 41 | resolveLoader: { 42 | modules: ['node_modules', path.resolve(__dirname, 'loaders')], 43 | }, 44 | module: { 45 | rules: [ 46 | { 47 | test: /\.js$/, 48 | use: { 49 | loader: path.resolve(__dirname, 'loaders', 'babel-loader'), 50 | options: { 51 | presets: [ 52 | '@babel/preset-env' 53 | ] 54 | } 55 | } 56 | }, 57 | { 58 | test: /\.(png|jpg)$/, 59 | use: path.resolve(__dirname, 'loaders', 'file-loader') 60 | }, 61 | { 62 | test: /\.css$/, 63 | use: [ 64 | path.resolve(__dirname, 'loaders', 'css-loader'), 65 | // { 66 | // loader: 'css-loader', 67 | // options: { 68 | // sourceMap: false 69 | // } 70 | // }, 71 | ] 72 | } 73 | ] 74 | }, 75 | plugins: [ 76 | new HtmlWebpackPlugin({ 77 | template: "./index.html" 78 | }) 79 | ] 80 | }; 81 | ``` 82 | 83 | 由于还没引入 style loader,因此需要在index.js文件中手动引用 css loader 打包的结果 84 | 85 | index.js文件 86 | ```javascript 87 | const css = require('./index.css') 88 | console.log('css...module', css) 89 | console.log('css...', css.default.toString()) 90 | const ele = document.createElement('div') 91 | ele.innerHTML = 'hello world'; 92 | document.body.appendChild(ele) 93 | const styleEle = document.createElement('style') 94 | styleEle.appendChild(document.createTextNode(css.default.toString())) 95 | document.head.appendChild(styleEle) 96 | ``` 97 | ### css loader原理 98 | css loader用于解析css文件中 `@import`、`url()`等语法。将其解析成通过 `import/require()`引入的模块。css loader使用 `postcss` 将css源码转换成抽象语法树,通过 `AtRule` 监听 `@import` 规则,通过 `Declaration` 监听值包含 `url()` 的 99 | 声明。 100 | 101 | ### css loader最终输出结果是个啥 102 | index.css 经过 css loader 处理后,结果如下: 103 | ```javascript 104 | // Imports 105 | import ___CSS_LOADER_API_IMPORT___ from "../loaders/css-loader/runtime/api.js"; 106 | import ___CSS_LOADER_AT_RULE_IMPORT_0___ from "./common.css"; 107 | import ___CSS_LOADER_GET_URL_IMPORT___ from "../loaders/css-loader/runtime/getUrl.js"; 108 | import ___CSS_LOADER_URL_IMPORT_0___ from "./2.png"; 109 | import ___CSS_LOADER_URL_IMPORT_1___ from "./2.jpg"; 110 | var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(function(i){return i[1]}); 111 | ___CSS_LOADER_EXPORT___.i(___CSS_LOADER_AT_RULE_IMPORT_0___); 112 | var ___CSS_LOADER_URL_REPLACEMENT_0___ = ___CSS_LOADER_GET_URL_IMPORT___(___CSS_LOADER_URL_IMPORT_0___); 113 | var ___CSS_LOADER_URL_REPLACEMENT_1___ = ___CSS_LOADER_GET_URL_IMPORT___(___CSS_LOADER_URL_IMPORT_1___); 114 | // Module 115 | ___CSS_LOADER_EXPORT___.push([module.id, "body{\n color: blue;\n background: yellow;\n background: url(" + ___CSS_LOADER_URL_REPLACEMENT_0___ + ");\n}\n\n.container{\n color: red;\n background: url(" + ___CSS_LOADER_URL_REPLACEMENT_1___ + ");\n}\n\n", ""]); 116 | // Exports 117 | export default ___CSS_LOADER_EXPORT___; 118 | ``` 119 | 120 | common.css 经过 css loader 处理后,结果如下: 121 | ```javascript 122 | // Imports 123 | import ___CSS_LOADER_API_IMPORT___ from "../loaders/css-loader/runtime/api.js"; 124 | var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(function(i){return i[1]}); 125 | // Module 126 | ___CSS_LOADER_EXPORT___.push([module.id, "div {\n height: 200px;\n color: red;\n overflow: hidden;\n}\n", ""]); 127 | // Exports 128 | export default ___CSS_LOADER_EXPORT___; 129 | ``` 130 | 131 | 经过css loader处理的css文件,最终导出的是一个数组 `list`,也就是上面的 `___CSS_LOADER_EXPORT___`。list是一个二维数组,`list[i]`第一项固定为 css 模块的id,默认为 `@import`的路径。 132 | `list[i]` 第二项固定为 css 模块的源码。 133 | 134 | 135 | 在index.css文件中,通过@import引用了common.css,因此可以看出经过css loader处理后,index.css 的 @import 源码被解析成 136 | ```javascript 137 | import ___CSS_LOADER_AT_RULE_IMPORT_0___ from "./common.css"; 138 | ``` 139 | 并通过 140 | ```javascript 141 | ___CSS_LOADER_EXPORT___.i(___CSS_LOADER_AT_RULE_IMPORT_0___); 142 | ``` 143 | 往index.css的list中添加一个模块。 144 | 145 | `background: url(./2.jpg);` 以及 `background: url('./2.png');` 被解析成 146 | ```javascript 147 | import ___CSS_LOADER_URL_IMPORT_0___ from "./2.png"; 148 | import ___CSS_LOADER_URL_IMPORT_1___ from "./2.jpg"; 149 | 150 | var ___CSS_LOADER_URL_REPLACEMENT_0___ = ___CSS_LOADER_GET_URL_IMPORT___(___CSS_LOADER_URL_IMPORT_0___); 151 | var ___CSS_LOADER_URL_REPLACEMENT_1___ = ___CSS_LOADER_GET_URL_IMPORT___(___CSS_LOADER_URL_IMPORT_1___); 152 | ``` 153 | 154 | 然后通过 `background: url(" + ___CSS_LOADER_URL_REPLACEMENT_0___ + ")`引用 155 | 156 | 157 | 如果我们在index.js中打印index.css的输出,会得到: 158 | ```javascript 159 | `div { 160 | height: 200px; 161 | color: red; 162 | overflow: hidden; 163 | } 164 | body{ 165 | color: blue; 166 | background: yellow; 167 | background: url(2.131f3c4fe7dda6ea7686ecff63b51db8.png); 168 | } 169 | .container{ 170 | color: red; 171 | background: url(2.e85ae876a8234ed3870b97aede7595e0.jpg); 172 | }` 173 | ``` 174 | 可以看出,不论 `@import` 出现在 `index.css` 中的什么位置,`common.css` 模块的代码都在 `index.css` 上面。 175 | 176 | 177 | #### @import规则的处理 178 | `postcss-import-parser` 解析 `@import` 规则,并使用变量 `importPluginImports` 收集需要引入的 css 模块路径信息。使用 `importPluginApi` 收集 179 | 需要引入的模块 180 | ```javascript 181 | // 注意:如果index.css中使用了多次 @import './common.css';那么importPluginImports只会保留一个import 182 | const importPluginImports = [ 183 | { 184 | importName: '___CSS_LOADER_AT_RULE_IMPORT_0___', 185 | url: '"./common.css"', 186 | index: 0 187 | } 188 | ] 189 | // 后面通过遍历importPluginImports中的每一项,输出 import importName from url: 190 | import ___CSS_LOADER_AT_RULE_IMPORT_0___ from "./common.css"; 191 | 192 | 193 | 194 | 195 | // 注意:和importPluginImports不同,如果index.css中多次使用了@import './common.css'; 则这些@import都会被收集到importPluginApi中。 196 | const importPluginApi = [ { importName: '___CSS_LOADER_AT_RULE_IMPORT_0___', index: 0 } ] 197 | // 通过遍历importPluginApi,输出___CSS_LOADER_EXPORT___.i(${item.importName}) 198 | ___CSS_LOADER_EXPORT___.i(___CSS_LOADER_AT_RULE_IMPORT_0___); 199 | ___CSS_LOADER_EXPORT___.i(___CSS_LOADER_AT_RULE_IMPORT_0___); 200 | ``` 201 | 202 | #### url规则的处理 203 | `postcss-url-parser` 解析 `url()`规则。并使用变量 `urlPluginImports` 收集需要引入的模块路径信息。 204 | ```javascript 205 | const urlPluginImports = [ 206 | { 207 | importName: '___CSS_LOADER_GET_URL_IMPORT___', 208 | url: '"../loaders/css-loader/runtime/getUrl.js"', 209 | index: -1 210 | }, 211 | { 212 | importName: '___CSS_LOADER_URL_IMPORT_0___', 213 | url: '"./2.png"', 214 | index: 0 215 | }, 216 | { 217 | importName: '___CSS_LOADER_URL_IMPORT_1___', 218 | url: '"./2.jpg"', 219 | index: 1 220 | } 221 | ] 222 | // 遍历urlPluginImports输出 import importName from url: 223 | import ___CSS_LOADER_GET_URL_IMPORT___ from "../loaders/css-loader/runtime/getUrl.js"; 224 | import ___CSS_LOADER_URL_IMPORT_0___ from "./2.png"; 225 | import ___CSS_LOADER_URL_IMPORT_1___ from "./2.jpg"; 226 | 227 | 228 | 229 | // 占位信息, postcss-url-paser将 background: url(./2.jpg); 解析成 background: url(___CSS_LOADER_URL_REPLACEMENT_0___);其中___CSS_LOADER_URL_REPLACEMENT_0___ 230 | // 用于占位。后面需要使用这个占位符将 background: url(___CSS_LOADER_URL_REPLACEMENT_0___); 解析成运行时代码 "background: url(" + ___CSS_LOADER_URL_REPLACEMENT_0___ + ")"; 231 | const replacements = [ 232 | { 233 | replacementName: '___CSS_LOADER_URL_REPLACEMENT_0___', 234 | importName: '___CSS_LOADER_URL_IMPORT_0___' 235 | }, 236 | { 237 | replacementName: '___CSS_LOADER_URL_REPLACEMENT_1___', 238 | importName: '___CSS_LOADER_URL_IMPORT_1___' 239 | } 240 | ] 241 | 242 | 243 | ``` -------------------------------------------------------------------------------- /mini-pack/lib/MainTemplate.js: -------------------------------------------------------------------------------- 1 | const { 2 | Tapable, 3 | SyncWaterfallHook, 4 | SyncHook, 5 | SyncBailHook 6 | } = require("tapable"); 7 | const Template = require('./Template') 8 | const OriginalSource = require('../webpack-sources/OriginalSource') 9 | const ConcatSource = require('../webpack-sources/ConcatSource') 10 | const PrefixSource = require('../webpack-sources/PrefixSource') 11 | const RawSource = require('../webpack-sources/RawSource') 12 | // require function shortcuts: 13 | // __webpack_require__.s = the module id of the entry point 14 | // __webpack_require__.c = the module cache 15 | // __webpack_require__.m = the module functions 16 | // __webpack_require__.p = the bundle public path 17 | // __webpack_require__.i = the identity function used for harmony imports 18 | // __webpack_require__.e = the chunk ensure function 19 | // __webpack_require__.d = the exported property define getter function 20 | // __webpack_require__.o = Object.prototype.hasOwnProperty.call 21 | // __webpack_require__.r = define compatibility on export 22 | // __webpack_require__.t = create a fake namespace object 23 | // __webpack_require__.n = compatibility get default export 24 | // __webpack_require__.h = the webpack hash 25 | // __webpack_require__.w = an object containing all installed WebAssembly.Instance export objects keyed by module id 26 | // __webpack_require__.oe = the uncaught error handler for the webpack runtime 27 | // __webpack_require__.nc = the script nonce 28 | 29 | module.exports = class MainTemplate extends Tapable { 30 | 31 | constructor(outputOptions) { 32 | super(); 33 | this.outputOptions = outputOptions || {}; 34 | this.hooks = { 35 | assetPath: new SyncWaterfallHook(["path", "options", "assetInfo"]), 36 | hash: new SyncHook(["hash"]), 37 | renderManifest: new SyncWaterfallHook(["result", "options"]), 38 | // bootstrap: new SyncWaterfallHook([ 39 | // "source", 40 | // "chunk", 41 | // "hash", 42 | // "moduleTemplate", 43 | // "dependencyTemplates" 44 | // ]), 45 | modules: new SyncWaterfallHook([ 46 | "modules", 47 | "chunk", 48 | "hash", 49 | "moduleTemplate", 50 | "dependencyTemplates" 51 | ]), 52 | render: new SyncWaterfallHook([ 53 | "source", 54 | "chunk", 55 | "hash", 56 | "moduleTemplate", 57 | "dependencyTemplates" 58 | ]), 59 | moduleObj: new SyncWaterfallHook([ 60 | "source", 61 | "chunk", 62 | "hash", 63 | "moduleIdExpression" 64 | ]), 65 | startup: new SyncWaterfallHook(["source", "chunk", "hash"]), 66 | requireExtensions: new SyncWaterfallHook(["source", "chunk", "hash"]), 67 | require: new SyncWaterfallHook(["source", "chunk", "hash"]), 68 | localVars: new SyncWaterfallHook(["source", "chunk", "hash"]), 69 | }; 70 | this.hooks.localVars.tap("MainTemplate", (source, chunk, hash) => { 71 | return Template.asString([ 72 | source, 73 | "// The module cache", 74 | "var installedModules = {};" 75 | ]); 76 | }); 77 | this.hooks.require.tap("MainTemplate", (source, chunk, hash) => { 78 | return Template.asString([ 79 | source, 80 | "// Check if module is in cache", 81 | "if(installedModules[moduleId]) {", 82 | Template.indent("return installedModules[moduleId].exports;"), 83 | "}", 84 | "// Create a new module (and put it into the cache)", 85 | "var module = installedModules[moduleId] = {", 86 | Template.indent(this.hooks.moduleObj.call("", chunk, hash, "moduleId")), 87 | "};", 88 | "", 89 | Template.asString([ 90 | "// Execute the module function", 91 | `modules[moduleId].call(module.exports, module, module.exports, ${this.renderRequireFunctionForModule( 92 | hash, 93 | chunk, 94 | "moduleId" 95 | )});` 96 | ] 97 | ), 98 | "", 99 | "// Flag the module as loaded", 100 | "module.l = true;", 101 | "", 102 | "// Return the exports of the module", 103 | "return module.exports;" 104 | ]); 105 | }); 106 | this.hooks.moduleObj.tap( 107 | "MainTemplate", 108 | (source, chunk, hash, varModuleId) => { 109 | return Template.asString(["i: moduleId,", "l: false,", "exports: {}"]); 110 | } 111 | ); 112 | this.hooks.requireExtensions.tap("MainTemplate", (source, chunk, hash) => { 113 | const buf = []; 114 | buf.push(""); 115 | buf.push("// expose the modules object (__webpack_modules__)"); 116 | buf.push(`${this.requireFn}.m = modules;`); 117 | 118 | buf.push(""); 119 | buf.push("// expose the module cache"); 120 | buf.push(`${this.requireFn}.c = installedModules;`); 121 | 122 | buf.push(""); 123 | buf.push("// define getter function for harmony exports"); 124 | buf.push(`${this.requireFn}.d = function(exports, name, getter) {`); 125 | buf.push( 126 | Template.indent([ 127 | `if(!${this.requireFn}.o(exports, name)) {`, 128 | Template.indent([ 129 | "Object.defineProperty(exports, name, { enumerable: true, get: getter });" 130 | ]), 131 | "}" 132 | ]) 133 | ); 134 | buf.push("};"); 135 | 136 | buf.push(""); 137 | buf.push("// define __esModule on exports"); 138 | buf.push(`${this.requireFn}.r = function(exports) {`); 139 | buf.push( 140 | Template.indent([ 141 | "if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {", 142 | Template.indent([ 143 | "Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });" 144 | ]), 145 | "}", 146 | "Object.defineProperty(exports, '__esModule', { value: true });" 147 | ]) 148 | ); 149 | buf.push("};"); 150 | 151 | buf.push(""); 152 | buf.push("// create a fake namespace object"); 153 | buf.push("// mode & 1: value is a module id, require it"); 154 | buf.push("// mode & 2: merge all properties of value into the ns"); 155 | buf.push("// mode & 4: return value when already ns object"); 156 | buf.push("// mode & 8|1: behave like require"); 157 | buf.push(`${this.requireFn}.t = function(value, mode) {`); 158 | buf.push( 159 | Template.indent([ 160 | `if(mode & 1) value = ${this.requireFn}(value);`, 161 | `if(mode & 8) return value;`, 162 | "if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;", 163 | "var ns = Object.create(null);", 164 | `${this.requireFn}.r(ns);`, 165 | "Object.defineProperty(ns, 'default', { enumerable: true, value: value });", 166 | "if(mode & 2 && typeof value != 'string') for(var key in value) " + 167 | `${this.requireFn}.d(ns, key, function(key) { ` + 168 | "return value[key]; " + 169 | "}.bind(null, key));", 170 | "return ns;" 171 | ]) 172 | ); 173 | buf.push("};"); 174 | 175 | buf.push(""); 176 | buf.push( 177 | "// getDefaultExport function for compatibility with non-harmony modules" 178 | ); 179 | buf.push(this.requireFn + ".n = function(module) {"); 180 | buf.push( 181 | Template.indent([ 182 | "var getter = module && module.__esModule ?", 183 | Template.indent([ 184 | "function getDefault() { return module['default']; } :", 185 | "function getModuleExports() { return module; };" 186 | ]), 187 | `${this.requireFn}.d(getter, 'a', getter);`, 188 | "return getter;" 189 | ]) 190 | ); 191 | buf.push("};"); 192 | 193 | buf.push(""); 194 | buf.push("// Object.prototype.hasOwnProperty.call"); 195 | buf.push( 196 | `${this.requireFn}.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };` 197 | ); 198 | 199 | const publicPath = '' 200 | buf.push(""); 201 | buf.push("// __webpack_public_path__"); 202 | buf.push(`${this.requireFn}.p = ${JSON.stringify(publicPath)};`); 203 | return Template.asString(buf); 204 | }); 205 | this.hooks.startup.tap("MainTemplate", (source, chunk, hash) => { 206 | const buf = []; 207 | if (chunk.entryModule) { 208 | buf.push("// Load entry module and return exports"); 209 | buf.push( 210 | `return ${this.renderRequireFunctionForModule( 211 | hash, 212 | chunk, 213 | JSON.stringify(chunk.entryModule.id) 214 | )}(${this.requireFn}.s = ${JSON.stringify(chunk.entryModule.id)});` 215 | ); 216 | } 217 | return Template.asString(buf); 218 | }); 219 | this.hooks.render.tap( 220 | "MainTemplate", 221 | (bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => { 222 | const source = new ConcatSource(); 223 | source.add("/******/ (function(modules) { // webpackBootstrap\n"); 224 | 225 | // PrefixSource的目标是将\t制表符替换成/******/ 226 | source.add(new PrefixSource("/******/", bootstrapSource)); 227 | source.add("/******/ })\n"); 228 | source.add( 229 | "/************************************************************************/\n" 230 | ); 231 | source.add("/******/ ("); 232 | // 在JavascriptModulesPlugin中注册this.hooks.modules插件 233 | source.add( 234 | this.hooks.modules.call( 235 | new RawSource(""), 236 | chunk, 237 | hash, 238 | moduleTemplate, 239 | dependencyTemplates 240 | ) 241 | ); 242 | source.add(")"); 243 | return source; 244 | } 245 | ); 246 | this.requireFn = "__webpack_require__"; 247 | } 248 | getRenderManifest(options){ 249 | const result = []; 250 | // 在JavascriptModulesPlugin.js中注册renderManifest插件 251 | this.hooks.renderManifest.call(result, options); 252 | 253 | return result; 254 | } 255 | 256 | updateHash(hash) { 257 | hash.update("maintemplate"); 258 | hash.update("3"); 259 | this.hooks.hash.call(hash); 260 | } 261 | 262 | getAssetPathWithInfo(path, options){ 263 | const assetInfo = {}; 264 | const newPath = this.hooks.assetPath.call(path, options, assetInfo); 265 | return { path: newPath, info: assetInfo } 266 | } 267 | getAssetPath(path, options) { 268 | return this.hooks.assetPath.call(path, options); 269 | } 270 | renderBootstrap(hash, chunk, moduleTemplate, dependencyTemplates){ 271 | const buf = ['']; 272 | buf.push(this.hooks.localVars.call("", chunk, hash)); 273 | buf.push(""); 274 | buf.push("// The require function"); 275 | buf.push(`function ${this.requireFn}(moduleId) {`); 276 | buf.push(Template.indent(this.hooks.require.call("", chunk, hash))); 277 | buf.push("}"); 278 | buf.push(""); 279 | buf.push( 280 | Template.asString(this.hooks.requireExtensions.call("", chunk, hash)) 281 | ); 282 | buf.push(""); 283 | buf.push(""); 284 | buf.push(Template.asString(this.hooks.startup.call("", chunk, hash))); 285 | return buf; 286 | } 287 | render(hash, chunk, moduleTemplate, dependencyTemplates) { 288 | const buf = this.renderBootstrap( 289 | hash, 290 | chunk, 291 | moduleTemplate, 292 | dependencyTemplates 293 | ); 294 | let source = this.hooks.render.call( 295 | new OriginalSource( 296 | Template.prefix(buf, " \t") + "\n", 297 | "webpack/bootstrap" 298 | ), 299 | chunk, 300 | hash, 301 | moduleTemplate, 302 | dependencyTemplates 303 | ); 304 | chunk.rendered = true; 305 | source.children.push(';') 306 | return source 307 | } 308 | renderRequireFunctionForModule(){ 309 | return this.requireFn 310 | } 311 | }; 312 | -------------------------------------------------------------------------------- /mini-pack/lib/Compilation.js: -------------------------------------------------------------------------------- 1 | const { 2 | Tapable, 3 | SyncHook 4 | } = require("tapable"); 5 | const path = require('path') 6 | const Chunk = require('./Chunk') 7 | const MainTemplate = require("./MainTemplate"); 8 | const ChunkTemplate = require('./ChunkTemplate') 9 | const Entrypoint = require('./Entrypoint') 10 | const buildChunkGraph = require('./buildChunkGraph') 11 | const createHash = require('./util/createHash') 12 | const ModuleTemplate = require('./ModuleTemplate') 13 | const CachedSource = require('../webpack-sources/CachedSource') 14 | class Compilation extends Tapable { 15 | constructor(compiler){ 16 | super(); 17 | this.compiler = compiler 18 | this.resolverFactory = compiler.resolverFactory; 19 | this.options = compiler.options // webpack options 20 | this.outputOptions = this.options && this.options.output; 21 | this.mainTemplate = new MainTemplate(this.outputOptions); 22 | this.chunkTemplate = new ChunkTemplate(this.outputOptions); 23 | this.moduleTemplates = { 24 | javascript: new ModuleTemplate(this.runtimeTemplate, "javascript"), 25 | }; 26 | // this.context = compiler.context 27 | this.inputFileSystem = compiler.inputFileSystem 28 | // this.outputFileSystem = compiler.outputFileSystem 29 | this.hooks = { 30 | beforeModuleIds: new SyncHook(["modules"]), 31 | beforeChunkIds: new SyncHook(["chunks"]), 32 | } 33 | // 一般都是通过compiler.hooks.compilation注册插件的方式 34 | // 为 this.dependencyFactories 设置对应的moduleFactory 35 | this.dependencyFactories = new Map(); 36 | this._preparedEntrypoints = []; 37 | 38 | // 代表我们的入口,里面放着所有的入口模块 39 | this.entries = [] 40 | 41 | this.modules = [] // 这是一个模块的数组,里面都是模块实例 42 | this._modules = new Map(); // 存储的是已经编译过的模块实例 43 | this.namedChunkGroups = new Map(); 44 | this.chunkGroups = []; 45 | this.entrypoints = new Map(); 46 | 47 | this.chunks = [] 48 | this.assets = {} 49 | } 50 | 51 | 52 | // 从webpack.config中entry指定的入口模块(即入口文件)开始打包 53 | addEntry(context, entry, name, callback){ 54 | const slot = { 55 | name: name, 56 | request: entry.request, 57 | module: null 58 | }; 59 | // 将入口模块添加进this._preparedEntrypoints数组中 60 | this._preparedEntrypoints.push(slot) 61 | this._addModuleChain( 62 | context, 63 | entry, 64 | module => { 65 | // 将编译后的入口模块添加到入口数组中 66 | this.entries.push(module) 67 | }, 68 | (err, module) => { 69 | slot.module = module; 70 | callback(null, module) 71 | } 72 | ) 73 | } 74 | 75 | _addModuleChain(context, dependency, onModule, callback){ 76 | const Dep = dependency.constructor 77 | // 在Compiler.newCompilation中触发this.hooks.compilation.call(compilation, params)钩子执行, 78 | // 从而在SingleEntryPlugin.js中设置了this.dependencyFactories的值 79 | const moduleFactory = this.dependencyFactories.get(Dep) 80 | moduleFactory.create( 81 | { 82 | contextInfo: { 83 | issuer: "", 84 | compiler: this.compiler.name 85 | }, 86 | context, 87 | dependencies: [dependency] 88 | }, 89 | (err, module) => { 90 | const addModuleResult = this.addModule(module) 91 | onModule(module) 92 | dependency.module = module 93 | if(addModuleResult.build){ 94 | this.buildModule(module, false, null, null, err => { 95 | if (addModuleResult.dependencies) { 96 | this.processModuleDependencies(module, err => { 97 | if (err) return callback(err); 98 | callback(null, module); 99 | }); 100 | } else { 101 | return callback(null, module); 102 | } 103 | }) 104 | } 105 | } 106 | ) 107 | } 108 | addModule(module){ 109 | const identifier = module.identifier(); 110 | // 判断模块是否已经编译过 111 | const alreadyAddedModule = this._modules.get(identifier); 112 | if (alreadyAddedModule) { 113 | return { 114 | module: alreadyAddedModule, 115 | issuer: false, 116 | build: false, 117 | dependencies: false 118 | }; 119 | } 120 | this._modules.set(identifier, module); 121 | this.modules.push(module) 122 | return { 123 | module: module, 124 | issuer: true, 125 | build: true, 126 | dependencies: true 127 | }; 128 | } 129 | buildModule(module, optional, origin, dependencies, thisCallback) { 130 | module.build( 131 | this.options, 132 | this, 133 | this.resolverFactory.get("normal", module.resolveOptions), 134 | this.inputFileSystem, 135 | error => { 136 | thisCallback(); 137 | } 138 | ) 139 | } 140 | processModuleDependencies(module, callback){ 141 | const sortedDependencies = []; 142 | module.dependencies.forEach(dep => { 143 | const factory = this.dependencyFactories.get(dep.constructor) 144 | sortedDependencies.push({ 145 | factory, 146 | dependencies: [dep] 147 | }) 148 | }) 149 | this.addModuleDependencies(module, sortedDependencies, callback) 150 | } 151 | 152 | addModuleDependencies(module, dependencies, callback){ 153 | if(!dependencies.length) callback() 154 | let count = 0; 155 | // console.log('addModuleDependencies==', dependencies) 156 | dependencies.forEach(item => { 157 | const dependencies = item.dependencies; 158 | const factory = item.factory 159 | factory.create( 160 | { 161 | contextInfo: { 162 | issuer: undefined, 163 | compiler: this.compiler.name 164 | }, 165 | resolveOptions: module.resolveOptions, 166 | context: module.context, 167 | dependencies: dependencies 168 | }, 169 | (err, dependentModule) => { 170 | const addModuleResult = this.addModule(dependentModule); 171 | for (let index = 0; index < dependencies.length; index++) { 172 | const dep = dependencies[index]; 173 | dep.module = dependentModule; 174 | } 175 | if (addModuleResult.build) { 176 | this.buildModule( 177 | dependentModule, 178 | false, 179 | null, 180 | null, 181 | err => { 182 | count++; 183 | if(addModuleResult.dependencies){ 184 | this.processModuleDependencies(dependentModule, err => { 185 | if(count === dependencies.length){ 186 | callback() 187 | } 188 | }); 189 | } else { 190 | if(count === dependencies.length){ 191 | callback() 192 | } 193 | } 194 | } 195 | ) 196 | } 197 | } 198 | ) 199 | }) 200 | } 201 | finish(callback){ 202 | callback(); 203 | } 204 | seal(callback){ 205 | for (const preparedEntrypoint of this._preparedEntrypoints) { 206 | const module = preparedEntrypoint.module; 207 | const name = preparedEntrypoint.name; 208 | const chunk = this.addChunk(name); 209 | // entrypoint即是一个chunk group,在webpack配置中配了几个入口点,就有几个 210 | // entrypoint对象 211 | const entrypoint = new Entrypoint(name); 212 | entrypoint.setRuntimeChunk(chunk) 213 | entrypoint.addOrigin(null, name, preparedEntrypoint.request); 214 | this.namedChunkGroups.set(name, entrypoint); 215 | this.entrypoints.set(name, entrypoint); 216 | this.chunkGroups.push(entrypoint); 217 | // 此时, entrypoint和chunk都只是根据name创建的一个空对象,还没和module关联。 218 | // 同时 entrypoint和chunk还没关联。 219 | 220 | // 下面两行代码相互关联chunk和entrypoint 221 | entrypoint.pushChunk(chunk) 222 | chunk.addGroup(entrypoint) 223 | // 下面两行代码相互管理chunk和module 224 | module.addChunk(chunk) 225 | chunk.addModule(module) 226 | chunk.entryModule = preparedEntrypoint.module; 227 | chunk.name = name; 228 | 229 | this.assignDepth(module); 230 | } 231 | 232 | buildChunkGraph( 233 | this, 234 | this.chunkGroups.slice() 235 | ) 236 | 237 | this.hooks.beforeModuleIds.call(this.modules); 238 | this.hooks.beforeChunkIds.call(this.chunks); 239 | this.createHash(); 240 | 241 | // seal阶段的逻辑主要是在createChunkAssets中,这里开始生成模块模版代码 242 | this.createChunkAssets(); 243 | 244 | callback() 245 | } 246 | addChunk(name){ 247 | const chunk = new Chunk(name) 248 | this.chunks.push(chunk) 249 | return chunk; 250 | } 251 | assignDepth(module){ 252 | let depth = 0; 253 | const queue = new Set([module]) 254 | module.depth = 0; 255 | for (module of queue) { 256 | queue.delete(module); 257 | depth = module.depth; 258 | 259 | depth++; 260 | for(let i = 0; i < module.dependencies.length; i++){ 261 | const dep = module.dependencies[i] 262 | if(dep.module){ 263 | queue.add(dep.module) 264 | dep.module.depth = depth 265 | } 266 | } 267 | } 268 | } 269 | // module包含dependency,即dependency是module.dependencies 270 | // 数组中的每一项,这里会为module.dependencies中的每一个dependency 271 | // 创造一个引用 272 | getDependencyReference(module, dependency){ 273 | return dependency.getReference(); 274 | } 275 | // buildDependencies(module, dependencies){ 276 | // module.dependencies = dependencies.map(data => { 277 | // const childModule = normalModuleFactory.create(data) 278 | // return childModule.build(this) 279 | // }) 280 | // } 281 | 282 | // seal(callback){ 283 | // this.hooks.seal.call(); 284 | // this.hooks.beforeChunks.call() 285 | // for(let entryModule of this.entries){ 286 | // const chunk = new Chunk(entryModule) 287 | // this.chunks.push(chunk) 288 | // // 只要模块的名字和代码的名字一样,就说明这个模块属于这个代码块 289 | // chunk.modules = this.modules.filter(module => module.name === chunk.name) 290 | // } 291 | // this.hooks.afterChunks.call() 292 | // this.createChunkAssets() 293 | // callback() 294 | // } 295 | createHash(){ 296 | const outputOptions = this.outputOptions; 297 | const hashFunction = outputOptions.hashFunction; // md4; 298 | const hashDigest = outputOptions.hashDigest; // hex 299 | const hashDigestLength = outputOptions.hashDigestLength; // 20 300 | const hash = createHash(hashFunction); 301 | this.mainTemplate.updateHash(hash); 302 | this.chunkTemplate.updateHash(hash); 303 | this.moduleTemplates.javascript.updateHash(hash) 304 | 305 | // 为每个module生成hash及renderedHash值 306 | const modules = this.modules; 307 | for (let i = 0; i < modules.length; i++) { 308 | const module = modules[i]; 309 | const moduleHash = createHash(hashFunction); 310 | module.updateHash(moduleHash); 311 | module.hash = moduleHash.digest(hashDigest); 312 | module.renderedHash = module.hash.substr(0, hashDigestLength); 313 | } 314 | 315 | // 生成chunk hash 316 | const chunks = this.chunks.slice(); 317 | for (let i = 0; i < chunks.length; i++) { 318 | const chunk = chunks[i]; 319 | const chunkHash = createHash(hashFunction); 320 | chunk.updateHash(chunkHash); 321 | chunk.hash = chunkHash.digest(hashDigest); 322 | // 上层的hash值 323 | hash.update(chunk.hash); 324 | chunk.renderedHash = chunk.hash.substr(0, hashDigestLength); 325 | } 326 | this.fullHash = hash.digest(hashDigest); 327 | this.hash = this.fullHash.substr(0, hashDigestLength); 328 | } 329 | createChunkAssets(){ 330 | const outputOptions = this.outputOptions; 331 | for(let i = 0; i < this.chunks.length; i++){ 332 | const chunk = this.chunks[i] 333 | chunk.files = [] 334 | const template = this.mainTemplate; 335 | const manifest = template.getRenderManifest({ 336 | chunk, 337 | hash: this.hash, 338 | fullHash: this.fullHash, 339 | outputOptions, 340 | moduleTemplates: this.moduleTemplates, 341 | dependencyTemplates: this.dependencyTemplates 342 | }); 343 | for (const fileManifest of manifest) { 344 | const cacheName = fileManifest.identifier; 345 | const usedHash = fileManifest.hash; 346 | const filenameTemplate = fileManifest.filenameTemplate; 347 | const pathAndInfo = this.mainTemplate.getAssetPathWithInfo(filenameTemplate, { 348 | ...fileManifest.pathOptions, 349 | hash: this.hash 350 | }) 351 | const file = pathAndInfo.path; 352 | const assetInfo = pathAndInfo.info; 353 | const source = fileManifest.render(); 354 | const cachedSource = new CachedSource(source); 355 | this.emitAsset(file, cachedSource, assetInfo); 356 | chunk.files.push(file); 357 | } 358 | } 359 | } 360 | emitAsset(file, source, assetInfo = {}) { 361 | this.assets[file] = source; 362 | } 363 | 364 | getPath(filename, data) { 365 | data = data || {}; 366 | data.hash = data.hash || this.hash; 367 | return this.mainTemplate.getAssetPath(filename, data); 368 | } 369 | 370 | getAssets() { 371 | const array = []; 372 | for (const assetName of Object.keys(this.assets)) { 373 | if (Object.prototype.hasOwnProperty.call(this.assets, assetName)) { 374 | array.push({ 375 | name: assetName, 376 | source: this.assets[assetName], 377 | info: {} 378 | }); 379 | } 380 | } 381 | return array; 382 | } 383 | 384 | } 385 | 386 | module.exports = Compilation -------------------------------------------------------------------------------- /mini-pack/lib/WebpackOptionsDefaulter.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const isProductionLikeMode = options => { 4 | return options.mode ==="production" || !options.mode; 5 | }; 6 | 7 | const isWebLikeTarget = options => { 8 | return options.target ==="web" || options.target ==="webworker"; 9 | }; 10 | 11 | const getDevtoolNamespace = library => { 12 | // if options.output.library is a string 13 | if (Array.isArray(library)) { 14 | return library.join("."); 15 | } else if (typeof library ==="object") { 16 | return getDevtoolNamespace(library.root); 17 | } 18 | return library ||""; 19 | }; 20 | const getProperty = (obj, path) => { 21 | let name = path.split("."); 22 | for (let i = 0; i < name.length - 1; i++) { 23 | obj = obj[name[i]]; 24 | if (typeof obj !== "object" || !obj || Array.isArray(obj)) return; 25 | } 26 | return obj[name.pop()]; 27 | }; 28 | const setProperty = (obj, path, value) => { 29 | let name = path.split("."); 30 | for (let i = 0; i < name.length - 1; i++) { 31 | if (typeof obj[name[i]] !== "object" && obj[name[i]] !== undefined) return; 32 | if (Array.isArray(obj[name[i]])) return; 33 | if (!obj[name[i]]) obj[name[i]] = {}; 34 | obj = obj[name[i]]; 35 | } 36 | obj[name.pop()] = value; 37 | }; 38 | class WebpackOptionsDefaulter{ 39 | constructor(){ 40 | // Stores configuration for options 41 | this.config = { 42 | cache:'make', 43 | devtool:'make', 44 | infrastructureLogging:'call', 45 | module:'call', 46 | 'module.defaultRules': 'make', 47 | 'module.unsafeCache':'make', 48 | node:'call', 49 | optimization:'call', 50 | 'optimization.checkWasmTypes':'make', 51 | 'optimization.concatenateModules':'make', 52 | 'optimization.flagIncludedChunks':'make', 53 | 'optimization.minimize':'make', 54 | 'optimization.minimizer':'make', 55 | 'optimization.namedChunks':'make', 56 | 'optimization.namedModules':'make', 57 | 'optimization.nodeEnv':'make', 58 | 'optimization.noEmitOnErrors':'make', 59 | 'optimization.occurrenceOrder':'make', 60 | 'optimization.portableRecords':'make', 61 | 'optimization.removeAvailableModules':'make', 62 | 'optimization.runtimeChunk':'call', 63 | 'optimization.sideEffects':'make', 64 | 'optimization.splitChunks.hidePathInfo':'make', 65 | 'optimization.splitChunks.maxAsyncRequests':'make', 66 | 'optimization.splitChunks.maxInitialRequests':'make', 67 | 'optimization.splitChunks.minSize':'make', 68 | 'optimization.usedExports':'make', 69 | output:'call', 70 | 'output.chunkCallbackName':'make', 71 | 'output.chunkFilename':'make', 72 | 'output.devtoolNamespace':'make', 73 | 'output.globalObject':'make', 74 | 'output.hotUpdateFunction':'make', 75 | 'output.jsonpFunction':'make', 76 | 'output.pathinfo':'make', 77 | performance:'call', 78 | 'performance.hints':'make', 79 | resolve:'call', 80 | 'resolve.aliasFields':'make', 81 | 'resolve.cacheWithContext':'make', 82 | 'resolve.ignoreRootsErrors':'make', 83 | 'resolve.mainFields':'make', 84 | 'resolve.preferAbsolute':'make', 85 | 'resolve.roots':'make', 86 | resolveLoader:'call', 87 | 'resolveLoader.cacheWithContext':'make', 88 | }; 89 | 90 | // Stores default options settings or functions for computing them 91 | this.defaults = { 92 | entry:"./src", 93 | devtool: options => 94 | options.mode ==="development" ?"eval" : false, 95 | cache: options => options.mode ==="development", 96 | context: process.cwd(), 97 | target:"web", 98 | module: value => Object.assign({}, value), 99 | "module.unknownContextRequest":".", 100 | "module.unknownContextRegExp": false, 101 | "module.unknownContextRecursive": true, 102 | "module.unknownContextCritical": true, 103 | "module.exprContextRequest":".", 104 | "module.exprContextRegExp": false, 105 | "module.exprContextRecursive": true, 106 | "module.exprContextCritical": true, 107 | "module.wrappedContextRegExp": /.*/, 108 | "module.wrappedContextRecursive": true , 109 | "module.wrappedContextCritical": false , 110 | "module.strictExportPresence": false , 111 | "module.strictThisContextOnImports": false , 112 | "module.unsafeCache": options => !!options.cache , 113 | "module.rules": [], 114 | "module.defaultRules": options => [ 115 | { 116 | type:"javascript/auto", 117 | resolve: {} 118 | }, 119 | { 120 | test: /\.mjs$/i, 121 | type:"javascript/esm", 122 | resolve: { 123 | mainFields: 124 | options.target ==="web" || 125 | options.target ==="webworker" || 126 | options.target ==="electron-renderer" 127 | ? ["browser","main"] 128 | : ["main"] 129 | } 130 | }, 131 | { 132 | test: /\.json$/i, 133 | type:"json" 134 | }, 135 | { 136 | test: /\.wasm$/i, 137 | type:"webassembly/experimental" 138 | } 139 | ] , 140 | "output": (value, options) => { 141 | if (typeof value ==="string") { 142 | return { 143 | filename: value 144 | }; 145 | } else if (typeof value !=="object") { 146 | return {}; 147 | } else { 148 | return Object.assign({}, value) 149 | } 150 | } , 151 | 152 | "output.filename":"[name].js" , 153 | "output.chunkFilename": options => { 154 | const filename = options.output.filename; 155 | if (typeof filename !=="function") { 156 | const hasName = filename.includes("[name]"); 157 | const hasId = filename.includes("[id]"); 158 | const hasChunkHash = filename.includes("[chunkhash]"); 159 | // Anything changing depending on chunk is fine 160 | if (hasChunkHash || hasName || hasId) return filename; 161 | // Elsewise prefix"[id]." in front of the basename to make it changing 162 | return filename.replace(/(^|\/)([^/]*(?:\?|$))/,"$1[id].$2"); 163 | } 164 | return"[id].js"; 165 | }, 166 | "output.webassemblyModuleFilename":"[modulehash].module.wasm" , 167 | "output.library":"" , 168 | "output.hotUpdateFunction": options => { 169 | return "webpackHotUpdate" 170 | // return Template.toIdentifier( 171 | // "webpackHotUpdate" + Template.toIdentifier(options.output.library) 172 | // ) 173 | }, 174 | "output.jsonpFunction": options => { 175 | return "webpackJsonp" 176 | // return Template.toIdentifier( 177 | // "webpackJsonp" + Template.toIdentifier(options.output.library) 178 | // ) 179 | } , 180 | "output.chunkCallbackName": options => { 181 | return "webpackChunk" 182 | // return Template.toIdentifier( 183 | // "webpackChunk" + Template.toIdentifier(options.output.library) 184 | // ) 185 | } , 186 | "output.globalObject": options => { 187 | switch (options.target) { 188 | case"web": 189 | case"electron-renderer": 190 | case"node-webkit": 191 | return"window"; 192 | case"webworker": 193 | return"self"; 194 | case"node": 195 | case"async-node": 196 | case"electron-main": 197 | return"global"; 198 | default: 199 | return"self"; 200 | } 201 | } , 202 | "output.devtoolNamespace": options => { 203 | return getDevtoolNamespace(options.output.library) 204 | } , 205 | "output.libraryTarget":"var" , 206 | "output.path": path.join(process.cwd(),"dist") , 207 | "output.pathinfo":options => options.mode ==="development", 208 | "output.sourceMapFilename":"[file].map[query]" , 209 | "output.hotUpdateChunkFilename":"[id].[hash].hot-update.js" , 210 | "output.hotUpdateMainFilename":"[hash].hot-update.json" , 211 | "output.crossOriginLoading": false , 212 | "output.jsonpScriptType": false , 213 | "output.chunkLoadTimeout": 120000 , 214 | "output.hashFunction":"md4" , 215 | "output.hashDigest":"hex" , 216 | "output.hashDigestLength": 20 , 217 | "output.devtoolLineToLine": false , 218 | "output.strictModuleExceptionHandling": false , 219 | "node": value => { 220 | if (typeof value ==="boolean") { 221 | return value; 222 | } else { 223 | return Object.assign({}, value); 224 | } 225 | }, 226 | "node.console": false, 227 | "node.process": true , 228 | "node.global": true , 229 | "node.Buffer": true , 230 | "node.setImmediate": true , 231 | "node.__filename":"mock" , 232 | "node.__dirname":"mock" , 233 | "performance": (value, options) => { 234 | if (value === false) return false; 235 | if ( 236 | value === undefined && 237 | (!isProductionLikeMode(options) || !isWebLikeTarget(options)) 238 | ) 239 | return false; 240 | return Object.assign({}, value) 241 | }, 242 | "performance.maxAssetSize": 250000 , 243 | "performance.maxEntrypointSize": 250000 , 244 | "performance.hints": options => 245 | isProductionLikeMode(options) ?"warning" : false, 246 | "optimization": value => Object.assign({}, value) , 247 | // TODO webpack 5: Disable by default in a modes 248 | "optimization.removeAvailableModules": 249 | options => options.mode !=="development", 250 | "optimization.removeEmptyChunks": true , 251 | "optimization.mergeDuplicateChunks": true , 252 | "optimization.flagIncludedChunks": options => 253 | isProductionLikeMode(options), 254 | // TODO webpack 5 add `moduleIds:"named"` default for development 255 | // TODO webpack 5 add `moduleIds:"size"` default for production 256 | // TODO webpack 5 remove optimization.occurrenceOrder 257 | "optimization.occurrenceOrder": options => 258 | isProductionLikeMode(options), 259 | "optimization.sideEffects": options => 260 | isProductionLikeMode(options), 261 | "optimization.providedExports": true , 262 | "optimization.usedExports": options => 263 | isProductionLikeMode(options), 264 | "optimization.concatenateModules": options => 265 | isProductionLikeMode(options), 266 | "optimization.splitChunks": {} , 267 | "optimization.splitChunks.hidePathInfo": options => { 268 | return isProductionLikeMode(options) 269 | }, 270 | "optimization.splitChunks.chunks":"async" , 271 | "optimization.splitChunks.minSize": options => { 272 | return isProductionLikeMode(options) ? 30000 : 10000; 273 | } , 274 | "optimization.splitChunks.minChunks": 1 , 275 | "optimization.splitChunks.maxAsyncRequests": options => { 276 | return isProductionLikeMode(options) ? 5 : Infinity; 277 | } , 278 | "optimization.splitChunks.automaticNameDelimiter":"~" , 279 | "optimization.splitChunks.automaticNameMaxLength": 109 , 280 | "optimization.splitChunks.maxInitialRequests": options => { 281 | return isProductionLikeMode(options) ? 3 : Infinity; 282 | } , 283 | "optimization.splitChunks.name": true , 284 | "optimization.splitChunks.cacheGroups": {} , 285 | "optimization.splitChunks.cacheGroups.default": { 286 | automaticNamePrefix:"", 287 | reuseExistingChunk: true, 288 | minChunks: 2, 289 | priority: -20 290 | } , 291 | "optimization.splitChunks.cacheGroups.vendors": { 292 | automaticNamePrefix:"vendors", 293 | test: /[\\/]node_modules[\\/]/, 294 | priority: -10 295 | }, 296 | "optimization.runtimeChunk": value => { 297 | if (value ==="single") { 298 | return { 299 | name:"runtime" 300 | }; 301 | } 302 | if (value === true || value ==="multiple") { 303 | return { 304 | name: entrypoint => `runtime~${entrypoint.name}` 305 | }; 306 | } 307 | return value; 308 | } , 309 | "optimization.noEmitOnErrors": options => 310 | isProductionLikeMode(options) 311 | , 312 | "optimization.checkWasmTypes": options => 313 | isProductionLikeMode(options) 314 | , 315 | "optimization.mangleWasmImports": false , 316 | // TODO webpack 5 remove optimization.namedModules 317 | "optimization.namedModules": 318 | options => options.mode ==="development", 319 | "optimization.hashedModuleIds": false , 320 | // TODO webpack 5 add `chunkIds:"named"` default for development 321 | // TODO webpack 5 add `chunkIds:"size"` default for production 322 | // TODO webpack 5 remove optimization.namedChunks 323 | "optimization.namedChunks": 324 | options => options.mode ==="development", 325 | "optimization.portableRecords": 326 | options => 327 | !!( 328 | options.recordsInputPath || 329 | options.recordsOutputPath || 330 | options.recordsPath 331 | ), 332 | "optimization.minimize": options => 333 | isProductionLikeMode(options) 334 | , 335 | "optimization.minimizer": options => [ 336 | { 337 | apply: compiler => { 338 | // Lazy load the Terser plugin 339 | const TerserPlugin = require("terser-webpack-plugin"); 340 | const SourceMapDevToolPlugin = require("./SourceMapDevToolPlugin"); 341 | new TerserPlugin({ 342 | cache: true, 343 | parallel: true, 344 | sourceMap: 345 | (options.devtool && /source-?map/.test(options.devtool)) || 346 | (options.plugins && 347 | options.plugins.some(p => p instanceof SourceMapDevToolPlugin)) 348 | }).apply(compiler); 349 | } 350 | } 351 | ], 352 | "optimization.nodeEnv": options => { 353 | // TODO: In webpack 5, it should return `false` when mode is `none` 354 | return options.mode ||"production"; 355 | } , 356 | "resolve": value => Object.assign({}, value) , 357 | "resolve.unsafeCache": true , 358 | "resolve.modules": ["node_modules"] , 359 | "resolve.extensions": [".wasm", ".mjs", ".js", ".json"] , 360 | "resolve.mainFiles": ["index"] , 361 | "resolve.aliasFields": options => { 362 | if ( 363 | options.target ==="web" || 364 | options.target ==="webworker" || 365 | options.target ==="electron-renderer" 366 | ) { 367 | return ["browser"]; 368 | } else { 369 | return []; 370 | } 371 | } , 372 | "resolve.mainFields": options => { 373 | if ( 374 | options.target ==="web" || 375 | options.target ==="webworker" || 376 | options.target ==="electron-renderer" 377 | ) { 378 | return ["browser", "module", "main"]; 379 | } else { 380 | return ["module", "main"]; 381 | } 382 | } , 383 | "resolve.cacheWithContext": options => { 384 | return ( 385 | Array.isArray(options.resolve.plugins) && 386 | options.resolve.plugins.length > 0 387 | ) 388 | }, 389 | "resolve.preferAbsolute": 390 | options => !options.resolve.roots || options.resolve.roots.length === 0, 391 | "resolve.ignoreRootsErrors": 392 | options => !options.resolve.roots || options.resolve.roots.length === 0, 393 | "resolve.roots": options => [options.context] , 394 | "resolveLoader": value => Object.assign({}, value) , 395 | "resolveLoader.unsafeCache": true , 396 | "resolveLoader.mainFields": ["loader", "main"] , 397 | "resolveLoader.extensions": [".js", ".json"] , 398 | "resolveLoader.mainFiles": ["index"] , 399 | "resolveLoader.cacheWithContext": options => { 400 | return ( 401 | Array.isArray(options.resolveLoader.plugins) && 402 | options.resolveLoader.plugins.length > 0 403 | ) 404 | } , 405 | "infrastructureLogging": value => Object.assign({}, value), 406 | "infrastructureLogging.level":"info" , 407 | "infrastructureLogging.debug": false , 408 | } 409 | } 410 | process(options){ 411 | for (let name in this.defaults) { 412 | switch (this.config[name]) { 413 | /** 414 | * If {@link ConfigType} doesn't specified and current value is `undefined`, then default value will be assigned 415 | */ 416 | case undefined: 417 | if (getProperty(options, name) === undefined) { 418 | setProperty(options, name, this.defaults[name]); 419 | } 420 | break; 421 | /** 422 | * Assign result of {@link CallConfigHandler} 423 | */ 424 | case "call": 425 | setProperty( 426 | options, 427 | name, 428 | this.defaults[name].call(this, getProperty(options, name), options) 429 | ); 430 | break; 431 | /** 432 | * Assign result of {@link MakeConfigHandler}, if current value is `undefined` 433 | */ 434 | case "make": 435 | if (getProperty(options, name) === undefined) { 436 | setProperty(options, name, this.defaults[name].call(this, options)); 437 | } 438 | break; 439 | /** 440 | * Adding {@link AppendConfigValues} at the end of the current array 441 | */ 442 | case "append": { 443 | let oldValue = getProperty(options, name); 444 | if (!Array.isArray(oldValue)) { 445 | oldValue = []; 446 | } 447 | oldValue.push(...this.defaults[name]); 448 | setProperty(options, name, oldValue); 449 | break; 450 | } 451 | default: 452 | throw new Error( 453 | "OptionsDefaulter cannot process " + this.config[name] 454 | ); 455 | } 456 | } 457 | return options; 458 | } 459 | } 460 | 461 | module.exports = WebpackOptionsDefaulter 462 | --------------------------------------------------------------------------------