Repository navigation
feat(vrowzer): build HTML apps in the browser with vrowzer.build() - #77
Merged
Merged
Conversation
…yfill Builds can now take an HTML entry. `buildHtmlPlugin()` and its helpers (`getCssFilesForChunk()`, `getLinkShouldInline()`, `isEntirelyImport()` and the import map hooks) come from upstream's `html.ts`, at the same commit as the fork's `asset.ts`, `css.ts` and `build.ts` (`cf5c0288d`, 23 commits after 8.3.1). The HTML plugin imports the id of the module preload polyfill, so `modulePreloadPolyfill.ts` comes with it. Both plugins are registered only for builds, so the dev Web Worker does not change: it has no bundled environment, and it did not register the polyfill before. The Worker aggregates import the polyfill plugin eagerly, like the other plugins. With `NOTE(kazupon)`: - `postImportMapHook()` is called in the fork's form, which does not support the chunk import map (Vite 8.3) yet. - The dev client of bundled dev mode is not injected (bundled dev was removed in #41). Tests: - ported from upstream: `getCssFilesForChunk()`, the module preload polyfill, and the builds of `build.spec.ts` that use the HTML entry (the hashes of CSS chunks, `renderBuiltUrl`, the tree shaking of JSON and the copy of the public directory). `onRollupLog` builds the HTML entry again, as upstream does. - `resolveConfig()` registers the plugins for builds, and leaves them out of the dev pipeline. - an HTML entry: the script and the style of the output HTML, and the polyfill with and without `modulePreload.polyfill` - Vitest gives the original module to the second of two concurrent dynamic imports of a mocked module, so the ported tests build one after another. - `html.ts` now imports the build code, so the tests that load it use the Node build of rolldown.
Builds now preload the CSS and the imports of lazy chunks. `importAnalysisBuild.ts` comes from upstream, at the same commit as `html.ts` (`cf5c0288d`, with #23510 and #23611 after 8.3.1), and `resolveBuildPlugins()` registers `buildImportAnalysisPlugin()` again. Library builds do not insert the preload, as upstream. With `NOTE(kazupon)`: - The preload code declares `isCssPreloadUrl()` under the name it has at runtime (`isCssPreloadUrl.name`). The build inlines `preload()` into the output with `toString()`. The host build of an app that uses vrowzer may minify the builder, which renames `isCssPreloadUrl`, also where `preload()` calls it, while upstream's code declares it under the original name. - `build.chunkImportMap` (Vite 8.3) is read without the type, as in `build.ts`. A new bundle check of the builder stops the build of vite-dev-server unless the preload code declares the helper with `.name`, `preload()` calls it, and `preload()` calls no other function of the bundle. Tests: - ported from upstream: `matchImportsToPreloadMarkers()` and `isCssPreloadUrl()` - an HTML entry with lazy chunks: each preload has the chunk, its shared chunk and its CSS, and the helper comes with `isCssPreloadUrl` - `resolveConfig()` registers the preload plugins for builds
`new URL('./file', import.meta.url)` now emits the file in builds, and
points to its output URL. `assetImportMetaUrl.ts` comes from upstream,
changed only by the `curly` lint style of the fork. It is registered
only for builds, as the dev Web Worker did not register it, and the
Worker aggregates import it eagerly.
Tests:
- ported from upstream: the template strings with variables, which
become `import.meta.glob()`. They parse with rolldown's `parseAst`, as
the fork does not depend on rollup.
- a build in Node: an image is emitted and its URL is replaced, a public
file keeps its URL, and a missing file is left for the runtime
- `resolveConfig()` registers the plugin for builds only
The builder no longer requires `build.lib`. Without it, a build builds an app from one HTML entry: `build.rolldownOptions.input`, then the top-level `input`, and `/index.html` without them, as `resolveRolldownOptions()` takes them. An app build is rejected with `VROWZER_UNSUPPORTED_OPTION` when its entry is an array or an object, as libraries with more than one entry are, or when the entry is not an HTML file. A JavaScript entry is built with `build.lib`. The descriptions of `build()` and of its `lib` and `rolldownOptions.input` options cover app builds. The browser test of unsupported options rejects an app entry that is not HTML, instead of a build without `build.lib`.
The fixture `integration/build/` gets the files of an HTML app: an `index.html` with a stylesheet, an image and a module script, an entry with CSS, a small SVG, `?url`, `?raw`, `new URL(..., import.meta.url)` and a lazy chunk with its CSS, two images larger than the inline limit, a public file, and a nested HTML entry. `build.integration-test.ts` builds the app in the browser, and serves the outputs from another origin with Playwright's routing, as a static server does: - with the bases `/`, `/app/` and `./` (served under a nested path), the app runs without the Service Worker, and every URL of the outputs is served. The CSS of the HTML, of the entry and of the lazy chunk applies, the images keep their bytes, and the outputs have nothing of the dev server. - the rewritten HTML, the lazy chunk and its CSS, the preload and the module preload polyfill, and the source maps - a nested HTML entry - the default `/index.html` that the runtime gives a project without one, and the preview after the build The fixture serves the builder minified by the host build, so the lazy CSS also checks the preload helper with the renamed `isCssPreloadUrl`. An app build with two HTML entries is rejected.
- README: `build()` builds an app from its HTML entry, or a library with `build.lib`. The entry of an app, what the build of an app does, the three kinds of `base`, and the outputs that run on a static server. - The list of what is not supported yet is split into the options that the build rejects (now including an app with more than one entry or with an entry that is not HTML), and what it does not check: `import.meta.glob()`, variables in the paths of dynamic imports and of `new URL()`, the Workers of the project, `.env` files, tsconfig and the CommonJS files of dependencies. CSS Modules are not tested yet. - The API reference of `build()` is generated again.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #36. Stacked on #75 (stage 1, library builds).
vrowzer.build()now builds HTML apps. This PR is stage 2 of #36: the HTML build plugin, the preload helpers andnew URL(..., import.meta.url)of upstream Vite, and the builder accepts an app with one HTML entry.What it does
build.lib,build()builds an app from one HTML entry:/index.htmlby default (the default one ofready()when the project has none), or another HTML file withbuild.rolldownOptions.input, e.g./nested/index.html.?raw,?url,new URL('./file', import.meta.url)and/publicworkmodulepreload, with its polyfill by default)basecan be/,/app/or./. The outputs run on another origin as static files, without the Service Worker, the dev server, the HMR client or the preview URLs.VrowzerBuildError(VROWZER_UNSUPPORTED_OPTION) when its entry is an array or an object, or is not an HTML file. A JavaScript entry is built withbuild.lib.Ported from upstream Vite
From the same commit as the fork's
asset.ts,css.tsandbuild.ts(cf5c0288d, 23 commits after 8.3.1):html.ts:buildHtmlPlugin(),getCssFilesForChunk(),getLinkShouldInline(),isEntirelyImport()and the import map hooksmodulePreloadPolyfill.tsimportAnalysisBuild.ts: the preload helpers (with #23510 and #23611)assetImportMetaUrl.tsThey are registered only for builds, so the previews do not change.
With
NOTE(kazupon):isCssPreloadUrl()under the name it has at runtime (isCssPreloadUrl.name).preload()into the output withtoString().isCssPreloadUrl, also wherepreload()calls it. In the fixture of the browser tests it becomesqG..namepreload()calls itpreload()calls no other function of the bundlepostImportMapHook()is called in the fork's form, which does not support the chunk import map (Vite 8.3) yet, andbuild.chunkImportMapis read without the type.Differences from the plan
import.meta.globis not included.importGlobPlugin()for builds, assuming the fork had its native path ready. It does not: the whole plugin is commented out, and is older than upstream (nocaseSensitive).import.meta.globeither, so an app that uses it could be built but not previewed.Not supported yet
Rejected with
VROWZER_UNSUPPORTED_OPTION:esand libraries with more than one entry, CSS minification, theterserandesbuildminifiers,build.watch, SSR, manifests and license files)Not checked, and documented:
import.meta.glob(), and variables in the paths of dynamic imports and ofnew URL(..., import.meta.url). The build leaves them in the output without the files, so they fail when they run.vite:asset-import-meta-urlturns a template string with variables intoimport.meta.glob(), as upstream does.new Worker(new URL(...))and?worker).envfiles, tsconfig, and the CommonJS files of dependencies for production (stage 3)CSS Modules work in a Node build, but are not tested in the browser yet (stage 3).
Other changes
picocolorstoo).@vrowzer/rolldownto the Noderolldownwithvi.mock(), so the ported upstream tests that run two builds withPromise.allbuild one after another.html.tsnow imports the build code, so the unit tests that load it use the Node build of rolldown too.There are no breaking changes.
Measurements
macOS arm64, Chromium, the app of the fixture:
build()of the app: 181–224 ms with builds spaced out, about 150 ms in a row. A library takes 168–208 ms in the same run. Each includes creating the build Worker and loading the builder.build: true, the builder (web-worker-builder-*.js) grows by 18,778 bytes to 1,389,952 bytes.web-worker-transformer-*.js, also withbuild: false) grows by about 16.8 KB: the new plugins are in its bundle, though only builds register them. This is estimated by minifying the bundles of stage 1 and stage 2 with Oxc (1,430,547 → 1,447,333 bytes); the host output is 1,448,581 bytes.Tests
All on macOS (arm64):
vp run build, the type checks, oxlint, knip and the format check pass.Unit tests: 2144 passed, 3 skipped (46 more than stage 1).
getCssFilesForChunk(), the module preload polyfill,matchImportsToPreloadMarkers(),isCssPreloadUrl()andassetImportMetaUrlPlugin()build.spec.tsthat use the HTML entry: the hashes of CSS chunks,renderBuiltUrl, the tree shaking of JSON and the copy of the public directoryonRollupLogbuilds the HTML entry again, as upstream doesresolveConfig()registers for builds and leaves out of the previewsnew URL()in buildsIntegration tests:
build.integration-test.tsbuilds an HTML app in the browser, and serves the outputs from another origin with Playwright's routing, as a static server does. It covers:/,/app/and./(served under/sub/dir/). The app runs without the Service Worker, and every URL of the outputs is served:<img>,?urlandnew URL())?rawand/public/index.htmlof the runtimeThe fixture's builder is minified by the host build, so the lazy CSS also checks the preload helper with the renamed
isCssPreloadUrl.e2e, with the Vite caches cleared first: