1. rollup #

1.1 debugger.js #

import { rollup, watch } from 'rollup';
import inputOptions from './rollup.config.js'
  ; (async function () {
    //打包阶段 
    const bundle = await rollup(inputOptions);
    //生成阶段
    await bundle.generate(inputOptions.output);
    //写入阶段
    await bundle.write(inputOptions.output);
    /* 
    const watcher = watch(inputOptions);
    watcher.on('event', event => {
      console.log(event);
    });
    setTimeout(() => {
      watcher.close();
    }, 1000); */
    //关闭阶段
    await bundle.close();
  })();

1.2 rollup.config.js #

rollup.config.js

export default {
  input: "./src/index.js",
  output: {
    dir: 'dist',
  }
}

1.3 package.json #

package.json

{
  "type": "module",
  "scripts": {
    "build": "rollup -c"
  },
}

2. rollup插件 #

2.1 插件规范 #

2.2.插件属性 #

2.2.1 name #

2.2 Build Hooks #

2.2.1 rollup-plugin-build.js #

plugins\rollup-plugin-build.js

import fs from 'fs';

function build() {
  return {
    name: 'build',
    async watchChange(id, change) {
      console.log('watchChange', id, change);
    },
    async closeWatcher() {
      console.log('closeWatcher');
    },
    async options(inputOptions) {
      console.log('options');
      //inputOptions.input = './src/main.js';
    },
    async buildStart(inputOptions) {
      console.log('buildStart');
    },
    async resolveId(source, importer) {
      if (source === 'virtual') {
        console.log('resolveId', source);
        //如果resolveId钩子有返回值了,那么就会跳过后面的查找逻辑,以此返回值作为最终的模块ID
        return source;
      }
    },
    //加载此模块ID对应的内容
    async load(id) {
      if (id === 'virtual') {
        console.log('load', id);
        return `export default "virtual"`;
      }
    },
    async shouldTransformCachedModule({ id, code, ast }) {
      console.log('shouldTransformCachedModule');
      //不使用缓存,再次进行转换
      return true;
    },
    async transform(code, id) {
      console.log('transform');
    },
    async moduleParsed(moduleInfo) {
      console.log('moduleParsed');
    },
    async resolveDynamicImport(specifier, importer) {
      console.log('resolveDynamicImport', specifier, importer);
    },
    async buildEnd() {
      console.log('buildEnd');
    }
  }
}
export default build;

2.2.2 rollup.config.js #

rollup.config.js

+import build from './plugins/rollup-plugin-build.js';
export default {
  input: "./src/index.js",
  output: [{
    dir: 'dist',
  }],
  plugins: [
+   build()
  ]
}

2.2.4 options #

字段
Type (options: InputOptions) => InputOptions null
Kind async, sequential
Previous Hook 这是构建阶段的第一个钩子
Next Hook buildStart

2.2.5 buildStart #

字段
Type (options: InputOptions) => void
Kind async, parallel
Previous Hook options
Next Hook resolveId并行解析每个入口点

build\plugin-buildStart.js

export default function buildStart() {
  return {
    name: 'buildStart',
    buildStart(InputOptions) {
      console.log('buildStart', InputOptions);
    }
  };
}

2.2.6 resolveId #

字段
Type (source, importer) => string false null
Kind async, first
Previous Hook buildStart(如果我们正在解析入口点),moduleParsed(如果我们正在解析导入),或者作为resolveDynamicImport的后备方案。此外,这个钩子可以在构建阶段通过调用插件钩子触发。emitFile发出一个入口点,或在任何时候通过调用此。resolve可手动解析id
Next Hook 如果解析的id尚未加载,则load,否则buildEnd

build\plugin-polyfill.js

//我们在polyfill id前面加上\0,告诉其他插件不要尝试加载或转换它
const POLYFILL_ID = '\0polyfill';
const PROXY_SUFFIX = '?inject-polyfill-proxy';

