Vivliostyle CLI v9+ スタイル読み込みの挙動の変化について

Vivliostyle CLI v9以降では、デフォルトの`workspaceDir`が`.vivliostyle`に変更されています。この変更により、「`workspaceDir`未指定」かつ「MarkdownのYAML FrontmatterにCSSファイルのパスを直接書く」という簡易な指定方法を使用している場合、挙動が大きく変わります。

## （従来の挙動）`workspaceDir`を指定しない場合

v9以前では、`workspaceDir`が未指定の場合、Markdownファイルと同じディレクトリに変換済みのHTMLが出力されていました。YAML Frontmatterの内容はほぼそのままHTMLの`head`要素に展開されるため、一見直感的にCSSを指定できました。また、CSSファイル内のローカルファイル参照もなりゆきで機能していました。

```
$ mkdir example-v8-without-workspaceDir && cd example-v8-without-workspaceDir
$ npm init --yes && npm install @vivliostyle/cli@8
$ npx vivliostyle init
```

```vivliostyle.config.js
// @ts-check
/** @type {import('@vivliostyle/cli').VivliostyleConfigSchema} */
const vivliostyleConfig = {
  title: "example-v8-without-workspaceDir",
  author: "u1f992",
  language: "ja",
  entry: ["manuscript.md"],
};

module.exports = vivliostyleConfig;
```

```manuscript.md
---
link:
  - rel: stylesheet
    href: style.css
---

Vivliostyle CLI v9以前では、YAML Frontmatterで一見直感的にCSSを指定できました。
```

```base.css
@page {
    size: A5;
}
```

```style.css
@import url("./base.css");
body {
    background-color: aqua;
}
```

```
$ npx vivliostyle build
$ tree -I node_modules
.
├── base.css
├── example-v8-without-workspaceDir.pdf
├── manuscript.html
├── manuscript.md
├── package-lock.json
├── package.json
├── publication.json
├── style.css
└── vivliostyle.config.js

1 directory, 9 files
$ head -n 8 manuscript.html
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>example-v8-without-workspaceDir</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="style.css">  ⇐位置関係が正しいのでとりあえず読み込まれていた
  </head>
```

## （従来の挙動）`workspaceDir`を指定する場合

ただしv9以前でも、`workspaceDir`を明示的に指定した場合はスタイルの解決に失敗していました。

```
$ cd .. && mkdir example-v8-with-workspaceDir && cd example-v8-with-workspaceDir
$ npm init --yes && npm install @vivliostyle/cli@8
$ npx vivliostyle init
$ cp ../example-v8-without-workspaceDir/{base.css,style.css,manuscript.md} .
```

```vivliostyle.config.js
// @ts-check
/** @type {import('@vivliostyle/cli').VivliostyleConfigSchema} */
const vivliostyleConfig = {
  title: "example-v8-with-workspaceDir",
  author: "u1f992",
  language: "ja",
  entry: ["manuscript.md"],
  workspaceDir: ".vivliostyle",
};

module.exports = vivliostyleConfig;
```

```
$ npx vivliostyle build
$ tree -a -I node_modules
.
├── .vivliostyle
│   ├── manuscript.html
│   └── publication.json
├── base.css
├── example-v8-with-workspaceDir.pdf
├── manuscript.md
├── package-lock.json
├── package.json
├── style.css
└── vivliostyle.config.js

2 directories, 9 files
$ head -n 8 .vivliostyle/manuscript.html
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>example-v8-with-workspaceDir</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="style.css">  ⇐位置関係が正しくないので読み込まれない
  </head>
```

<details>
<summary>npx vivliostyle previewの開発者ツールの出力</summary>

```
Failed to load resource: net::ERR_FILE_NOT_FOUND  / ... /style.css:1
```

</details>

CSSファイルをvivliostyle.config.jsの`theme`に指定することで、style.cssについては`workspaceDir`にコピーされるようになりますが、その中で参照されているbase.cssについては依然として正しく読み込まれません。

```diff_javascript
  // @ts-check
  /** @type {import('@vivliostyle/cli').VivliostyleConfigSchema} */
  const vivliostyleConfig = {
    title: "example-v8-with-workspaceDir",
    author: "u1f992",
    language: "ja",
+   theme: "style.css",
    entry: ["manuscript.md"],
    workspaceDir: ".vivliostyle",
  };
  
  module.exports = vivliostyleConfig;
```

なお`theme`を指定する場合はYAML Frontmatterは不要です。

```diff_markdown
- ---
- link:
-   - rel: stylesheet
-     href: style.css
- ---
- 
  Vivliostyle CLI v9以前では、YAML Frontmatterで一見直感的にCSSを指定できました。
```

```
$ npx vivliostyle build
$ head -n 8 .vivliostyle/manuscript.html
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>example-v8-with-workspaceDir</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="style.css">
  </head>
$ tree -a -I node_modules
.
├── .vivliostyle
│   ├── manuscript.html
│   ├── publication.json
│   └── style.css
├── base.css
├── example-v8-with-workspaceDir.pdf
├── manuscript.md
├── package-lock.json
├── package.json
├── style.css
└── vivliostyle.config.js
```

<details>
<summary>npx vivliostyle previewの開発者ツールの出力</summary>

```npx vivliostyle previewで立ち上がるChromiumの開発者ツール
Failed to load resource: net::ERR_FILE_NOT_FOUND  / ... /base.css:1
```

</details>

これはVivliostyle CLI側ではHTMLやCSS内部で参照されているファイルまでは解析しないという、考えてみれば当然の仕様による挙動です。仮にこれを実装しても不具合が多く発生しそうです。

https://github.com/vivliostyle/vivliostyle-cli/issues/537#issuecomment-2435021522

## v9における破壊的変更点

前述の通り、v9では未指定の場合の`workspaceDir`のデフォルト値が`.vivliostyle`に変更されました[^1]。これにより、`workspaceDir`を指定せずYAML FrontmatterでCSSのパスを直接指定したり、`theme`で（後述のテーマではなく）単純にCSSファイルを指定していた場合の挙動は、正しく壊れています。また、`workspaceDir`の内部のディレクトリ構造も変更されています。次の例で確認してください。

[^1]: なお筆者はこの決定を強く支持します。Vivliostyle CLIにおいて、Markdownを原稿とする出力はMarkdownそれ自体から透過的に得られると理解すべきものであり、HTMLファイルはあくまで中間表現です。我々は、実装の都合としてHTMLがファイルに出力された後に、そのファイルを直接見たり手を加えたりするべきではありません。それらは、v8.16以降で追加された`documentProcessor`や、v9以降で行われたVite統合によって、自動生成パイプラインに組み込まれるべき作業です。とはいえ、CSSを書くにはもちろん変換後のHTMLを理解する必要がありますし、他に手段がないこともありますので、現実はこの限りではありません。

## 解決方法

筆者が理解している筋の良い解決方法は、Vivliostyle CLIのスタイル指定の基本単位がテーマ＝Node.jsパッケージであると認識を改めることです（特にCSSが別ファイルを参照する場合）。

スタイルに対してディレクトリを切り、package.jsonに上記Issueで紹介されている最低限の指定を行います。`"name"`にはディレクトリ名など、`"main"`には起点となるCSSファイルを指定します。

```
$ cd .. && mkdir example-correct && cd example-correct
$ npm init --yes && npm install @vivliostyle/cli@9
$ npx vivliostyle init
$ mkdir css
$ cp ../example-v8-with-workspaceDir/{base.css,style.css} css/
$ cp ../example-v8-with-workspaceDir/manuscript.md .
```

```css/package.json
{
    "name": "css",
    "main": "style.css"
}
```

