@napi-rs/canvas v1.0.12 で実質的な破壊的変更が入った話
@napi-rs/canvas を 1.0.11 から 1.0.12 以降に上げると、ctx.font = '48px M PLUS Rounded 1c' のように引用符なしで書いたフォント指定が、まるごと無視されるようになりました。日本語は 10px の豆腐で描かれ、例外も警告も出ません。フォント名を "M PLUS Rounded 1c" のように引用符で囲めば直ります。
この記事は、気づいたきっかけから原因の特定、対策までをまとめたものです。
きっかけ
makeitaquote は、メッセージから Make it a Quote 風の引用画像を作る自作の npm ライブラリです。描画には @napi-rs/canvas を使っています。これを使う Discord bot を v8 から v12 に上げたので、試しに1枚描かせたところ、出てきたのがこれです。
日本語がすべて豆腐になり、文字もやたら小さくなっています。英数字と絵文字は出ているので、描画そのものは動いているようです。コードはフォントまわりを何も触っていなかったので、普通にビビりました。
x.com
起きたこと
条件をそろえて調べると、次のことがわかりました。
同じ makeitaquote 12.1.3 でも、@napi-rs/canvas が 1.0.8 なら正しく描け、1.0.13 なら豆腐になる
豆腐の文字は 10px 程度まで小さくなる
例外も警告も出ず、toBuffer() は普通に画像を返す
makeitaquote の package.json は "@napi-rs/canvas": "^1.0.8" と書いていました。リポジトリのロックファイルは 1.0.8 で固定されていたので、CI はずっと通っていました。一方で、利用者が npm install makeitaquote をすると、範囲内の最新である 1.0.13 が入ります。つまり、新しく入れた人だけが壊れる状態でした。私の bot は依存を入れ直したタイミングで踏みました。
対象バージョン
バージョン
公開日
引用符なしの M PLUS Rounded 1c
1.0.11
2026-10-10
描ける
1.0.12
2026-10-20
無視される
1.0.13
2026-10-21
無視される
1.0.8 から 1.0.13 までを1つずつ入れ替えて確かめ、1.0.12 から変わったことを特定しました。1.0.13 での変更は再評価まわりの修正1件だけで、この件とは関係ありません。Windows 11 と WSL の Ubuntu 24.04 (どちらも Node.js 24) で同じ結果になりました。
最小の再現コード
ここからは makeitaquote を使わず、@napi-rs/canvas だけで再現します。フォントは Google Fonts の GitHub リポジトリから取ってきたファイルを、別名を付けて登録しています。
repro.mjs
import { writeFileSync } from "node:fs";
import { createCanvas, GlobalFonts } from "@napi-rs/canvas";
GlobalFonts.registerFromPath("./fonts/MPLUSRounded1c-Regular.ttf", "M PLUS Rounded 1c");
GlobalFonts.registerFromPath("./fonts/NotoSansJP.ttf", "Noto Sans JP");
const fonts = [
"48px M PLUS Rounded 1c",
'48px "M PLUS Rounded 1c"',
"48px Noto Sans JP",
"bold 48px Noto Sans JP",
];
const canvas = createCanvas(900, 500);
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#111";
for (const [i, font] of fonts.entries()) {
// 無視された指定を見分けやすいよう、毎回既定値に戻してから代入する
ctx.font = "10px sans-serif";
ctx.font = font;
console.log(${font} -> ${ctx.font});
ctx.fillText("日本語のテスト Quote 123", 20, 100 + i * 110);
}
writeFileSync("out.png", canvas.toBuffer("image/png"));
ctx.font を代入したあとに読み返した値は、次のようになりました。
1.0.11
48px M PLUS Rounded 1c -> 48px M PLUS Rounded 1c
48px "M PLUS Rounded 1c" -> 48px "M PLUS Rounded 1c"
48px Noto Sans JP -> 48px Noto Sans JP
bold 48px Noto Sans JP -> bold 48px Noto Sans JP
1.0.12 / 1.0.13
48px M PLUS Rounded 1c -> 10px sans-serif
48px "M PLUS Rounded 1c" -> 48px "M PLUS Rounded 1c"
48px Noto Sans JP -> 48px "Noto Sans JP"
bold 48px Noto Sans JP -> bold 48px "Noto Sans JP"
1.0.12 以降は、1行目の代入だけが受け付けられず、直前の 10px sans-serif のまま残っています。描画結果は次のとおりです(画像のラベルは見やすさのために別のフォントで描いています)。
豆腐になった1行目は、10px sans-serif で日本語を描こうとした結果です。この環境の sans-serif に日本語の字形がないので豆腐になります。どのフォントが sans-serif に割り当たるかは環境しだいなので、豆腐にならず、小さい日本語で出る環境もあるはずです。
原因: ctx.font の解析の変更
公式情報
リリースノートを見ると、1.0.12 にはこの変更が1行だけ載っています。
fix(canvas): parse fonts, lay out and measure text like Blink
詳しいことは、該当する PR #1381 の本文に書かれていました。ctx.font の解析、フォントの選択、テキストのレイアウトと measureText を、Chrome 155 の Blink と同じ動きにするという PR です。挙動の変化として、CSS として正しくないフォント指定('12px' や '12 serif' など)は無視される、と明記されています。同じ箇所には "none of them is a breaking change" (どれも破壊的変更ではない) とも書かれています。
つまり、この挙動は意図された変更です。
M PLUS Rounded 1c が不正な値になる理由
CSS では、引用符なしのフォントファミリー名は、CSS の識別子をスペースで区切って並べたものとして読まれます。識別子は数字から始められません。M PLUS Rounded 1c の 1c は数字から始まるので識別子にならず、フォント指定全体が不正になります。不正な値を ctx.font に代入しても無視され、直前の値が残ります。何も設定していなければ、残るのは既定値の 10px sans-serif です。
ブラウザでも同じかを、手元の Chromium 156 (Brave) で確かめました。
const ctx = document.createElement("canvas").getContext("2d");
ctx.font = "48px M PLUS Rounded 1c";
ctx.font; // "10px sans-serif"
ctx.font = '48px "M PLUS Rounded 1c"';
ctx.font; // '48px "M PLUS Rounded 1c"'
@napi-rs/canvas 1.0.12 以降とまったく同じ結果です。ブラウザの canvas では最初からこの動きで、1.0.11 までの @napi-rs/canvas が寛容すぎた、というのが実態です。
引っかかるフォント名、引っかからないフォント名
よく使われそうなフォント名を、1.0.13 で引用符なしのまま試しました。
フォント名
引用符なしで
M PLUS Rounded 1c
無視される
M PLUS 1p
無視される
M PLUS 1 Code
無視される
07やさしさゴシック
無視される
851チカラヅヨク
無視される
Font Awesome 6 Free
無視される
Noto Sans JP
通る
BIZ UDPGothic
通る
Zen Maru Gothic
通る
Mochiy Pop P One
通る
DotGothic16
通る
游ゴシック
通る
ヒラギノ角ゴ ProN W3
通る
無視されたのは、どれも数字から始まる単語を含む名前です。DotGothic16 のように途中に数字があるのは問題ありません。日本語の名前も識別子として認められるので通ります。ほかに ' などの記号を含む名前(O'Brien Sans など)も無視されました。
日本語フォントでは M PLUS ファミリーのように数字を含む名前がわりとあるので、日本語を描く人ほど踏みやすい変更だと思います。
!
registerFromPath() に別名を渡さないと、フォントファイルの中に書かれたファミリー名で登録されます。Google Fonts の GitHub リポジトリにある MPLUSRounded1c-Regular.ttf の場合、その名前は Rounded Mplus 1c でした。これも数字から始まる 1c を含むので、引用符なしで指定すると同じように無視されます。何という名前で登録されたかは GlobalFonts.families で確認できます。
もう1つの変化: 可変フォントの太さ
再現画像の3行目と4行目を見比べると、もう1つ違いがあります。
Noto Sans JP の可変フォント(NotoSansJP[wght].ttf)は、1.0.11 では普通の太さが細く描かれ、bold を付けても普通の太さ程度にしかなりませんでした。1.0.12 以降は、普通の太さも bold も指定どおりの太さで描かれます。PR #1381 には、可変フォントの wght 軸に font-weight を効かせる対応(#1165)も入っています。
正しくなる方向の変化ですが、上げると見た目は変わります。どのくらい変わるのか、makeitaquote の見本画像 142 枚を 1.0.8 と 1.0.13 で描き比べました。47 枚は完全に一致し、残りもピクセルの差は 0.1% から 1.85% でした。改行の位置や文字の大きさは変わらず、差のほとんどは字形の位置が1ピクセル未満ずれたことによるものです。はっきり変わったのは、可変フォントの Noto Sans JP、Noto Sans SC、Noto Serif JP を使った画像だけでした。
対策
フォント名を引用符で囲む
いちばん確実なのは、ctx.font に渡すフォント名をすべて引用符で囲むことです。sans-serif などの総称ファミリー名は、囲むと同じ名前の実在しないフォント扱いになるので、囲まずに残します。
私は、フォント名を並べた文字列を受け取って1つずつ囲む関数を makeitaquote に入れました。
fontFamily.ts
const GENERIC_FAMILIES = new Set([
"serif",
"sans-serif",
"monospace",
"cursive",
"fantasy",
"system-ui",
"emoji",
"math",
"fangsong",
"ui-serif",
"ui-sans-serif",
"ui-monospace",
"ui-rounded",
]);
/**
- Quotes every family in a comma-separated list for ctx.font, leaving generic keywords and already-quoted names alone.
- @example
- cssFontFamily("M PLUS Rounded 1c, Noto Sans JP, sans-serif")
- // => '"M PLUS Rounded 1c", "Noto Sans JP", sans-serif'
*/
export function cssFontFamily(families: string): string {
return (families.match(/"[^"]"|'[^']'|[^,]+/g) ?? [])
.map((family) => family.trim())
.filter((family) => family !== "")
.map((family) =>
/^["']/.test(family) || GENERIC_FAMILIES.has(family.toLowerCase())
? family
: "${family.replace(/["\\]/g, "\\$&")}",
)
.join(", ");
}
ctx.font = bold 48px ${cssFontFamily("M PLUS Rounded 1c, sans-serif")};
引用符で囲んだ書き方は 1.0.11 以前でもそのまま通るので、バージョンを問わず使えます。
代入した値を読み返して確かめる
不正な値は黙って無視されるので、テストでは代入後の ctx.font を読み返すと気づけます。同じ趣旨のテストを、本物の canvas を使う形で makeitaquote にも足しました。
fontFamily.test.ts
import { createCanvas } from "@napi-rs/canvas";
import { expect, it } from "vitest";
import { cssFontFamily } from "./fontFamily";
// 不正な ctx.font は黙って無視されるので、代入後の値を読み返して確かめる
it.each(["M PLUS Rounded 1c, Noto Sans JP, sans-serif", "M PLUS 1p", "O'Brien Sans"])(
"ctx.font accepts %s once quoted",
(families) => {
const ctx = createCanvas(1, 1).getContext("2d");
ctx.font = "7px serif";
ctx.font = bold 32px ${cssFontFamily(families)};
expect(ctx.font).not.toBe("7px serif");
},
);
引用符で囲む処理を外すと、1.0.13 では3件とも落ちます。今後また解析が変わったときにも、このテストで気づけるはずです。
すぐ直せないならバージョンを固定する
コードをすぐ直せない場合は、ひとまず 1.0.12 より前に固定すれば元の動きに戻ります。
package.json
{
"dependencies": {
"@napi-rs/canvas": ">=1.0.8 <1.0.12"
}
}
ライブラリとして公開しているなら、あわせてロックファイルなしの環境でもテストしておくと安心です。今回の makeitaquote のように、ロックファイルで固定した CI では利用者側の破損に気づけません。
makeitaquote は 12.1.4 でフォント名の引用符付けとこの固定を入れ、可変フォントの見た目を確かめてから、12.1.5 で ^1.0.13 に移りました。
破壊的変更と呼びたい理由
公式の立場を整理すると、PR では破壊的変更ではないと明記されていて、実際に Chrome と同じ、仕様どおりの動きになっただけです。引用符なしで M PLUS Rounded 1c と書いていた側が、仕様から外れていました。つまり半分は私が悪いです。
それでも私は、日本語を描く人にとっては実質的な破壊的変更だったと考えています。理由は3つです。
日本語フォントでよく使われる M PLUS ファミリーなどが、パッチバージョンの更新で突然描けなくなった
例外も警告も出ないので、画像を目で見るまで気づけない
リリースノートの1行からは、ctx.font の扱いが変わることまで読み取れない
ブラウザ互換に寄せる方向そのものには賛成です。ただ、この規模の変更なら、せめて破壊的変更として告知してほしかった、というのが正直な感想です。
まとめ
@napi-rs/canvas 1.0.12 以降、CSS として正しくない ctx.font の値は無視され、既定の 10px sans-serif のまま描かれる
数字から始まる単語を含むフォント名(M PLUS Rounded 1c、M PLUS 1p など)は、引用符なしだと正しくない値になる
フォント名を " で囲めばどのバージョンでも動く。代入後に ctx.font を読み返すテストを入れておくと、黙って無視される指定に気づける
同じ 1.0.12 から可変フォントの太さが効くようになったので、Noto Sans JP などは見た目が変わる
日本語フォントで canvas に文字を描いているなら、一度 ctx.font を読み返してみてください。
Mentions
@napi-rs/canvas v1.0.12 で実質的な破壊的変更が入った話 @napi-rs/canvas を 1.0.11 から 1.0.12 以降に上げると、ctx.font = '48px M PLUS Rounded 1c' のように引用符なしで書いたフォント指定が、まるごと無視されるようになりました。日本語は 10px の豆腐で描かれ、例外も警告も出ません。フォント名を "M PLUS Rounded 1c" のように引用符で囲めば直ります。 この記事は、気づいたきっかけから原因の特定、対策までをまとめたものです。 きっかけ makeitaquote は、メッセージから Make it a Quote 風の引用画像を作る自作の npm ライブラリです。描画には @napi-rs/canvas を使っています。これを使う Discord bot を v8 から v12 に上げたので、試しに1枚描かせたところ、出てきたのがこれです。 日本語がすべて豆腐になり、文字もやたら小さくなっています。英数字と絵文字は出ているので、描画そのものは動いているようです。コードはフォントまわりを何も触っていなかったので、普通にビビりました。 x.com 起きたこと 条件をそろえて調べると、次のことがわかりました。 同じ makeitaquote 12.1.3 でも、@napi-rs/canvas が 1.0.8 なら正しく描け、1.0.13 なら豆腐になる 豆腐の文字は 10px 程度まで小さくなる 例外も警告も出ず、toBuffer() は普通に画像を返す makeitaquote の package.json は "@napi-rs/canvas": "^1.0.8" と書いていました。リポジトリのロックファイルは 1.0.8 で固定されていたので、CI はずっと通っていました。一方で、利用者が npm install makeitaquote をすると、範囲内の最新である 1.0.13 が入ります。つまり、新しく入れた人だけが壊れる状態でした。私の bot は依存を入れ直したタイミングで踏みました。 対象バージョン バージョン 公開日 引用符なしの M PLUS Rounded 1c 1.0.11 2026-10-10 描ける 1.0.12 2026-10-20 無視される 1.0.13 2026-10-21 無視される 1.0.8 から 1.0.13 までを1つずつ入れ替えて確かめ、1.0.12 から変わったことを特定しました。1.0.13 での変更は再評価まわりの修正1件だけで、この件とは関係ありません。Windows 11 と WSL の Ubuntu 24.04 (どちらも Node.js 24) で同じ結果になりました。 最小の再現コード ここからは makeitaquote を使わず、@napi-rs/canvas だけで再現します。フォントは Google Fonts の GitHub リポジトリから取ってきたファイルを、別名を付けて登録しています。 repro.mjs import { writeFileSync } from "node:fs"; import { createCanvas, GlobalFonts } from "@napi-rs/canvas"; GlobalFonts.registerFromPath("./fonts/MPLUSRounded1c-Regular.ttf", "M PLUS Rounded 1c"); GlobalFonts.registerFromPath("./fonts/NotoSansJP.ttf", "Noto Sans JP"); const fonts = [ "48px M PLUS Rounded 1c", '48px "M PLUS Rounded 1c"', "48px Noto Sans JP", "bold 48px Noto Sans JP", ]; const canvas = createCanvas(900, 500); const ctx = canvas.getContext("2d"); ctx.fillStyle = "#fff"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "#111"; for (const [i, font] of fonts.entries()) { // 無視された指定を見分けやすいよう、毎回既定値に戻してから代入する ctx.font = "10px sans-serif"; ctx.font = font; console.log(${font} -> ${ctx.font}); ctx.fillText("日本語のテスト Quote 123", 20, 100 + i * 110); } writeFileSync("out.png", canvas.toBuffer("image/png")); ctx.font を代入したあとに読み返した値は、次のようになりました。 1.0.11 48px M PLUS Rounded 1c -> 48px M PLUS Rounded 1c 48px "M PLUS Rounded 1c" -> 48px "M PLUS Rounded 1c" 48px Noto Sans JP -> 48px Noto Sans JP bold 48px Noto Sans JP -> bold 48px Noto Sans JP 1.0.12 / 1.0.13 48px M PLUS Rounded 1c -> 10px sans-serif 48px "M PLUS Rounded 1c" -> 48px "M PLUS Rounded 1c" 48px Noto Sans JP -> 48px "Noto Sans JP" bold 48px Noto Sans JP -> bold 48px "Noto Sans JP" 1.0.12 以降は、1行目の代入だけが受け付けられず、直前の 10px sans-serif のまま残っています。描画結果は次のとおりです(画像のラベルは見やすさのために別のフォントで描いています)。 豆腐になった1行目は、10px sans-serif で日本語を描こうとした結果です。この環境の sans-serif に日本語の字形がないので豆腐になります。どのフォントが sans-serif に割り当たるかは環境しだいなので、豆腐にならず、小さい日本語で出る環境もあるはずです。 原因: ctx.font の解析の変更 公式情報 リリースノートを見ると、1.0.12 にはこの変更が1行だけ載っています。 fix(canvas): parse fonts, lay out and measure text like Blink 詳しいことは、該当する PR #1381 の本文に書かれていました。ctx.font の解析、フォントの選択、テキストのレイアウトと measureText を、Chrome 155 の Blink と同じ動きにするという PR です。挙動の変化として、CSS として正しくないフォント指定('12px' や '12 serif' など)は無視される、と明記されています。同じ箇所には "none of them is a breaking change" (どれも破壊的変更ではない) とも書かれています。 つまり、この挙動は意図された変更です。 M PLUS Rounded 1c が不正な値になる理由 CSS では、引用符なしのフォントファミリー名は、CSS の識別子をスペースで区切って並べたものとして読まれます。識別子は数字から始められません。M PLUS Rounded 1c の 1c は数字から始まるので識別子にならず、フォント指定全体が不正になります。不正な値を ctx.font に代入しても無視され、直前の値が残ります。何も設定していなければ、残るのは既定値の 10px sans-serif です。 ブラウザでも同じかを、手元の Chromium 156 (Brave) で確かめました。 const ctx = document.createElement("canvas").getContext("2d"); ctx.font = "48px M PLUS Rounded 1c"; ctx.font; // "10px sans-serif" ctx.font = '48px "M PLUS Rounded 1c"'; ctx.font; // '48px "M PLUS Rounded 1c"' @napi-rs/canvas 1.0.12 以降とまったく同じ結果です。ブラウザの canvas では最初からこの動きで、1.0.11 までの @napi-rs/canvas が寛容すぎた、というのが実態です。 引っかかるフォント名、引っかからないフォント名 よく使われそうなフォント名を、1.0.13 で引用符なしのまま試しました。 フォント名 引用符なしで M PLUS Rounded 1c 無視される M PLUS 1p 無視される M PLUS 1 Code 無視される 07やさしさゴシック 無視される 851チカラヅヨク 無視される Font Awesome 6 Free 無視される Noto Sans JP 通る BIZ UDPGothic 通る Zen Maru Gothic 通る Mochiy Pop P One 通る DotGothic16 通る 游ゴシック 通る ヒラギノ角ゴ ProN W3 通る 無視されたのは、どれも数字から始まる単語を含む名前です。DotGothic16 のように途中に数字があるのは問題ありません。日本語の名前も識別子として認められるので通ります。ほかに ' などの記号を含む名前(O'Brien Sans など)も無視されました。 日本語フォントでは M PLUS ファミリーのように数字を含む名前がわりとあるので、日本語を描く人ほど踏みやすい変更だと思います。 ! registerFromPath() に別名を渡さないと、フォントファイルの中に書かれたファミリー名で登録されます。Google Fonts の GitHub リポジトリにある MPLUSRounded1c-Regular.ttf の場合、その名前は Rounded Mplus 1c でした。これも数字から始まる 1c を含むので、引用符なしで指定すると同じように無視されます。何という名前で登録されたかは GlobalFonts.families で確認できます。 もう1つの変化: 可変フォントの太さ 再現画像の3行目と4行目を見比べると、もう1つ違いがあります。 Noto Sans JP の可変フォント(NotoSansJP[wght].ttf)は、1.0.11 では普通の太さが細く描かれ、bold を付けても普通の太さ程度にしかなりませんでした。1.0.12 以降は、普通の太さも bold も指定どおりの太さで描かれます。PR #1381 には、可変フォントの wght 軸に font-weight を効かせる対応(#1165)も入っています。 正しくなる方向の変化ですが、上げると見た目は変わります。どのくらい変わるのか、makeitaquote の見本画像 142 枚を 1.0.8 と 1.0.13 で描き比べました。47 枚は完全に一致し、残りもピクセルの差は 0.1% から 1.85% でした。改行の位置や文字の大きさは変わらず、差のほとんどは字形の位置が1ピクセル未満ずれたことによるものです。はっきり変わったのは、可変フォントの Noto Sans JP、Noto Sans SC、Noto Serif JP を使った画像だけでした。 対策 フォント名を引用符で囲む いちばん確実なのは、ctx.font に渡すフォント名をすべて引用符で囲むことです。sans-serif などの総称ファミリー名は、囲むと同じ名前の実在しないフォント扱いになるので、囲まずに残します。 私は、フォント名を並べた文字列を受け取って1つずつ囲む関数を makeitaquote に入れました。 fontFamily.ts const GENERIC_FAMILIES = new Set([ "serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui", "emoji", "math", "fangsong", "ui-serif", "ui-sans-serif", "ui-monospace", "ui-rounded", ]); /** - Quotes every family in a comma-separated list for ctx.font, leaving generic keywords and already-quoted names alone. - @example - cssFontFamily("M PLUS Rounded 1c, Noto Sans JP, sans-serif") - // => '"M PLUS Rounded 1c", "Noto Sans JP", sans-serif' */ export function cssFontFamily(families: string): string { return (families.match(/"[^"]"|'[^']'|[^,]+/g) ?? []) .map((family) => family.trim()) .filter((family) => family !== "") .map((family) => /^["']/.test(family) || GENERIC_FAMILIES.has(family.toLowerCase()) ? family : "${family.replace(/["\\]/g, "\\$&")}", ) .join(", "); } ctx.font = bold 48px ${cssFontFamily("M PLUS Rounded 1c, sans-serif")}; 引用符で囲んだ書き方は 1.0.11 以前でもそのまま通るので、バージョンを問わず使えます。 代入した値を読み返して確かめる 不正な値は黙って無視されるので、テストでは代入後の ctx.font を読み返すと気づけます。同じ趣旨のテストを、本物の canvas を使う形で makeitaquote にも足しました。 fontFamily.test.ts import { createCanvas } from "@napi-rs/canvas"; import { expect, it } from "vitest"; import { cssFontFamily } from "./fontFamily"; // 不正な ctx.font は黙って無視されるので、代入後の値を読み返して確かめる it.each(["M PLUS Rounded 1c, Noto Sans JP, sans-serif", "M PLUS 1p", "O'Brien Sans"])( "ctx.font accepts %s once quoted", (families) => { const ctx = createCanvas(1, 1).getContext("2d"); ctx.font = "7px serif"; ctx.font = bold 32px ${cssFontFamily(families)}; expect(ctx.font).not.toBe("7px serif"); }, ); 引用符で囲む処理を外すと、1.0.13 では3件とも落ちます。今後また解析が変わったときにも、このテストで気づけるはずです。 すぐ直せないならバージョンを固定する コードをすぐ直せない場合は、ひとまず 1.0.12 より前に固定すれば元の動きに戻ります。 package.json { "dependencies": { "@napi-rs/canvas": ">=1.0.8 <1.0.12" } } ライブラリとして公開しているなら、あわせてロックファイルなしの環境でもテストしておくと安心です。今回の makeitaquote のように、ロックファイルで固定した CI では利用者側の破損に気づけません。 makeitaquote は 12.1.4 でフォント名の引用符付けとこの固定を入れ、可変フォントの見た目を確かめてから、12.1.5 で ^1.0.13 に移りました。 破壊的変更と呼びたい理由 公式の立場を整理すると、PR では破壊的変更ではないと明記されていて、実際に Chrome と同じ、仕様どおりの動きになっただけです。引用符なしで M PLUS Rounded 1c と書いていた側が、仕様から外れていました。つまり半分は私が悪いです。 それでも私は、日本語を描く人にとっては実質的な破壊的変更だったと考えています。理由は3つです。 日本語フォントでよく使われる M PLUS ファミリーなどが、パッチバージョンの更新で突然描けなくなった 例外も警告も出ないので、画像を目で見るまで気づけない リリースノートの1行からは、ctx.font の扱いが変わることまで読み取れない ブラウザ互換に寄せる方向そのものには賛成です。ただ、この規模の変更なら、せめて破壊的変更として告知してほしかった、というのが正直な感想です。 まとめ @napi-rs/canvas 1.0.12 以降、CSS として正しくない ctx.font の値は無視され、既定の 10px sans-serif のまま描かれる 数字から始まる単語を含むフォント名(M PLUS Rounded 1c、M PLUS 1p など)は、引用符なしだと正しくない値になる フォント名を " で囲めばどのバージョンでも動く。代入後に ctx.font を読み返すテストを入れておくと、黙って無視される指定に気づける 同じ 1.0.12 から可変フォントの太さが効くようになったので、Noto Sans JP などは見た目が変わる 日本語フォントで canvas に文字を描いているなら、一度 ctx.font を読み返してみてください。