Skip to content

fix(editor): vite-plugin-top-level-await を外して本番ビルドのエディタ起動を直す - #256

Merged
shinyaoguri merged 3 commits into
mainfrom
fix/editor-drop-tla-plugin
Aug 5, 2026
Merged

fix(editor): vite-plugin-top-level-await を外して本番ビルドのエディタ起動を直す#256
shinyaoguri merged 3 commits into
mainfrom
fix/editor-drop-tla-plugin

Conversation

@shinyaoguri

Copy link
Copy Markdown
Owner

目的

本番ビルド (Cloudflare Pages / vite preview) で エディタが一切起動しない 不具合を直す。

TypeError: Class extends value undefined is not a constructor or null  @ toggleHighContrast-*.js
TypeError: Le is not a function                                        @ editor.api-*.js

Fixes #255

原因

犯人は vite-plugin-top-level-await、引き金は monaco 0.56.0 (#206) でのチャンク構成変更。

このプラグインはチャンクの全エクスポートを先頭の let へ巻き上げ、本体を async IIFE で包む。この変換で export function代入文に降格 するため、関数宣言の巻き上げに正当に依存している monaco が壊れる:

// vs/platform/instantiation/common/instantiation.js
const IInstantiationService = createDecorator('instantiationService');  // 16 行目 — 先に実行される
function createDecorator(serviceId) { ... }                            // 29 行目 — 後で代入される

throw は async IIFE の中で起きるので __tla が reject するだけでモジュール評価は「成功」扱いになり、以降のエクスポート代入が全て飛ぶ。その結果 Action2 などが undefined のまま class extends に渡り、二次症状の Class extends value undefined に至る。

monaco 0.55.1 では monaco が単一チャンクに収まりラップ対象になっていなかったため顕在化していなかった。

変更点

1. 修正本体 (377bd40)

  • packages/editor/vite.config.ts から topLevelAwait() を削除 (理由をコメントで残す)。build.target: 'esnext' + worker.format: 'es' でネイティブ TLA が使えるため、そもそも不要な変換だった
  • packages/editor/package.json から vite-plugin-top-level-await と、そのプラグインが require するためだけに置いていた rollup を削除 (Vite 8 は rolldown ベース)
  • package-lock.json 更新 (削除のみ・730 行減、バージョン bump は無し)

2. 再発防止 (ebe3f09)

#255 は「dev サーバでは console エラー 0 件、本番ビルドだけ壊れる」という形だったため、既存 E2E が全て vite dev サーバ向けで原理的に検出できず、CI が緑のまま main に載った。この穴を塞ぐ。

  • packages/e2e/tests/production-build.spec.ts — dist に対するスモーク 1 本。オラクルは「pageerror / console.error が 0 件」「Monaco が実際にマウントされる」「打鍵がエディタに入る」の 3 点のみ
  • packages/e2e/playwright.build.config.ts — editor を vite buildvite preview (:4173) で配信し、この spec だけを走らせる専用 config。reuseExistingServer: false で「いま build した dist」を保証する
  • packages/e2e/playwright.config.tstestIgnore でこの spec を除外 (既存の dev 向けシナリオの挙動は不変)
  • .github/workflows/deploy.yml — 既存の e2e job の最終ステップとして実行。この job は既に deploy-preview / deploy-staging / deploy-production の必須ゲート (needs: [test, check, e2e]) なので、本番ビルドの起動が deploy 条件になる。job を分けなかったのは npm ci + playwright install の重複を避けるため

3. docs (e6b1431)

editor/CLAUDE.md の「rollup を明示せよ」という旧・罠を「再導入しない (#255)」に差し替え。e2e/CLAUDE.md / README.md に 2 config の役割分担を反映。

確認方法

壊して赤を見た

topLevelAwait() を一時的に戻してビルドすると、スモークが 失敗し、収集した pageerror がそのまま #255 の症状を再現 した:

Error: Monaco がマウントされなかった (#255 と同じ症状)。
pageerror:
Class extends value undefined is not a constructor or null
Le is not a function

外すと 1 passed。オラクルが機能していることを確認済み。

実ブラウザでの実測 (vite preview = dist 配信)

ネイティブ TLA へ切り替わる影響を受けるため、E2E に spec が無い実行系を手で確認した。

項目 結果
console エラー 0 件 (警告のみ: monaco の aria-hidden 通知、@wasmer/sdk の wasm-bindgen deprecation)
Monaco マウント OK
C 実行 (@wasmer/sdk) TLA_SMOKE_C_OK 42 を出力
Python 実行 (Pyodide) TLA_SMOKE_PY_OK 45 / Python exited with code 0

コマンド

コマンド 結果
npm run lint green (exit 0)
npm run typecheck green
npm run test:run --workspaces --if-present green (728 tests)
npm run build green (rollup 無しで通る。生成物に __tla が 1 箇所も出ない)
npm run test -w @typedcode/e2e green (17 passed)
npm run test:build -w @typedcode/e2e green (1 passed)

⚠️ リリース時の注意: content hash が変わらない

修正前後で editor.api の中身が違う (2,686.57 kB → 2,682.40 kB) のに、出力ファイル名のハッシュが両方 editor.api-BYMKrlhl.js で同一 だった。content hash が TLA プラグインの renderChunk の内容で計算されていたため。

デプロイしてもファイル名が変わらず CDN / ブラウザキャッシュから壊れた旧版が配られる恐れ がある。本番リリース時は Cloudflare 側のキャッシュパージを前提にすること。

🤖 Generated with Claude Code

shinyaoguri and others added 3 commits August 6, 2026 00:33
本番ビルド (Cloudflare Pages / vite preview) でエディタが一切起動せず、
console に以下が出ていた (#255):

    TypeError: Class extends value undefined is not a constructor or null
    TypeError: Le is not a function   (= createDecorator is not a function)

原因は `vite-plugin-top-level-await`。このプラグインはチャンクの全エクスポートを
先頭の `let` へ巻き上げ、本体を async IIFE で包む。この変換で `export function` が
代入文に降格するため、**関数宣言の巻き上げに正当に依存している monaco** が壊れる:

    // vs/platform/instantiation/common/instantiation.js
    const IInstantiationService = createDecorator('instantiationService');  // 先に実行
    function createDecorator(serviceId) { ... }                            // 後に代入

throw は async IIFE の中で起きるので `__tla` が reject するだけでモジュール評価は
「成功」扱いになり、以降のエクスポート代入が全て飛ぶ。その結果 `Action2` 等が
undefined のまま参照され `Class extends value undefined` に至る。

monaco 0.55.1 では monaco が単一チャンクに収まりラップ対象になっていなかったため
顕在化せず、0.56.0 (#206) でチャンク構成が変わって踏んだ。

`build.target: 'esnext'` + `worker.format: 'es'` を既に指定しており、ネイティブ TLA が
Chrome 89+ / Safari 15+ / Firefox 89+ で使える。このプラグインは「TLA 非対応ブラウザ
向けの降格」が本来の役目で、現在の target では不要な変換をかけているだけなので削除する。

あわせて、このプラグインが rollup を `require` するためだけに置いていた
devDependencies の `rollup` も削除した (Vite 8 は rolldown ベースで本来不要)。

確認方法:
- `npm run build` green (rollup 無しで通る)。生成物に `__tla` が 1 箇所も出ない
- `vite preview` を実ブラウザで開いて console エラー 0 件・Monaco マウント済み
- Python (Pyodide) と C (@wasmer/sdk) を実行して出力が返ることを実測
  (ネイティブ TLA へ切り替わる影響を受けるため)

Refs #255

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#255 は「dev サーバでは console エラー 0 件、本番ビルドだけエディタが起動しない」
という壊れ方をした。既存の E2E は全て vite dev サーバに対して走るため、チャンク
分割やバンドル変換に起因する退行を**原理的に検出できず**、CI が緑のまま main に
載ってしまった。この穴を塞ぐ。

変更点:
- `tests/production-build.spec.ts` … dist に対するスモーク 1 本。オラクルは
  「pageerror / console.error が 0 件」「Monaco が実際にマウントされる」
  「打鍵がエディタに入る」の 3 点のみ。重い round-trip は dev 側の spec が担う
- `playwright.build.config.ts` … editor を `vite build` してから `vite preview`
  (:4173) で dist を配信し、上の spec だけを走らせる専用 config。検証対象が
  「いま build した dist」であることを保証するため `reuseExistingServer: false`。
  workers も起動する (無いと fetch 失敗が console エラーとして混ざり、本題の
  バンドル起因エラーと区別がつかなくなるため)
- `playwright.config.ts` … `testIgnore` でこの spec を除外。dev サーバに対して
  走らせても目的の退行を検出できないため。既存シナリオの挙動は変えていない
- `package.json` … `test:build` スクリプト
- `deploy.yml` … 既存の `e2e` job の最後のステップとして実行する。この job は
  既に `deploy-preview` / `deploy-staging` / `deploy-production` の必須ゲート
  (`needs: [test, check, e2e]`) なので、これで本番ビルドの起動が deploy 条件になる。
  job を分けなかったのは npm ci + playwright install の重複を避けるため

確認方法 (「壊して赤を見る」):
- `topLevelAwait()` を一時的に戻してビルド → このスモークが失敗し、収集した
  pageerror がそのまま #255 の症状を再現する:
    Class extends value undefined is not a constructor or null
    Le is not a function
- 外すと 1 passed。dev 向け `npm run test -w @typedcode/e2e` は緑のまま

Refs #255

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- editor/CLAUDE.md … 「rollup を devDependencies に明示せよ」という旧・罠の記述を
  「`vite-plugin-top-level-await` を再導入しない (#255)」に差し替える。壊れ方
  (monaco の関数巻き上げ破壊 → 全エクスポート代入が飛ぶ) と、dev では再現せず
  本番ビルドだけが壊れること、番人が production-build.spec.ts であることを明記
- editor/README.md … 依存表から `vite-plugin-top-level-await` を削除
- e2e/CLAUDE.md … 2 つの config の役割分担を構成図と実行コマンドに反映。罠に
  「dev サーバ向けの spec は本番ビルドの退行を検出できない」を追加し、この経路の
  spec を増やすときは dev 限定 API (`__tcTestInsertBlock`) に依存しないよう注意書き
- e2e/README.md … `npm run test:build` の説明と、なぜ別 config が要るのかの節を追加

Refs #255

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation pkg:editor packages/editor pkg:repo リポジトリ横断 (docs/CI/依存) pkg:e2e packages/e2e labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

🚀 Preview Deployment

Name URL
Preview https://cb518534.typedcode.pages.dev

Deployed from commit f6b52a5
API: staging Workers

@shinyaoguri

Copy link
Copy Markdown
Owner Author

Cloudflare Pages プレビューでの実測

https://cb518534.typedcode.pages.dev/casual#255 が起きていたのと同じ経路 (Pages 配信の本番ビルド) で確認した。

項目 結果
Class extends value undefined / Le is not a function 消えた (0 件)
Monaco マウント OK
イベント記録 OK (#0 humanAttestation 含め 5 events)
C 実行 (@wasmer/sdk) PAGES_PREVIEW_C_OK 42 / C exited with code 0

残っている console エラー 4 件はすべてアプリ外のインフラ由来で、本件とは無関係:

  • 2 件 … Cloudflare Web Analytics のビーコン (cloudflareinsights.com/cdn-cgi/rum) の CORS。スモークの isIrrelevant フィルタが除外している対象そのもの
  • 2 件 … Turnstile の challenge-platform iframe が出す %c%d 形式の内部ログ

ローカル vite preview では Python (Pyodide) も TLA_SMOKE_PY_OK 45 / exit 0 を確認済み。

@shinyaoguri
shinyaoguri merged commit dfe07c1 into main Aug 5, 2026
7 checks passed
@shinyaoguri
shinyaoguri deleted the fix/editor-drop-tla-plugin branch August 5, 2026 15:52
shinyaoguri added a commit that referenced this pull request Aug 5, 2026
…259)

## 目的

Dependabot の security update (undici) が **failure
で終わり続けていた**。自動更新が詰まっており、放置すると脆弱性が残る。

原因は依存の経路にある:

```
wrangler 4.115.0 → miniflare 4.20260722.1 → undici 7.28.0 (脆弱)
```

パッチ版は `7.29.0` だが、Dependabot が見つけた唯一の更新パスが **wrangler を 4.115.0 → 4.35.0
へダウングレードする**ものだったため、自ら中止していた。

**wrangler を上げても解決しない。** 最新の 4.119.0 が要求する `miniflare
5.20260801.0-alpha` も undici を `7.28.0` で固定している (確認済み)。そのため Dependabot
自身が提示するもう一方の案 — override による pin — を採る。

## 影響度

対象は 5 件 (`GHSA-4cwx-7wf7-3272` high / 他 medium 4)。**いずれも `scope:
development`** で、lockfile 上も `dev=true` の 1 エントリのみ。undici は
wrangler・miniflare のローカル開発サーバとデプロイ用ツールにしか入っておらず、**Cloudflare Workers
の本番ランタイムには配布されない** (Workers は自前の fetch 実装を使う)。実際の影響範囲はローカルの `wrangler
dev` と CI の e2e 環境。

## 変更点

- root `package.json` の `overrides` に `"undici": "^7.29.0"` を追加
  - このリポは既に `vite` / `dompurify` / `ws` / `uuid` で同じ手を使っており、その延長
- `overrides` から `"rollup": "^4.59.0"` を**削除**
- [#256](#256) で editor から
`vite-plugin-top-level-await` (と、そのためだけの rollup 依存) を落とした結果、lockfile 上の
rollup エントリは 0 件・依存元も 0 件で dead になった
- `package-lock.json` は **undici 7.28.0 → 7.29.0 の 3
行のみ**。他のバージョン変動・パッケージの増減なし

> 2 つの override 変更を 1 PR にまとめている。同じ 7 行のオブジェクトを触るため分割すると lockfile
が衝突するのと、rollup の除去は #256 の直接の帰結で文脈が同じため。コミットは分けていない (どちらも 1 行の削除/追加)。

## 確認方法

| 検証 | 結果 |
|---|---|
| `node_modules/undici` の解決先 | **7.29.0** |
| `npm audit` | **found 0 vulnerabilities** |
| rollup が lockfile に復活していないか | エントリ **0 件** |
| `npm run lint` | exit 0 |
| `npm run typecheck` | exit 0 |
| ユニットテスト (全 workspace) | **608 passed** / 3 skipped |
| `npm run build` | exit 0 |
| **`npm run test -w @typedcode/e2e`** | **17 passed** |
| `npm run test:build -w @typedcode/e2e` | 1 passed |

**e2e が本命の検証。** miniflare は undici を `7.28.0` で厳密指定しているため、7.29.0
への差し替えは未テストの組み合わせになる。実際に `wrangler dev` (= miniflare) を起動する e2e
が通ることを、この override を入れてよいかの判断材料にした。

## 残る懸念

miniflare 側が undici を上げるまで、この override は「上流が想定していない組み合わせ」であり続ける。壊れた場合は
e2e が CI で落ちるので気付ける。上流が追いついたら override を外せる。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation pkg:e2e packages/e2e pkg:editor packages/editor pkg:repo リポジトリ横断 (docs/CI/依存)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(editor): 本番ビルドで Monaco が起動しない (Class extends value undefined) — main が壊れている

1 participant