Skip to content

Vite migration#1526

Draft
cocomarine wants to merge 12 commits into
mainfrom
vite-migration
Draft

Vite migration#1526
cocomarine wants to merge 12 commits into
mainfrom
vite-migration

Conversation

@cocomarine

Copy link
Copy Markdown
Contributor

No description provided.

cocomarine and others added 2 commits July 8, 2026 15:51
Phase 0 of migrating the root editor-ui build from the custom ejected-CRA
webpack 5 setup to Vite ~8.1.x, matching the toolchain already used by
apps/scratch-frame (rolldown-vite, @vitejs/plugin-react classic runtime,
envPrefix REACT_APP_, vite-plugin-static-copy).

Captures the locked decisions and the phased approach:
- Keep Jest (bundler-independent; one edit to scripts/test.js to drop the
  config/env dependency, preserve Jest-only deps).
- Use a `define` shim so process.env.* stays in source (keeps Jest green);
  no codemod to import.meta.env.
- Land as a single PR; root pinned to ~8.1.x, scratch-frame left at ^8.0.16.

Documents the non-trivial webpack behaviours needing deliberate Vite
equivalents: SCSS-as-string Shadow DOM injection (?inline), split SVG
handling (svgr for icons, URLs otherwise), the standalone PyodideWorker.js
output plus COOP/COEP/CORP headers, multi-page HTML entries, node
polyfills, static-copy, and dev server on port 3011 for Cypress.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 1 of the Vite migration: add the build-time devDependencies needed
for the Vite config, without yet touching webpack. Webpack-only packages
are intentionally left in place and will be removed in the final phase,
once the Vite build is verified green.

