Rollup Plugin Name: commonjs Rollup Plugin Version: 13.0.0
Данный плагин (и Rollup in расширениях) работает настолько seamlessly насколько это возможно как с тем кодом который он генерирует сам, так и другими инструментами в экосистеме.
Есть большое количество интеграционных вопросов, о чем написано ниже. Описание будет в виде обзра того как плагин в принципе работает и более-менее полных описаний всех ситуации инстеграции разных инструментов и текущее поведение. Идея этого документа в том, чтобы бать базу для обсуждения и обозначения связаных проблем которые могут быть решены в рамках разумного. Я с удовольствием позапочусь об имплементации всех необходимых изменений в ядре Rollup, но я так же буду счастлив если кто-то поучаствует в улучшении этого плагина вместе со мной.
Этот список ОЧЕНЬ длинный, и я постараюсь обновлять его по мере принятых решений. Я так же постараюсь добавить в скором будущем собранный результат [compiled summary] в конце.
Я убежден что все предположения должны быть проверены, так что я добавил примеры реального кода почти ко всему. Для генерации этих отформатированных примеов я использовал следующие настройки.
Rollup config
// "rollup.config.js"
import path from "path";
import cjs from "@rollup/plugin-commonjs";
const inputFiles = Object.create(null);
const transformedFiles = Object.create(null);
const formatFiles = (files) =>
Object.keys(files)
.map((id) => `// ${JSON.stringify(id)}\n${files[id].code}\n`)
.join("\n");
const formatId = (id) => {
const [, prefix, modulePath] = /(\0)?(.*)/.exec(id);
return `${prefix || ""}${path.relative(".", modulePath)}`;
};
export default {
input: "main",
plugins: [
{
name: "collect-input-files",
transform(code, id) {
if (id[0] !== "\0") {
inputFiles[formatId(id)] = { code: code.trim() };
}
},
},
cjs(),
{
name: "collect-output",
transform(code, id) {
// Never display the helpers file
if (id !== "\0commonjsHelpers.js") {
transformedFiles[formatId(id)] = { code: code.trim() };
}
},
generateBundle(options, bundle) {
console.log(`<details>
<summary>Input</summary>
\`\`\`js
${formatFiles(inputFiles)}
\`\`\`
</details>
<details>
<summary>Transformed</summary>
\`\`\`js
${formatFiles(transformedFiles)}
\`\`\`
</details>
<details>
<summary>Output</summary>
\`\`\`js
${formatFiles(bundle)}
\`\`\`
</details>`);
},
},
],
output: {
format: "es",
file: "bundle.js",
},
};
Плагину нужно позаботится о том чтобы все работало гладко [seamlessly], разрешать require
вызовы содержимым module.exports
запрошеного модуля. Здесь нет никакой проблемы интеграции, суть этой секции только в том чтобы обозначить как имеенно плагин работает и обрабаывает все возможные ситуации перед тем как мы перейдет к проблемным местам где он может быть улучшен.
При трансформации require
раскалдывается на два импорта: "пустой" импорт исходного модуля и импорт прокси-модуля, в котром реализована фактическая привязка. Причина по которой мы импортируем пустой импорт оригинального файла в том, что нам нужно запустить загрузку и преобразование оригинального файла, что позволит нам узнать заранее - это CJS или ESM и создать соотвествующий формату прокси-файл.
В свою очередь в эспортируемом файле эспорта тоже генерируется два: то что было записано в module.exports
экспортируется дважды - как default
и как __moduleExports
.
Прокси еще раз экспортирует __moduleExports, но уже как default (о ситуациях где прокси ведет себя чучуть иначе, описано в секции про импорт ESM из CJS).
Input
// "main.js"
const dep = require("./dep.js");
console.log(dep);
// "dep.js"
module.exports = "foo";
Transformed
// "main.js"
import "./dep.js";
import dep from "./dep.js?commonjs-proxy";
console.log(dep);
// "dep.js"
var dep = "foo";
export default dep;
export { dep as __moduleExports };
// "\u0000dep.js?commonjs-proxy"
import { __moduleExports } from "/Users/lukastaegert/Github/rollup-playground/dep.js";
export default __moduleExports;
Output
// "main.js"
var dep = "foo";
console.log(dep);
В этом сценарии, Rollup создает искусственный обьект в который помещены сразу все присвоеные module.exports
значения. Это очень эффективно в отличии от присваивания свойств обьекту по одному в рантайме. Благодоря этому действию движок сможет сразу же оптимизировать обьект для быстрого доступа к обьекту в рантайме. Этот обьект эспортируется дважды default
и под именем __moduleExports
. Дополнительно к этому все свойства так же эспоритруются по одному под своими именами (прим. переводчика - видимо для treeshaking оптимизации).
Input
// "main.js"
const dep = require("./dep.js");
console.log(dep);
// "dep.js"
module.exports.foo = "foo";
exports.bar = "bar";
exports.default = "baz";
Transformed
// "main.js"
import "./dep.js";
import dep from "./dep.js?commonjs-proxy";
console.log(dep);
// "dep.js"
var foo = "foo";
var bar = "bar";
var _default = "baz";
var dep = {
foo: foo,
bar: bar,
default: _default,
};
export default dep;
export { dep as __moduleExports };
export { foo };
export { bar };
// "\u0000dep.js?commonjs-proxy"
import { __moduleExports } from "/Users/lukastaegert/Github/rollup-playground/dep.js";
export default __moduleExports;
Output
// "bundle.js"
var foo = "foo";
var bar = "bar";
var _default = "baz";
var dep = {
foo: foo,
bar: bar,
default: _default,
};
console.log(dep);
🐞 Баг 1: Повтороное присваивание одного итого же свойства сгенерирует два экспорта с одинаковым именем, что заставит Rollup выкинуть ошибку "Duplicate export ".
(Прим. переводчика: имеется ввиду такой код)
exports.bar = "bar";
exports.bar = 3;
Есть много случаев где плагин деоптмизируется, например там где свойство читается вместо присваивания. В подобных случаях, createCommonjsModule
утилита используется для создания обертки вызывающй модуль более-менее в том виде, в котором это делает нода, т.е. выполнит модуль без попытки обнаружить какие-либо именованые эспорты.
Input
// "main.js"
const dep = require("./dep.js");
console.log(dep);
// "dep.js"
if (true) {
exports.foo = "foo";
}
Transformed
// "main.js"
import "./dep.js";
import dep from "./dep.js?commonjs-proxy";
console.log(dep);
// "dep.js"
import * as commonjsHelpers from "commonjsHelpers.js";
var dep = commonjsHelpers.createCommonjsModule(function (module, exports) {
if (true) {
exports.foo = "foo";
}
});
export default dep;
export { dep as __moduleExports };
// "\u0000dep.js?commonjs-proxy"
import { __moduleExports } from "/Users/lukastaegert/Github/rollup-playground/dep.js";
export default __moduleExports;
Output:
// "bundle.js"
function createCommonjsModule(fn, basedir, module) {
return (
(module = {
path: basedir,
exports: {},
require: function (path, base) {
return commonjsRequire(
path,
base === undefined || base === null ? module.path : base
);
},
}),
fn(module, module.exports),
module.exports
);
}
function commonjsRequire() {
throw new Error(
"Dynamic requires are not currently supported by @rollup/plugin-commonjs"
);
}
var dep = createCommonjsModule(function (module, exports) {
{
exports.foo = "foo";
}
});
console.log(dep);
На данный момент времени, плагин НЕ способен соблюсти одинаковый порядок выполнения очереди. Напротив, даже вложенные и условно выполняемые операторы require
(если они не написаны с помощью оператора if определенным образом) всегда поднимаются наверх.
Это отдельная ситуация, которую можно улучшить кардинальными изменениями, то есть заключением модулей в замыкании функций и их вызовом при первом использовании. Однако это отрицательно скажется на эффективности сгенерированного кода по сравнению со статус-кво, поэтому это должно происходить только тогда, когда это действительно необходимо.
К сожалению, невозможно определить, требуется ли модуль нестандартным способом, пока не будет построен весь граф модулей, поэтому в худшем случае может потребоваться оборачивать все модули. Rollup может немного помочь здесь, реализовав некоторый алгоритм встраивания, однако мы забегаем слишком далеко вперед (скажем, до этого еще как минимум год) и, скорее всего, будет применяться только в том случае, если модуль используется ровно в одном месте.
Другие подходы могут заключаться в том, что плагин анализирует фактический порядок выполнения чтобы убедится что первое использование не потребовало оборачивания, и это значило бы что в последствии можно не заботится об этом, но это кажется сликом сложным и подверженным ошибкам.
В любом случае, это в основном перечислено здесь для полноты, так как на самом деле не затрагивает тему взаимодействия, но гарантирует самосоятельную проблему. Вот пример для иллюстрации:
Input
// "main.js"
console.log("first");
require("./dep.js");
console.log("third");
false && require("./broken-conditional.js");
// There is special logic to handle this exact case, which is why
// "working-conditional.js" is not in the module graph, but it is not easily
// generalized.
if (false) {
require("./working-conditional.js");
}
// "dep.js"
console.log("second");
// "broken-conditional.js"
console.log("not executed");
Transformed
// "main.js"
import "./dep.js";
import "./broken-conditional.js";
import "./dep.js?commonjs-proxy";
import require$$1 from "./broken-conditional.js?commonjs-proxy";
console.log("first");
console.log("third");
false && require$$1;
// There is special logic to handle this exact case, which is why
// "working-conditional.js" is not in the module graph, but it is not easily
// generalized.
if (false) {
require("./working-conditional.js");
}
// "dep.js"
console.log("second");
// "\u0000dep.js?commonjs-proxy"
import * as dep from "/Users/lukastaegert/Github/rollup-playground/dep.js";
export default dep;
// "broken-conditional.js"
console.log("not executed");
// "\u0000broken-conditional.js?commonjs-proxy"
import * as brokenConditional from "/Users/lukastaegert/Github/rollup-playground/broken-conditional.js";
export default brokenConditional
Output:
// "bundle.js"
console.log("second");
console.log("not executed");
console.log("first");
console.log("third");