Skip to content

Instantly share code, notes, and snippets.

@Akiyamka
Last active August 27, 2020 14:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Akiyamka/2cd541ac5c62cfe27397a42bd2b8af62 to your computer and use it in GitHub Desktop.
Save Akiyamka/2cd541ac5c62cfe27397a42bd2b8af62 to your computer and use it in GitHub Desktop.

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",
  },
};

Импортирование из CJS в CJS

Плагину нужно позаботится о том чтобы все работало гладко [seamlessly], разрешать require вызовы содержимым module.exports запрошеного модуля. Здесь нет никакой проблемы интеграции, суть этой секции только в том чтобы обозначить как имеенно плагин работает и обрабаывает все возможные ситуации перед тем как мы перейдет к проблемным местам где он может быть улучшен.

Присваивание значения 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);

Присваивание нескольких значений exports или module.exports

В этом сценарии, 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;

Обработка неподдерживаемого использования module.exports или exports

Есть много случаев где плагин деоптмизируется, например там где свойство читается вместо присваивания. В подобных случаях, 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);

Inline require calls

На данный момент времени, плагин НЕ способен соблюсти одинаковый порядок выполнения очереди. Напротив, даже вложенные и условно выполняемые операторы 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");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment