跳至主要内容

[note] rollup-js 筆記

keywords: 打包, bundle

rollup.js @ official website

Quick Start

可以參考 Rollup 的這兩個啟動專案:

假設應用程式的進入點(entry point)是 main.js,打包後的檔案稱作 bundle.js,則

給瀏覽器(iife):

# compile to a <script> containing a self-executing function ('iife')
rollup main.js --file bundle.js --format iife

給 Node.js(cjs):

# compile to a CommonJS module ('cjs')
rollup main.js --file bundle.js --format cjs

同時給瀏覽器和 Node.js(umd):

# UMD format requires a bundle name
rollup main.js --file bundle.js --format umd --name "myBundle"

發佈 ES Modules

為了確保你的 ES Modules 可以立即 Node.js 或 Webpack 這類 CommonJS 的工具所使用,你可以使用 Rollup 來編譯成 UMD 或 CommonJS 的格式,接著在 package.json 中的 main 屬性指定該打包好的檔案,如果你的 package.json 中也有 module 這個屬性,則 ESM-aware 的工具(例如,Rollup 和 web pack 2+)將會直接載入 ES Module 的版本

Plugins

  • @rollup/plugin-commonjs:可以將以 CommonJS 寫的模組轉換成 ES6,如此就可以在 Rollup 中匯入 CommonJS 的模組。
  • @rollup/plugin-node-resolve:rollup 才會解析 node_modules 裡面的第三方套件,如果自己的套件中有用到其他第三方的套件時需安裝。
  • rollup-plugin-terser:可以用來壓縮打包後的檔案

Command Line Interface (CLI)

Command Line 可用參數

Command line flags @ rollup.js > CLI

# 直接透過 CLI 打包
# -f 是 --format <format>, -o 是 --file <output>
$ rollup index.js -f cjs -o bundle.js

# 根據 rollup.config.js 打包
# -c 是 --config <filename>,沒填 filename 預設會直接吃 rollup.config.js
$ rollup -c

設定檔(Configuration Files)

configuration files @ rollup.js > CLI

若下面情況一定要使用 Rollup 設定檔:

  1. 一個專案打包成多支 output 檔案
  2. 使用 Rollup 的外掛(plugin),像是 @rollup/plugin-node-resolve@rollup/plugin-commonjs

透過 rollup CLI 的 --config-c 可以執行設定檔 :

# Rollup 會讀取 rollup.config.js
rollup --config

# Rollup 會讀取自定義檔名的設定檔
rollup --config my.config.js

# .js and .mjs are supported
rollup --config my.config.mjs

Rollup 的設定檔名稱為 rollup.config.js,它用 ES Module 或 CommonJS 來寫都可以:

// rollup.config.js

import babel from 'rollup-plugin-babel';
import commonjs from 'rollup-plugin-commonjs';
import external from 'rollup-plugin-peer-deps-external';
import resolve from 'rollup-plugin-node-resolve';
import url from 'rollup-plugin-url';

import pkg from './package.json';

export default {
input: 'src/index.js',
output: [
{
file: pkg.main, // 會去讀取 package.json 的 main 欄位
format: 'cjs', // Common JS
sourcemap: true,
},
{
file: pkg.module, // 會去讀取 package.json 的 module 欄位
format: 'es', // ES Module
sourcemap: true,
},
{
name: 'myBundle',
file: 'dist/bundle.umd.js',
format: 'umd', // 給瀏覽器和 Node.js
sourcemap: true,
},
{
file: 'dist/bundle.js',
format: 'iife', // 給瀏覽器
sourcemap: true,
},
],
plugins: [
external(),
url({ exclude: ['**/*.svg'] }),
babel({
exclude: 'node_modules/**',
}),
resolve(),
commonjs(),
],
};

其他

With NPM Packages

keywords: @rollup/plugin-node-resolve, @rollup/plugin-commonjs

With NPM Packages @ GitHub

+ import resolve from '@rollup/plugin-node-resolve';
+ import commonjs from '@rollup/plugin-commonjs';

export default {
input: 'src/index.js',
output: [
{
file: 'dist/index.cjs.js',
format: 'cjs',
},
{
file: 'dist/index.esm.js',
format: 'esm',
},
],
+ plugins: [resolve(), commonjs()]
};

(!) Unresolved dependencies: @rollup/plugin-node-resolve