`theme`にローカルのディレクトリを指定する際は`./`を明示する点に注意してください。`theme`は本来、npmレジストリに公開されているパッケージ名を指定する箇所です（Vivliostyleが提供しているテーマとは、この記事で紹介している「テーマのディレクトリ」を所定の手順でnpmレジストリに公開したものに他なりません）。そのため、カレントディレクトリの`css`ディレクトリを意図して単に`theme: "css"`とすると、レジストリに公開されている[css](https://www.npmjs.com/package/css)パッケージがダウンロードされて失敗します。[参考](https://github.com/vivliostyle/vivliostyle-cli/issues/524)

```vivliostyle.config.js
// @ts-check
/** @type {import('@vivliostyle/cli').VivliostyleConfigSchema} */
const vivliostyleConfig = {
  title: "example-correct",
  author: "u1f992",
  language: "ja",
  theme: "./css",
  entry: ["manuscript.md"],
};

module.exports = vivliostyleConfig;
```

これで`./css`へのリンクが`.vivliostyle`以下に追加され、HTMLからも正しく参照されるようになります。

```
$ npx vivliostyle build
$ tree -a
.
├── .vivliostyle
│   ├── manuscript.html
│   ├── publication.json
│   └── themes
│       ├── node_modules
│       │   ├── .package-lock.json
│       │   └── css -> / ... /example-correct/css
│       ├── package-lock.json
│       └── package.json
├── css
│   ├── base.css
│   ├── package.json
│   └── style.css
├── example-correct.pdf
├── manuscript.md
├── node_modules
├── package-lock.json
├── package.json
└── vivliostyle.config.js

1777 directories, 12494 files
$ head -n 12 .vivliostyle/manuscript.html        
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>example-correct</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link
      rel="stylesheet"
      type="text/css"
      href="themes/node_modules/css/style.css"
    />
  </head>
```

## 例1：ファイルごとにテーマを切り替えたい

`entry`に、Markdownファイルと適用したいテーマの組み合わせを指定できます。

```vivliostyle.config.js
// @ts-check
/** @type {import('@vivliostyle/cli').VivliostyleConfigSchema} */
const vivliostyleConfig = {
  title: "example-correct",
  author: "u1f992",
  language: "ja",
  entry: [
    { path: "chapter1.md", theme: "./css/chapter1" },
    { path: "chapter2.md", theme: "./css/chapter2" },
  ],
};

module.exports = vivliostyleConfig;
```

## 例2：chapter1とchapter2のテーマで、共通部をくくりたい

テーマのpackage.jsonに`"dependencies"`を追加するのが[Vivliostyle Themesにも採用されている方法](https://github.com/vivliostyle/themes/blob/8654d9819d3a1cb8bbfddb3ec45489bf9a921500/packages/%40vivliostyle/theme-techbook/package.json#L12-L14)ですが、執筆時点で最新のv9.1.1では、ローカルのテーマがさらにローカルのパスで依存関係を指定すると正しく解決されません。[参考](https://github.com/vivliostyle/vivliostyle-cli/issues/598)

```
$ tree css
css
├── base
│   ├── package.json
│   └── style.css
├── chapter1
│   ├── package.json
│   └── style.css
└── chapter2
    ├── package.json
    └── style.css

4 directories, 6 files
```

```css/base/package.json
{
    "name": "base",
    "main": "style.css"
}
```

```css/base/style.css
@page {
    size: A5;
}

/* ... */
```

```css/chapter1/package.json
{
    "name": "chapter1",
    "main": "style.css",
    "dependencies": {
        "base": "../base"
    }
}
```

```css/chapter1/style.css
@import url("../base/style.css");
/* ... */
```

```css/chapter2/package.json
{
    "name": "chapter2",
    "main": "style.css",
    "dependencies": {
        "base": "../base"
    }
}
```

```css/chapter2/style.css
@import url("../base/style.css");
/* ... */
```

この例は、現状次のように失敗します。`node_modules`以下に`base`のリンクが作成されず、CSSもリンク切れになります。

```
$ npx vivliostyle build
INFO Start building
INFO Launching PDF build environment
INFO Building pages
ERROR 404 http://localhost:13000/vivliostyle/themes/node_modules/base/style.css
INFO Building PDF
INFO Processing PDF
SUCCESS Finished building u1f992.pdf
📗 Built successfully!

$ tree .vivliostyle/
.vivliostyle/
├── chapter.html
├── publication.json
└── themes
    ├── node_modules
    │   ├── chapter1 -> ../../../css/chapter1
    │   └── chapter2 -> ../../../css/chapter2
    ├── package-lock.json
    └── package.json
```

暫定の解決方法は2つあります。base.cssを`chapter1`と`chapter2`の両方に追加するか、baseテーマを文書全体の`theme`に指定することで`workspaceDir`に取り込まれるようにします。