export default function injectPolyfillPlugin() {
  return {
    name: 'inject-polyfill',
    async resolveId(source, importer, options) {
      if (source === POLYFILL_ID) {
        //重要的是,对于polyfills,应始终考虑副作用
        //否则,使用`treeshake.moduleSideEffects:false`可能会阻止包含polyfill
        return { id: POLYFILL_ID, moduleSideEffects: true };
      }
      if (options.isEntry) {
        //确定实际的入口是什么。我们需要skipSelf来避免无限循环。
        const resolution = await this.resolve(source, importer, { skipSelf: true, ...options });
        //如果它无法解决或是外部的,只需返回它,这样Rollup就可以显示错误
        if (!resolution || resolution.external) return resolution;
        //在代理的加载钩子中,我们需要知道入口是否有默认导出
        //然而,在那里,我们不再有完整的“解析”对象,它可能包含来自其他插件的元数据,这些插件只在第一次加载时添加
        //仅在第一次加载时添加。因此我们在这里触发加载。
        const moduleInfo = await this.load(resolution);
        //我们需要确保即使对于treeshake来说,原始入口点的副作用也得到了考虑。moduleSideEffects:false。
        //moduleSideEffects是ModuleInfo上的一个可写属性
        moduleInfo.moduleSideEffects = true;
        //重要的是,新入口不能以\0开头,并且与原始入口具有相同的目录,以免扰乱相对外部导入的生成
        //此外,保留名称并在末尾添加一个“?查询”可以确保preserveModules将为该条目生成原始条目名称
        return `${resolution.id}${PROXY_SUFFIX}`;
      }
      return null;
    },
    load(id) {
      if (id === POLYFILL_ID) {
        // 替换为实际的polyfill import '@babel/polyfill'
        return "console.log('polyfill');";
      }
      if (id.endsWith(PROXY_SUFFIX)) {
        const entryId = id.slice(0, -PROXY_SUFFIX.length);
        //我们知道ModuleInfo.hasDefaultExport是可靠的,因为我们在等待在resolveId中的this.load
        // We know ModuleInfo.hasDefaultExport is reliable because we awaited this.load in resolveId
        const { hasDefaultExport } = this.getModuleInfo(entryId);
        let code =
          `import ${JSON.stringify(POLYFILL_ID)};` + `export * from ${JSON.stringify(entryId)};`;
        //命名空间重新导出不会重新导出默认值,因此我们需要在这里进行特殊处理
        if (hasDefaultExport) {
          code += `export { default } from ${JSON.stringify(entryId)};`;
        }
        return code;
      }
      return null;
    }
  };
}

2.2.7 load #

字段
(id) => string null
Kind async, first
Previous Hook 解析加载id的resolveIdresolveDynamicImport。此外,这个钩子可以在任何时候从插件钩子中通过调用this.load来触发预加载与id对应的模块
Next Hook transform可在未使用缓存或没有使用相同代码的缓存副本时转换加载的文件,否则应使用TransformCachedModule

2.2.8 transform #

字段
Type (code, id) => string
Kind async, sequential
Previous Hook load 当前处理的文件的位置。如果使用了缓存,并且有该模块的缓存副本,那么如果插件为该钩子返回true,则应shouldTransformCachedModule
Next Hook moduleParsed 一旦文件被处理和解析,模块就会被解析
npm install rollup-pluginutils @rollup/plugin-babel @babel/core @babel/preset-env  -D

plugins\rollup-plugin-babel.js
```js
import { createFilter } from 'rollup-pluginutils'
import babel from '@babel/core'
function plugin(pluginOptions = {}) {
  const defaultExtensions = ['.js', '.jsx']
  const { exclude, include, extensions = defaultExtensions } = pluginOptions;
  const extensionRegExp = new RegExp(`(${extensions.join('|')})$`)
  const userDefinedFilter = createFilter(include, exclude);
  const filter = id => extensionRegExp.test(id) && userDefinedFilter(id);
  return {
    name: 'babel',
    async transform(code, filename) {
      if (!filter(filename)) return null;
      let result = await babel.transformAsync(code);
      return result
    }
  }
}
export default plugin

2.2.9 shouldTransformCachedModule #

字段
Type ({id, code, ast, resoledSources, moduleSideEffects, syntheticNamedExports) => boolean
Kind: async, first
Previous Hook load 加载缓存文件以将其代码与缓存版本进行比较的位置
Next Hook moduleParsed if no plugin returns true, otherwise transform.
npx rollup -c -w
shouldTransformCachedModule
transform
moduleParsed

shouldTransformCachedModule
moduleParsed

2.2.10 moduleParsed #

字段
Type (moduleInfo: ModuleInfo) => void
Kind: async, parallel
Previous Hook transform 转换当前处理的文件的位置
Next Hook resolveIdresolveDynamicImport 并行解析所有发现的静态和动态导入(如果存在),否则buildEnd

2.2.10 resolveDynamicImport #

字段
Type (specifier, importer) => string
Kind async, first
Previous Hook moduleParsed 已为导入文件分配模块
Next Hook load 如果钩子使用尚未加载的id ,如果动态导入包含字符串且钩子未解析,请加载resolveId,否则为buildEnd

index.js

import('./msg.js').then(res => console.log(res))

2.2.11 buildEnd #

字段
Type (error) => void
Kind async, parallel
Previous Hook moduleParsed, resolveId or resolveDynamicImport.
Next Hook outputOptions 输出生成阶段的输出,因为这是构建阶段的最后一个挂钩

2.3 Output Generation Hooks #

2.3.1 rollup-plugin-generation.js #

plugins\rollup-plugin-generation.js

function generation() {
  return {
    name: 'rollup-plugin-generation',
    //这个钩子是同步的,不能加async
    outputOptions(outputOptions) {
      console.log('outputOptions');
    },
    renderStart() {
      console.log('renderStart');
    },
    banner() {
      console.log('banner');
    },
    footer() {
      console.log('footer');
    },
    intro() {
      console.log('intro');
    },
    outro() {
      console.log('outro');
    },
    renderDynamicImport() {
      console.log('renderDynamicImport');
    },
    augmentChunkHash() {
      console.log('augmentChunkHash');
    },
    resolveFileUrl() {
      console.log('resolveFileUrl');
    },
    resolveImportMeta() {
      console.log('resolveImportMeta');
    },
    renderChunk() {
      console.log('renderChunk');
    },
    generateBundle() {
      console.log('generateBundle');
    },
    writeBundle() {
      console.log('writeBundle');
    },
    renderError() {
      console.log('renderError');
    },
    closeBundle() {
      console.log('closeBundle');
    }
  }
}
export default generation;

2.3.2 rollup.config.js #

rollup.config.js

import build from './plugins/rollup-plugin-build.js';
+import generation from './plugins/rollup-plugin-generation.js';
export default {
  input: "./src/index.js",
  output: [{
    dir: 'dist',
  }],
  plugins: [
    build(),
+   generation()
  ]
}

2.3.3 outputOptions #

字段
Type (outputOptions) => null
Kind async, parallel
Previous Hook buildEnd如果这是第一次生成输出,否则为generateBundlewriteBundlerenderError取决于先前生成的输出。这是输出生成阶段的第一个钩子
Next Hook outputOptions 输出生成阶段的输出,因为这是构建阶段的最后一个挂钩

2.3.4 renderStart #

字段
Type (outputOptions, inputOptions) => void
种类 async, parallel
上一个钩子 outputOptions
下一个钩子 banner, footer, intro and outro 并行运行
字段
Type string (() => string)
Kind async, parallel
Previous Hook renderStart
Next Hook 针对每个动态导入表达式 renderDynamicImport
字段
Type string (() => string)
Kind async, parallel
Previous Hook renderStart
Next Hook 针对每个动态导入表达式 renderDynamicImport

2.3.7 intro #

字段
Type string (() => string)
Kind async, parallel
Previous Hook renderStart
Next Hook 针对每个动态导入表达式 renderDynamicImport

2.3.8 outro #

字段
Type string (() => string)
Kind async, parallel
Previous Hook renderStart
Next Hook 针对每个动态导入表达式 renderDynamicImport

2.3.9 renderDynamicImport #

字段
Type ({format, moduleId, targetModuleId, customResolution}) => {left: string, right: string}
Kind async, parallel
Previous Hook banner , footer, intro, outro
Next Hook augmentChunkHash对于每个在文件名中包含哈希的块

plugins\rollup-plugin-renderDynamicImport.js

export default function dynamicImportPolyfillPlugin() {
  return {
    name: 'dynamic-import-polyfill',
    renderDynamicImport() {
      return {
        left: 'dynamicImportPolyfill(',
        right: ', import.meta.url)'
      };
    }
  };
}
dynamicImportPolyfill('./msg-ca034dda.js', import.meta.url).then(res => console.log(res.default));
function dynamicImportPolyfill(filename, url) {
  return new Promise((resolve) => {
    const script = document.createElement("script");
    script.type = "module";
    script.onload = () => {
      resolve(window.mod);
    };
    const absURL = new URL(filename, url).href;
    console.log(absURL);
    const blob = new Blob([
      `import * as mod from "${absURL}";`,
      ` window.mod = mod;`], { type: "text/javascript" });
    script.src = URL.createObjectURL(blob);
    document.head.appendChild(script);
  });
}

2.3.9 augmentChunkHash #

字段
Type (chunkInfo: ChunkInfo) => string
Kind sync, sequential
Previous Hook renderDynamicImport针对每个动态导入表达式
Next Hook resolveFileUrl对于每次使用import.meta.ROLLUP_FILE_URL_referenceIdresolveImportMeta所有其他访问import.meta

2.3.10 resolveFileUrl #

字段
Type ({chunkId, fileName, format, moduleId, referenceId, relativePath}) => string
Kind sync, first
Previous Hook augmentChunkHash对于在文件名中包含哈希的每个块
Next Hook renderChunk对于每个块

src\index.js

import logger from 'logger'
console.log(logger);

plugins\rollup-plugin-resolveFileUrl.js

export default function resolveFileUrl() {
  return {
    name: 'resolveFileUrl',
    resolveId(source) {
      if (source === 'logger') {
        return source;
      }
    },
    load(importee) {
      if (importee === 'logger') {
        let referenceId = this.emitFile({ type: 'asset', source: 'console.log("logger")', fileName: "logger.js" });
        return `export default import.meta.ROLLUP_FILE_URL_${referenceId}`;
      }
    },
    resolveFileUrl({ chunkId, fileName, format, moduleId, referenceId, relativePath }) {//import.meta.url
      return `new URL('${fileName}', document.baseURI).href`;
    }
  };
}

2.3.11 resolveImportMeta #

字段
Type (property, {chunkId, moduleId, format}) => string
Kind sync, first
Previous Hook augmentChunkHash对于在文件名中包含哈希的每个块
Next Hook renderChunk对于每个块

2.3.12 renderChunk #

字段
Type (code, chunk, options) => string
Kind async, sequential
Previous Hook resolveFileUrl对于 . 的每次使用import.meta.ROLLUP_FILE_URL_referenceIdresolveImportMeta所有其他访问import.meta
Next Hook generateBundle

2.3.13 generateBundle #

字段
Type (options, bundle, isWrite) => void
Kind async, sequential
Previous Hook renderChunk对于每个块
Next Hook writeBundle如果输出是通过生成的,否则这是输出生成阶段的最后一个钩子,如果生成另一个输出bundle.write(),可能会再次跟随outputOptions
npm i dedent

plugins\rollup-plugin-html.js

import dedent from 'dedent';
export default function html() {
  return {
    name: 'html',
    generateBundle(options, bundle) {
      let entryName;
      for (let fileName in bundle) {
        let assetOrChunkInfo = bundle[fileName];
        //console.log(fileName, assetOrChunkInfo);
        if (assetOrChunkInfo.isEntry) {
          entryName = fileName;
        }
      }
      this.emitFile({
        type: 'asset',
        fileName: 'index.html',
        source: dedent`
        <!DOCTYPE html>
        <html>
        <head>
          <meta charset="UTF-8">
          <title>rollup</title>
         </head>
        <body>
          <script src="${entryName}" type="module"></script>
        </body>
        </html>`
      });
    }
  };
}

2.3.14 writeBundle #

字段
Type (options,bundle) => void
Kind async, parallel
Previous Hook generateBundle
Next Hook 如果被调用,这是输出生成阶段的最后一个钩子,如果生成另一个输出,可能会再次跟随outputOptions

2.3.15 renderError #

字段
Type (error: Error) => void
Kind async, parallel
Previous Hook renderStart从到 的任何钩子renderChunk
Next Hook outputOptions如果它被调用,这是输出生成阶段的最后一个钩子,如果生成另一个输出,可能会再次跟随

2.3.16 closeBundle #

字段
Type closeBundle: () => Promise void
Kind async, parallel
Previous Hook buildEnd 如果有构建错误.否则何时bundle.close()被调用,在这种情况下,这将是最后一个被触发的钩子。

3.Plugin Context #

3.1 this.emitFile #

type EmittedChunk = {
  type: 'chunk';
  id: string;
  name?: string;
  fileName?: string;
};

type EmittedAsset = {
  type: 'asset';
  name?: string;
  fileName?: string;
  source?: string | Uint8Array;
};

3.2 this.load #

3.3 this.load #

3.4 this.resolve #

4.实战案例 #

4.1 @rollup/plugin-commonjs #

4.1.1 安装 #

npm install  @rollup/plugin-commonjs   --save

4.1.2 src\index.js #

src\index.js

import catValue from './cat.js';
console.log(catValue);

4.1.3 src\cat.js #

src\cat.js

module.exports = 'catValue';

4.1.4 rollup-plugin-commonjs.js #

plugins\rollup-plugin-commonjs.js

import { createFilter } from 'rollup-pluginutils'
import MagicString from 'magic-string';
import { walk } from 'estree-walker';
import path from 'path';
export default function (pluginOptions = {}) {
  const defaultExtensions = ['.js', '.jsx']
  const { exclude, include, extensions = defaultExtensions } = pluginOptions;
  const extensionRegExp = new RegExp(
    `(${extensions.join('|')})$`
  )
  const userDefinedFilter = createFilter(include, exclude);
  const filter = id => extensionRegExp.test(id) && userDefinedFilter(id);
  return {
    name: 'commonjs',
    transform(code, id) {
      if (!filter(id)) return null;
      const result = transformAndCheckExports(this.parse, code, id)
      return result;
    }
  }
}
function transformAndCheckExports(parse, code, id) {
  const { isEsModule, ast } = analyzeTopLevelStatements(parse, code, id);
  if (isEsModule) {
    return null;
  }
  return transformCommonjs(code, id, ast)
}
function getKeypath(node) {
  const parts = [];
  while (node.type === 'MemberExpression') {
    parts.unshift(node.property.name);
    node = node.object;
  }
  if (node.type !== 'Identifier') return null;
  const { name } = node;
  parts.unshift(name);
  return { name, keypath: parts.join('.') };
}
function analyzeTopLevelStatements(parse, code) {
  const ast = parse(code);
  let isEsModule = false;
  for (const node of ast.body) {
    switch (node.type) {
      case 'ExportDefaultDeclaration':
        isEsModule = true;
        break;
      case 'ExportNamedDeclaration':
        isEsModule = true;
        break;
      case 'ImportDeclaration':
        isEsModule = true;
        break;
      default:
    }
  }
  return { isEsModule, ast };
}
function transformCommonjs(code, id, ast) {
  const magicString = new MagicString(code);
  const exportDeclarations = [];
  let moduleExportsAssignment;
  walk(ast, {
    enter(node) {
      switch (node.type) {
        case 'AssignmentExpression':
          if (node.left.type === 'MemberExpression') {
            const flattened = getKeypath(node.left);
            if (flattened.keypath === 'module.exports') {
              moduleExportsAssignment = node;
            }
          }
          break;
        default:
          break;
      }
    }
  });
  const { left } = moduleExportsAssignment;
  const exportsName = path.basename(id, path.extname(id));
  magicString.overwrite(left.start, left.end, exportsName);
  magicString.prependRight(left.start, 'var ');
  exportDeclarations.push(`export default ${exportsName};`);
  const exportBlock = `\n\n${exportDeclarations.join('\n')}`;
  magicString.trim().append(exportBlock);
  return {
    code: magicString.toString()
  }
}

4.1.5 rollup.config.js #

rollup.config.js

//import babel from '@rollup/plugin-babel'
//import babel from './plugins/rollup-plugin-babel.js'
//import commonjs from '@rollup/plugin-commonjs'
+import commonjs from './plugins/rollup-plugin-commonjs'
export default {
  input: "./src/index.js",
  output: {
    dir: 'dist'
  },
  plugins: [
    //babel(),
+   commonjs()
  ]
}

4.2 @rollup/plugin-node-resolve #

4.2.1 安装 #

npm install @rollup/plugin-node-resolve check-is-array -D
(!) Unresolved dependencies
https://rollupjs.org/guide/en/#warning-treating-module-as-external-dependency
isarray (imported by src/index.js)

4.2.2 src\index.js #

src\index.js

import isArray from 'check-is-array';
console.log(isArray);

4.2.3 rollup-plugin-node-resolve.js #

plugins\rollup-plugin-node-resolve.js

import path from 'path';
import Module from 'module';
function resolve() {
  return {
    name: 'resolve',
    //因为我们要改造根据模块的名称查找模所路径的逻辑
    async resolveId(importee, importer) {
      //如果是相对路径,则走默认逻辑
      if (importee[0] === '.' || path.isAbsolute(importee)) {
        return null;
      }
      let location = Module.createRequire(path.dirname(importer)).resolve(importee);
      console.log(location);
      return location;
    }
  }
}
export default resolve;

4.3 @rollup/plugin-alias #

4.3.1 rollup.config.js #

rollup.config.js

//import build from './plugins/rollup-plugin-build.js';
//import polyfill from './plugins/rollup-plugin-inject-polyfill.js';
//import babel from './plugins/rollup-plugin-babel.js';
//import generation from './plugins/rollup-plugin-generation.js';
//import importPolyFill from './plugins/rollup-plugin-import-polyfill.js';
//import commonjs from '@rollup/plugin-commonjs';
//import commonjs from './plugins/rollup-plugin-commonjs';
//import resolve from '@rollup/plugin-node-resolve';
import resolve from './plugins/rollup-plugin-node-resolve.js';
//import alias from '@rollup/plugin-alias';
import alias from './plugins/rollup-plugin-alias.js';

export default {
  input: './src/index.js',
  //watch: true,
  output: {
    //file: 'dist/main.js',
    dir: 'dist'
  },
  plugins: [
    resolve(),
    alias({
      entries: [
        { find: './xx.js', replacement: 'check-is-array' }
      ]
    }),
  ],
  watch: {
    clearScreen: false
  }
}

4.3.2 rollup-plugin-alias.js #

plugins\rollup-plugin-alias.js

function matches(pattern, importee) {
    if (pattern instanceof RegExp) {
        return pattern.test(importee);
    }
    if (importee.length < pattern.length) {
        return false;
    }
    if (importee === pattern) {
        return true;
    }
    return importee.startsWith(pattern + '/');
}

function alias(options = {}) {
    const { entries } = options;
    if (entries.length === 0) {
        return {
            name: 'alias',
            resolveId: () => null
        };
    }
    return {
        name: 'alias',
        resolveId(importee, importer) {
            if (!importer) {
                return null;
            }
            const matchedEntry = entries.find((entry) => matches(entry.find, importee));
            if (!matchedEntry) {
                return null;
            }
            const updatedId = importee.replace(matchedEntry.find, matchedEntry.replacement);
            //调用this.resolve意味着重新解析
            return this.resolve(updatedId, importer, Object.assign({ skipSelf: true }))
                .then((resolved) => resolved || { id: updatedId });
        }
    };
}
export default alias;