kaz.dev

11ty + ts + rollup

tag: 11ty, javascript, typescript, rollup


TL;DR

https://github.com/kobakazu0429/11ty-typescript-rollup





解説

11tyでtsを使うには各ファイルから型を捨てつつcjsにできれば良い
問題は近年ライブラリのesm only化が進んでいることで、node_modules側もトランスパイルしておく必要がある
FYI: Pure ESM package

単純にesm→cjsにするとentryファイルなどはimport→requireにいい感じにinteropされるが、その先は何もされていない





import { VFile } from "vfile"
const file = new VFile("test");

これをesbuildなどでビルドすると

build.js
const esbuild = require("esbuild");

async function build() {
  const option = {
    entryPoints: ["entry.ts"],
    outdir: "output",
    format: "cjs"
  };

  await esbuild.build(option);
}

build();
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __markAsModule = (target) => __defProp(target, "__esModule", { value: true });
var __reExport = (target, module2, desc) => {
  if (module2 && typeof module2 === "object" || typeof module2 === "function") {
    for (let key of __getOwnPropNames(module2))
      if (!__hasOwnProp.call(target, key) && key !== "default")
        __defProp(target, key, { get: () => module2[key], enumerable: !(desc = __getOwnPropDesc(module2, key)) || desc.enumerable });
  }
  return target;
};
var __toModule = (module2) => {
  return __reExport(__markAsModule(__defProp(module2 != null ? __create(__getProtoOf(module2)) : {}, "default", module2 && module2.__esModule && "default" in module2 ? { get: () => module2.default, enumerable: true } : { value: module2, enumerable: true })), module2);
};
var import_vfile = __toModule(require("vfile"));
const file = new import_vfile.VFile("test");

となる

最後の2行がメインの部分
__toModule(require("vfile"))に注目する
一見トランスパイルで読み込めそうだが、vfile自体がバンドルされていない(esmで配布)なので、次のようなエラーが出る

node:internal/modules/cjs/loader:1112
      throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath);
      ^

Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /Users/kazu/workspace/vfile/node_modules/vfile/index.js
require() of ES modules is not supported.
require() of /Users/kazu/workspace/vfile/node_modules/vfile/index.js from /Users/kazu/workspace/vfile/output/example.js is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules.
Instead rename index.js to end in .cjs, change the requiring code to use import(), or remove "type": "module" from /Users/kazu/workspace/vfile/node_modules/vfile/package.json.

    at new NodeError (node:internal/errors:370:5)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1112:13)
    at Module.load (node:internal/modules/cjs/loader:975:32)
    at Function.Module._load (node:internal/modules/cjs/loader:816:12)
    at Module.require (node:internal/modules/cjs/loader:999:19)
    at require (node:internal/modules/cjs/helpers:93:18)
    at Object. (/Users/kazu/workspace/vfile/output/example.js:19:31)
    at Module._compile (node:internal/modules/cjs/loader:1095:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1124:10)
    at Module.load (node:internal/modules/cjs/loader:975:32) {
  code: 'ERR_REQUIRE_ESM'
}

解決策として、vfileごとバンドルしてしまう手などが考えられる





解決手順

Googleで検索

調べているとUsing esbuild with 11tyという記事に出会った。
これは11tyのイベント(https://www.11ty.dev/docs/events/)をフックにesbuildでビルドするもの。

BEFOREBUILD
The beforeBuild event runs every time Eleventy starts building, so it will run before the start of each stand-alone build, as well as each time building starts as either part of --watch or --serve. To use it, attach the event handler to your Eleventy config:

module.exports = function (eleventyConfig) {
 eleventyConfig.on('beforeBuild', () => {
   // Run me before the build starts
 });
};

beforeBuildがあるのでここでビルドしたら良いのではと考えた

その方向で実装してみると、記事データの取得APIがビルドの前に走ってしまい、こける問題が発生
これはhttps://github.com/11ty/eleventy/issues/1488と類似の問題だと思う

ただこのissueは非アクティブで、まだ自分の英語力に自信がないので

のような意識があり、見るだけで終わってしまった
理想はソースコードを読んだり、PRを投げることだが、そこに至れない

これは今後枷になりそうなので、どうにかしたいと思ってる...





11ty + ts を目指す他のプロジェクトを探す

https://github.com/jhukdev/11tyby などが見つかった
tsだけなら問題なく動くが、esm onlyなライブラリを試すとうまくいかない

今思うと少し手を加えたら動いたかも





諦めて頑張る

本題

サンプルリポジトリはtl;drでも書いたがここ

メインのrollup.config.jsを以下に示す

import path from "path";
import glob from "tiny-glob/sync";

import nodeResolve from "@rollup/plugin-node-resolve";
import typescript from "rollup-plugin-typescript2";
import commonjs from "@rollup/plugin-commonjs";
import json from "@rollup/plugin-json";

const tsFiles = [
  ...glob(
    "./src/**/*.ts"
  ),
].map(p => path.resolve(path.join(__dirname, p)));

const entryFiles = tsFiles.map(v => {
  const filename = path.basename(v, path.extname(v)) + ".js"
  const outputPath = path.join(__dirname, "build", path.relative(path.join(__dirname, "src"), path.dirname(v)))
  return { entry: v, outputPath, filename }
});

const config = /** @type import("rollup").NormalizedInputOptions */ ({
  plugins: [
    typescript(),
    nodeResolve({
      preferBuiltins: true,
    }),
    commonjs(),
    json(),
  ]
});

export default entryFiles.map(f => ({
  ...config,
  input: f.entry,
  output: [{
    format: "cjs",
    file: path.join(f.outputPath, f.filename),
    exports: "auto"
  }]
}));

あまり大したことはしていなくてentryファイルをglobで列挙して、それをディレクトリ構造を維持しつつ、build/に出力するだけ
また各ファイルにライブラリをバンドルすることでesm onlyでも問題なし

ただ、node-canvasを使っておりこちらには.nodeというファイルが含まれていた(自分が見たのは初めて)
調べてみるとnative moduleというものでc++をnodeで実行できるようにしたものっぽい
rollup-plugin-nativesというpluginもあったが上手に動かなかったのでexternalにいれて、バンドルしないことにした(node-canvasがesm onlyではないため出来た)

const config = {
  ...
  external: [
    "canvas"
  ]
}

rollupでバンドルしたコードは全てサーバーサイドなどで動くのでminifyはしていない
また自分の環境だとファイル数が少し多いせいかそれなりにビルド時間がかかっている

ここら辺をすれば多少はマシになりそう
あとは頻繁にビルド(ts→cjs)するものでもないので気にし過ぎなくてもいいかも





最後に

この記事が公開された段階ではまだblogに適用はしていないので、今度運用していく中で何かしら不具合に出会うかも
そしたら追記します





2021/08/06 追記

tl;dr

tsxを使えるようにした
リポジトリは同じ
PRはこれ #1

解説

@rollup/plugin-babelbabel-plugin-transform-jsx-to-htmでtsxをhtmを使った形に変換してる
さらにpreact-render-to-stringを使って文字列化してるだけ

あとは型エラーが出るのでとりあえず @types/react を入れたけど、正解がわからないので誰か教えてください