碰到這個問題時要考慮的情況是:「這個 modules 有沒有要打包到最後 build 好的檔案中」

  • 如果沒有,並沒有要放到最終 build 好的檔案中,而是只要當作依賴,使用者仍須自己安裝該套件的話,則把該套件放到 external 欄位中,如下一段「Peer Dependencies:告訴 rollup 哪些 module 不需要被打包」所述。
  • 如果要,這個 modules 執行後的結果是需要被打包進去的,那麼才需要使用 @rollup/plugin-node-resolve

需要安裝 @rollup/plugin-node-resolve,它會讓 rollup 知道如何找到外部的模組,並把它放入打包好的檔案中

如果沒有使用此 plugin 的話,安裝的套件不會一起被打包,使用者需要自行安裝

import answer from 'the-answer';

function index() {
console.log('answer ' + answer);
}

export default index;

若有使用此 plugin,則會一併把該套件或套件執行後的結果打包進去:

// 搭配 resolve 打包後的結果
var index = 42;

const A = `I am A, and the answer is ${index}`;

export { A as a };

沒有使用 resolve 的話,node_modules 的內容即使在打包後,仍會透過 import 的方式:

// 搭配 resolve 打包後的結果
import answer from 'the-answer';

const A = `I am A, and the answer is ${answer}`;

export default A;

@rollup/plugin-commonjs

@rollup/plugin-commonjs @ guide > with npm packages

由於打包過的檔案不一定支援 ESM 的使用,透過 @rollup/plugin-commonjs 可以把 CommonJS modules 轉成 ES6 後,放到 Rollup 打包好的檔案內。

備註

官方建議 commonjs 先執行後再執行 babel;resolve 先執行後再執行 commonjs(參考 #1148),但搭配 React 時,可能會需要先 babel 再 commonjs (參考 #2518#247' 和 #295)。

Peer Dependencies:告訴 rollup 哪些 module 不需要被打包

keywords: external

透過 external 可以告訴 rollup 哪些檔案不需要一起被打包,這些檔案通常是 peerDependencies,像是 lodash 或 React,是需要使用此套件的人自己安裝的。

// rollup.config.js
export default {
// 可以用陣列的方式來定義
external: ['lodash', 'styled-components', '/@babel/runtime/'], // 陣列不接受 wildcard 的用法

// 可以用 function 的方式,回傳 true 表示它是 external
external: (id) => /lodash/.test(id),
};

另外,external 也接受正規式的使用:

export const rollupExternal = [
'prop-types',
'react',
/react\/jsx-runtime/,
'styled-components',
/@od-lego\/*/, // all @od-lego packages will treat as external
/@babel\/runtime/,
];

export default {
input: 'src/index.js',
output: [
/* ... */
],
external: rollupExternal,
plugins: rollupPluginList,
};

Babel

用途

使用 Babel 的話可以把專案程式碼打包成支援更舊版本語法。

--- a/dist/index.esm.js
+++ b/dist/index.esm.js
@@ -2,15 +2,14 @@ var version = "1.0.0";

var index$1 = 42;

-// 沒使用 babel
-var getAnswer = () => {
- console.log(`the answer is ${index$1}`);
-};
+// 有使用 babel
+var getAnswer = (function () {
+ console.log("the answer is ".concat(index$1));
+});

function index () {
console.log('version ' + version);
getAnswer();
}
-
console.log('version ' + version);
getAnswer();

babelHelpers: runtime

設定 rollup.config.js,並套用 babelHelpers: 'runtime'

// rollup.config.js
import { babel } from '@rollup/plugin-babel';

export default {
plugins: [
babel({
+ babelHelpers: 'runtime',
}),
],
+ external: [/@babel\/runtime/],
};

使用 babelHelpers: 'runtime' 時,需要搭配 @babel/plugin-transform-runtime

// babel.config.json
{
"name": "mono-components-sandbox",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
+ "@babel/plugin-transform-runtime": "^7.14.3",
"@babel/preset-env": "^7.14.4",
"@babel/preset-react": "^7.13.13",
}
}

並且將該 plugin 設定到 babel.config.js 中:

// babel.config.js
module.exports = {
presets: [
'@babel/preset-env',
[
'@babel/preset-react',
{
runtime: 'automatic',
},
]
],
+ plugins: ['@babel/plugin-transform-runtime'],
};

TypeScript

搭配 @babel/preset-react 來編譯 TS

要使用 Babel 來編譯 TypeScript 檔案,需要先讓 rollup 認得 TS 的檔案,所以會需要先安裝 @rollup/plugin-node-resolve,並且修改 rollup.config.js

// rollup.config.js
import { babel } from '@rollup/plugin-babel';
+ import resolve from '@rollup/plugin-node-resolve';

+ const extensions = ['.js', '.jsx', '.ts', '.tsx'];

export default {
- input: 'lib/index.js'
+ input: 'lib/index', // js | ts
// ...
plugins: [
+ resolve({
+ extensions,
+ }),
babel({
rootMode: 'upward',
babelHelpers: 'runtime',
+ extensions,
}),
],
external: [/@babel\/runtime/],
};

如果是搭配 @babel/preset-react 而不是使用 @rollup/plugin-typescript 的話,需要額外透過 tsc 來產生 type definition。

透過 tsc 產生 type definition

  • scripts 中新增一個 build:ts 的指令來透過 tsc 產生 type definition 檔案
  • 建立 types 欄位,這個欄位很重要,目的是要讓載入這個 component 的使用者知道從哪裡去找到 Type Definition Files(參考:Including declarations in your npm package @ TypeScript)
// /packages/a/package.json
{
"name": "@mono-sandbox/a",
"version": "0.0.4",
"module": "dist/index.esm.js",
+ "types": "dist/lib/index.d.ts",
"directories": {
"lib": "lib",
"test": "__tests__"
},
"scripts": {
"build": "rollup -c ../../rollup.config.js",
+ "build:ts": "tsc --emitDeclarationOnly --project tsconfig.json",
"clean": "rimraf dist *.tsbuildinfo",
"test": "echo \"Error: run tests from root\" && exit 1"
}
}

這時候執行 npm run build:ts 並不會正常執行,而是會出現下一段落的錯誤訊息:

安裝 @types/react

如果沒有安裝 @types/react 的話,會出現以下錯誤訊息:

Cannot find module 'react/jsx-runtime' or its corresponding type declarations.

Cannot find module react/jsx-runtime

只需要安裝 @types/react 即可:

// package.json
{
"name": "mono-components-sandbox",
"devDependencies": {
"@rollup/plugin-node-resolve": "^13.0.0",
+ "@types/react": "^17.0.8",
"lerna": "^4.0.0",
"rimraf": "^3.0.2",
},
"workspaces": [
"packages/*"
]
}

常見問題

如果檔案中只有用到 default exports 會出現的警示:is implicitly using "default"

(!) Entry module "src/index.js" is implicitly using "default" export mode, which means for CommonJS output that its default export is assigned to "module.exports". For many tools, such CommonJS output will not be interchangeable with the original ES module. If this is intended, explicitly set "output.exports" to either "auto" or "default", otherwise you might want to consider changing the signature of "src/index.js" to use named exports only.

is implicitly using "default"

之所以會有這個警示,是因為在這支檔案中,只有使用 default exports,預設的情況下(exports: "auto"),rollup 會自動轉成 common JS 中 module.exports 的寫法,但因為沒有明確宣告這個 module 是使用 default 的方式 export,所以會跳出警示。

但如果是把 rollup 的設定改成 exports: "named" 的話,rollup 就會使用 named exports 的方式匯出,default 會變成一個變數,使用的人會變成需要使用 const foo = require('./modules').default 這種寫法。

為什麼不建議同時使用 default exports 和 named exports:Mixing named and default exports

主要的原因在於,如果在一支檔案中同時使用 default exports 和 named exports 時:

/* esm 的寫法 */
const A = `I am A`;

export const B = 'I am B';
export default A;

由於同時用了 named 和 default exports,因此 rollup 打包時會跳出警告:

(!) Mixing named and default exports

Mixing named and default exports

這時候如果透過 rollup 想要打包成 CJS 的檔案時,它沒有辦法辨認要匯出的 A 是 default export 可以直接拿,或者 default 其實是個變數。Compile 出來後會變這樣:

'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

const A = `I am A`;

const B = 'I am B';

exports.B = B;
exports.default = A; // default 變成一個 compile 出來的變數

因此當我們透過 common JS 想要取得 A 這個變數時,變成需要使用 cjs.default,會變得很奇怪也不不合理(但如果是用 bundler 試圖載入 cjs 的檔案就不會有這個問題,因為 bundler 會再做解析):

// 使用 CommonJS
const cjs = require('./packages/a');

console.log({ cjs }); // { cjs: { B: 'I am B', default: 'I am A' } }

但如果在檔案中只有使用 named exports 或 default exports 的話,則都不會有這樣的問題。

例如,只有 default exports:

// source:只有 default export
const A = `I am A`;
export default A;

// bundled 後
const A = `I am A`;
module.exports = A;

例如,只有 named exports:

Object.defineProperty(exports, '__esModule', { value: true });
const B = 'I am B';
exports.B = B;