Added (resolved versions):
- vite ~8.1 -> 8.1.2 (Rolldown-powered, exposes build.rolldownOptions,
  matching apps/scratch-frame's config style)
- @vitejs/plugin-react ^6 -> 6.0.3 (classic JSX runtime, hosts the
  babel-plugin-prismjs config)
- vite-plugin-svgr ^5.2.0 (SVG-as-React-component for src/assets/icons,
  replacing @svgr/webpack)
- vite-plugin-static-copy ^4 -> 4.1.1 (replaces copy-webpack-plugin for
  src/projects and python-error-copydecks; same plugin scratch-frame uses)
- vite-plugin-node-polyfills ^0.28.0 (replaces webpack resolve.fallback
  for stream/assert/path/url pulled in transitively by skulpt/jszip)

sass, postcss, babel-plugin-prismjs and the stylelint stack are already
present and reused. The pre-existing peer-dependency warnings (eslint,
react ranges) are unchanged by this step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cocomarine cocomarine temporarily deployed to previews/1526/merge July 8, 2026 15:07 — with GitHub Actions Inactive
cocomarine and others added 4 commits July 8, 2026 16:20
Vite is HTML-centric and emits each HTML input at its path relative to the
project root, so the two entry templates must live at the root (not under
src/) to build to build/web-component.html and build/html-renderer.html --
matching the filenames webpack's HtmlWebpackPlugin produced today.

- Move src/web-component.html -> web-component.html
- Move src/index-html-renderer.html -> html-renderer.html
- Add explicit <script type="module"> tags pointing at the entry modules
  (/src/web-component.js and /src/html-renderer.jsx). HtmlWebpackPlugin
  injected these automatically; Vite requires them in the template.
- Point the still-present webpack HtmlWebpackPlugin templates at the new
  root paths so webpack.config.js stays loadable until it is removed in the
  final phase.

All references to the served URLs (/web-component.html in Cypress specs and
docs, /html-renderer.html in HtmlRunner's iframe) are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the Vite config for the main editor bundle, mirroring the conventions
already used by apps/scratch-frame while adapting to editor-ui's needs.
Webpack is left in place; it is removed in the final phase.

Plugins:
- @vitejs/plugin-react with the DEFAULT automatic JSX runtime (not classic):
  babel-preset-react-app and the Jest transform both use automatic, and many
  components do not import React, so classic would break them. Carries over
  babel-plugin-prismjs from .babelrc.
- vite-plugin-svgr scoped to src/assets/icons/**/*.svg with exportType
  "default", reproducing the old @svgr/webpack rule; all other .svg imports
  remain URL assets (as url-loader did).
- vite-plugin-node-polyfills for stream/path/url/assert, replacing webpack's
  resolve.fallback (these are reached transitively by skulpt/jszip).
- vite-plugin-static-copy for src/projects -> projects and the
  python-friendly-error-messages copydecks, replacing copy-webpack-plugin
  (public/ is handled natively by Vite's publicDir).
- A small cross-origin-resource-policy dev plugin reproducing the
  setupMiddlewares CORP header for the Pyodide shims, the worker and the
  html-renderer page (required under COEP: require-corp).

Env: a define shim keeps process.env.* in the source (so Jest is unaffected);
every key used in src/ is enumerated, with NODE_ENV bound to the Vite mode.

Base: root-relative in dev (Vite owns the origin) and the absolute PUBLIC_URL
in production (bundle is served from a CDN and embedded cross-origin), matching
react-dev-utils' getPublicUrlOrPath under the old build.

Output: entryFileNames "[name].js" so external consumers keep loading a stable,
unhashed /web-component.js and /html-renderer.js, exactly as webpack emitted.

Server headers (COOP/COEP/CORS) and port 3011 match the old devServer so the
Cypress suite is unaffected. Config verified to load in serve and build modes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PyodideWorker.js is a standalone classic script (globalThis.PyodideWorker +
importScripts) loaded cross-origin from a Blob via
`importScripts("${PUBLIC_URL}/PyodideWorker.js")` in PyodideRunner.jsx, so it
is not part of the main module graph and cannot be an ESM chunk. Reproduce the
dedicated webpack "PyodideWorker" entry:

- vite.worker.config.js: a separate build that emits an unhashed IIFE
  build/PyodideWorker.js with emptyOutDir:false and copyPublicDir:false (so it
  neither clobbers the main build nor re-copies public/). Its only build-time
  need is substituting process.env.ASSETS_URL, done via define. Verified: the
  output is a classic script (no import/export), keeps its importScripts calls,
  assigns globalThis.PyodideWorker, and has ASSETS_URL fully inlined.
- vite.config.js: a serve-only middleware that responds to /PyodideWorker.js in
  dev by reading the source and applying the same process.env substitutions
  (the worker build is not run during `yarn start`), with a permissive CORP
  header. Read per-request so worker edits are picked up without a restart.

The getWorkerURL blob loader is unchanged; it keeps fetching
${PUBLIC_URL}/PyodideWorker.js. Wiring the worker build into the `build` npm
script is done in phase 4 (scripts & CI).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
External host sites embed the editor with a plain classic
<script src="...web-component.js"> tag (README "Usage"), not an ES module, and
web-component.js is side-effect-only (it just registers the <editor-wc> custom
element). Vite's default HTML-entry output is an ES module loaded via
<script type="module">, which a classic <script> tag cannot execute -- this
would silently break every consuming site. The app also has no dynamic imports,
so, exactly like the old webpack `[name].js` entries, each entry can and must be
a single self-contained bundle.

Change the build to emit classic IIFE bundles instead of an HTML-entry MPA:
- vite.lib.js: shared helpers (buildDefine, resolveBase, appPlugins,
  emitClassicHtml, iifeBuildOptions). iifeBuildOptions produces an unhashed
  [name].js IIFE; emitClassicHtml emits the entry's HTML template with its
  dev-only module <script> rewritten to a classic <script src="./bundle.js">, so
  the deployed preview pages load the bundle the same way external hosts do.
- vite.config.js: primary build emits web-component.js (IIFE) + its preview
  page, empties build/ and copies public/. Dev server behaviour is unchanged
  (appType "mpa" for the two HTML test pages, CORP headers, worker dev serving).
- vite.html-renderer.config.js: emits html-renderer.js (IIFE) + its preview
  page, appended to the same build/. It is loaded as a full page inside an
  iframe, so a single classic script is simplest and most robust cross-origin.

A classic IIFE build cannot contain more than one entry, so the three bundles
(web-component, html-renderer, PyodideWorker) are separate build configs chained
in the build script (wired in phase 4). No import.meta usage exists in src, so
IIFE is safe. The IIFE mechanism is already verified by the PyodideWorker build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cocomarine cocomarine temporarily deployed to previews/1526/merge July 8, 2026 16:02 — with GitHub Actions Inactive
The Vite/Rolldown native transform (builtin:vite-transform, oxc) treats .js as
JSX-free and errored on the <editor-wc> JSX in src/web-component.js; unlike
webpack's babel-loader (which ran on all .js), oxc keys JSX parsing on the file
extension and OxcOptions omits `lang`, so it cannot be forced per-extension.

The codebase convention is already .jsx for JSX and .js for plain JS. Only three
non-test .js files actually contain JSX, so rename them to match:
- src/web-component.js -> src/web-component.jsx
- src/utils/Notifications.js -> src/utils/Notifications.jsx
- src/utils/ResizableWithHandle.js -> src/utils/ResizableWithHandle.jsx

The emitted bundle keeps the name web-component.js (entryFileNames), so external
consumers are unaffected. All importers use extensionless imports, which resolve
to .jsx under Vite, webpack (resolve.extensions) and Jest (moduleFileExtensions),
so no import sites change. The entry references are updated: the dev module
script in web-component.html, the Vite build entry, and the webpack entry.

Note: this only fixes JSX parsing of the entry graph. A full `vite build` still
depends on the phase 3 source touch-ups (?inline SCSS, ?raw markdown) and is
verified end-to-end in phase 5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cocomarine cocomarine temporarily deployed to previews/1526/merge July 8, 2026 16:13 — with GitHub Actions Inactive
cocomarine and others added 3 commits July 8, 2026 17:38
Under webpack the CSS rule used css-loader with NO style-loader, so no .scss or
.css import ever injected into the DOM; the web component instead injects the
aggregated stylesheets into its shadow root manually via
<style>{internalStyles.toString()}</style>. Vite, by contrast, auto-injects any
plain CSS import into the light DOM, which would both leak styles onto the host
page and emit dead CSS.

Append ?inline to every .scss and .css import in src so each returns the
compiled CSS as a string and never auto-injects - exactly matching webpack:
- The four aggregator imports (InternalStyles, ExternalStyles, index, HtmlRunner)
  are used as strings; .toString() on a string is a no-op, so the call sites are
  unchanged and still feed the shadow-DOM <style> tags.
- The 56 per-component side-effect .scss imports and 3 third-party .css imports
  were redundant no-ops under webpack (the real styles come from the aggregated
  InternalStyles/ExternalStyles injected into the shadow root); ?inline keeps
  them no-ops instead of letting Vite inject them.

There are no CSS-module (.module.scss) imports, so ?inline is safe everywhere.

Jest: add moduleNameMapper entries stripping the ?inline query so the existing
jest-scss-transform / jest-css-modules-transform still apply. Verified
WebComponentLoader.test.js (which asserts on the injected <style> contents)
still passes 33/33.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The single .md import (demoInstructions in InstructionsPanel) was handled by
raw-loader under webpack, giving the markdown source as a string. Vite's
equivalent is the ?raw query suffix. Append it so the import keeps returning the
raw markdown string.

Jest: add a moduleNameMapper entry stripping the ?raw query so the import
resolves to the .md file, which continues to hit jest-transform-stub exactly as
before (markdown was already stubbed in tests, not loaded as real content).
Verified InstructionsPanel.test.js still passes 36/36.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first real `vite build` failed in vite-plugin-static-copy: it globs with
onlyFiles:true, so `copydecks/*` matched nothing (copydecks contains only an
`en/` subdirectory, no top-level files). It also builds each destination path
relative to the project root, so a deep node_modules source would otherwise be
recreated verbatim under dest.

Add a copyDirTarget helper (vite.lib.js) that copies a directory's contents
recursively (`**/*`) and strips exactly the leading path segments up to and
including the source dir via rename.stripBase - the same technique
apps/scratch-frame uses. This reproduces copy-webpack-plugin's
`{ from: dir, to: dest }` behaviour:
- src/projects -> build/projects/*.json
- .../python-friendly-error-messages/copydecks -> build/python-error-copydecks/
  preserving en/ (loadCopydeckFor fetches `${base}/${lang}/copydeck.json`).

With this, `vite build` completes and emits build/web-component.js. Verified it
is a self-contained classic IIFE (starts with `(function(){`), contains no
top-level import/export and no import.meta, and parses under vm.Script as a
classic script - i.e. external sites loading it via a plain <script> tag keep
working. The preview build/web-component.html correctly references it with a
classic <script src="web-component.js">.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cut the root start/build/analyze scripts over from webpack to the three Vite
configs (webpack.config.js itself is left in place; it is deleted in the final
phase once everything is verified):

- start: "vite" (was webpack serve). Dev server config, port and headers are
  unchanged - all defined in vite.config.js already.
- build: chains the three per-entry Vite builds in the order they must run -
  `vite build` (web-component, primary: empties build/ and copies public/),
  then `vite build --config vite.html-renderer.config.js` (appends
  html-renderer.js), then `vite build --config vite.worker.config.js` (appends
  PyodideWorker.js) - because a classic IIFE build can only have one entry.
  Verified: full chained build reproduces the old build/ tree (public/ assets,
  projects/, python-error-copydecks/, plus all three bundles), and both
  web-component.js and html-renderer.js parse as valid classic scripts.
- analyze: "ANALYZE=true yarn build" replaces "ANALYZE_WEBPACK_BUNDLE=true yarn
  build". Added rollup-plugin-visualizer (devDependency) and wired it into the
  primary build only, gated on ANALYZE=true, writing an interactive treemap to
  build/stats.html. It's ESM-only, so it's imported dynamically inside the
  (now async) config function rather than statically, so requiring the config
  never has to load it when analysis isn't requested. Verified the report is
  generated.

NODE_ENV/BABEL_ENV are no longer exported manually - the Vite CLI sets mode
(development for `vite`, production for `vite build`) automatically.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Update the docs that described the now-replaced ejected-CRA webpack build:

- AGENTS.md (symlinked from CLAUDE.md, read by AI agents working in this repo):
  replace "webpack dev server" / "webpack compiled successfully" mentions with
  the Vite equivalents, correct the dev-server startup timing (Vite compiles on
  demand and is ready in well under a second, not ~15s), note that config/ is
  now Jest-only (CRA/webpack-era leftovers, unused by the Vite build), and add
  an orientation note listing the three root vite.config*.js files so a future
  agent touching the build knows where to look.
- README.md: describe the build as Vite-based, drop the now-inaccurate "build
  is minified and the filenames include the hashes" (bundle filenames are
  fixed, not hashed - other sites load web-component.js by name) and the CRA
  deployment-docs link (replaced with the repo's own Deployment section),
  document the new `yarn analyze` output.
- docs/WebComponent.md: replace the CRA-eject / webpack.config.js description
  with the three Vite config files and what each builds.

docs/Deployment.md's webpack mention is left as-is; it's an explicitly dated
2022-08 historical snapshot, not living build documentation.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant