Skip to content

feat(vrowzer): finish vrowzer.build() with .env files, tsconfig and CommonJS dependencies - #78

Merged
kazupon merged 8 commits into
feat/build-html-appfrom
feat/build-rest
Oct 2, 2026
Merged

kazupon merged 8 commits into
feat/build-html-appfrom
feat/build-rest

Conversation

@kazupon

@kazupon kazupon commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Closes #36. Stacked on #77 (stage 2, HTML app builds).

This PR is the last stage of #36. vrowzer.build() now reads the .env files and tsconfig of the project, builds CommonJS dependencies for production, supports CSS Modules, and reports what it does not support.

What it does

  • .env files:
    • import.meta.env exposes the variables with the client prefix (VITE_ by default) of .env, .env.local, .env.[mode] and .env.[mode].local, as in Vite.
    • Only the files of the project are read, not the environment variables of the host. They are not copied to the outputs.
    • The previews read them too, when ready() starts them.
  • tsconfig: builds apply the tsconfig.json of the project, or the file of the new tsconfig option (Vite 8.3), e.g. JSX settings, experimentalDecorators, verbatimModuleSyntax, and paths with resolve.tsconfigPaths.
  • CommonJS dependencies: with build: true, the manifest of @vrowzer/vite-plugin also has the original files of the CommonJS packages that it bundles into ES modules for the previews, e.g. react. Builds bundle them for production, as Vite does. The previews keep the ES modules.
  • CSS Modules work in the previews and in builds (now tested, with composes).
  • Warnings:
    • They keep the code, plugin, module, location and code frame of rolldown's warnings.
    • The errors that Vite logs without failing the build are returned with them.
    • The warning about large chunks is back.
  • What builds do not support is reported, with the module and the location:
    • import.meta.glob(), dynamic imports with variables in their relative paths, and new Worker(new URL(...)) are warnings.
    • ?worker imports fail with a clear error instead of "default" is not exported.

How it works

  • .env files:
    • env.ts takes the form of Vite 8.3.1. With NOTE(kazupon): it parses with dotenv's parse(), because the browser has no parseEnv() of node:util, and leaves out Vite Task's getEnvs().
    • config.ts calls loadEnv() again. Its envFile warning and envPrefix check are back, and so is the %ENV% prefix of html.ts.
    • dotenv-expand goes to ^13.0.0 with upstream's patch, which lets a variable use the ones of the other .env files. Upstream's test fails without it.
    • The Web Worker of the previews now writes the project files before it resolves the config, so that the config reads them. The fork's resolveConfig() does not load config files, so the project's vite.config.* is not read.
  • tsconfig: builds pass config.tsconfig to rolldown and to the native transform and resolve plugins, as upstream does.
    • Without it, rolldown and the native plugins already found the tsconfig.json of each module.
    • The warning that resolve.tsconfigPaths has no effect is now for the previews only.
  • CommonJS dependencies:
    • The original files go to .vrowzer-cjs/ inside each package, with the original package.json, so that they stay CommonJS: the rewritten package.json is type: "module".
    • Its exports give the development condition the ES module, and the other conditions ./.vrowzer-cjs/....
    • The targets stay inside the package, because the native resolver of builds rejects a target outside it (Invalid "exports" target).
    • build is part of the cache key, and scripts/generate-manifest.ts --build sets it.
  • Warnings: a plugin of the builder wraps build.rolldownOptions.onLog. It calls the config's onLog, then Vite's handler, and adds the details to the warning that Vite logged, so Vite's handling of the logs does not change.
  • What builds do not support: another plugin checks the JavaScript of the project's modules after the other plugins. Strings, comments and dependencies are left out.

Differences from the plan

  • The original CommonJS files are inside each package instead of a shared directory, for the type and the resolver above.
  • tsconfig applies to builds only. The previews do not read tsconfig, because their transform still has the old, stubbed path. Porting upstream's dev transform would touch the transform of every TypeScript file of the previews, so it is left for another issue.

Not supported, or not checked

  • A development build (NODE_ENV=development, e.g. in a .env file) cannot resolve the CommonJS packages of the manifest: the development condition still points outside the package (../.vrowzer-esm/), which the native resolver rejects.
  • Changes to .env files after ready() reach the next build, but not the running previews (upstream restarts its server).
  • Browsers:
    • The build is tested in Chromium.
    • In WebKit 26 (Playwright), crossOriginIsolated is false, so SharedArrayBuffer is missing and Vrowzer itself does not start. The likely cause is that WebKit does not support COEP credentialless. This is outside Add vrowzer.build() for browser-side production builds #36.
    • Firefox could not be launched in my environment (a macOS sandbox restriction), so it is not tested.
  • import.meta.glob, variable dynamic imports and the Workers of the project stay out of Add vrowzer.build() for browser-side production builds #36's scope. They are reported, not built.

Measurements

macOS arm64, Chromium:

  • Host output:
    • the builder grows by 14,519 bytes to 1,404,471 bytes
    • the transformer of the previews grows by 12,114 bytes to 1,460,695 bytes (dotenv, because the previews read .env files)
  • The manifest of build: true for the react e2e project gets the original files of react (169,358 bytes) and react-dom (7,317,229 bytes). Minified with Oxc, as the automatic manifest does, that is about 3.1 MB. The spike's estimate (549 KiB) counted only the production entries for browsers. Trimming the files to what the production conditions reach is left for another issue.

Tests

All on macOS (arm64):

  • vp run build, the type checks, oxlint, knip and the format check pass.

  • Unit tests: 2183 passed, 3 skipped (39 more than stage 2).

    • ported from upstream:
      • loadEnv() with its fixtures, and resolveEnvPrefix()
      • the test of the top-level tsconfig
      • the test that the library entry of the top-level input does not mutate the user config, missed in stage 1
    • new:
      • .env files in resolveConfig(), and the order of setupWorker()
      • tsconfig in builds in Node, including the tsconfig option and extends
      • the manifest for builds, and a build in Node that resolves its layout and bundles only the production branch
      • the details of the warnings in a build in Node
      • the reports of what builds do not support
  • Integration tests:

    Project Result
    fs 3 passed
    node-polyfill 769 passed, 6 skipped
    rolldown 4 passed
    service-worker-server 10 passed
    service-worker 27 passed
    unplugin-service-worker 63 passed, 1 skipped
    vrowzer 109 passed, 1 skipped (3 new)

    build.integration-test.ts gets a project with .env files, a tsconfig.json, CSS Modules and a CommonJS dependency in the layout of the manifest. In the browser, it checks:

    • the variables of the .env files in the preview (development) and the build (production), without the unprefixed ones and without the env files in the outputs
    • tsconfig in the build: the build Worker reads it from the snapshot
    • the production branch of the CommonJS dependency in the build, and its ES module in the preview
    • CSS Modules in the preview and in the build
    • the details of the warnings, and the warning about import.meta.glob()
  • e2e, with the Vite caches cleared first:

    • dev mode: 46 passed, 6 skipped
    • host build mode: 39 passed, 13 skipped

…p-level input

Stage 1 ported the fix of upstream #23135, which does not mutate the
user's `build.lib` when the top-level `input` gives its entry, but not
its test. The test fails without the fix.
`resolveConfig()` loads the `.env` files of `envDir` again, so
`import.meta.env` gets the variables with the client prefix (`VITE_` by
default), in builds and in previews.

- `env.ts` takes the form of Vite 8.3.1, with `NOTE(kazupon)`: the files
  are parsed with dotenv's `parse()`, because the browser has no
  `parseEnv()` of `node:util`, and the variables of Vite Task
  (`getEnvs()`) are left out.
- `dotenv-expand` goes to `^13.0.0` with upstream's patch, which lets a
  variable use the ones of the other `.env` files. The lockfile is
  updated by hand, because pnpm 12.1.0 drops unrelated entries when it
  updates it.
- `config.ts` restores the deprecation warning of `envFile` and the
  check of `envPrefix`, and `html.ts` the prefixes of the `%ENV%`
  replacement.
- The Web Worker of the previews now writes the project files before it
  resolves the config, so the previews read the `.env` files given to
  `ready()`. Later changes of them do not restart the previews; the
  next build reads them.

Only the files of the project are read: `process.env` is the polyfill's
empty object, so nothing of the host is exposed.

Tests:

- ported from upstream: `loadEnv()` with its fixtures, and
  `resolveEnvPrefix()`
- `resolveConfig()` exposes only the prefixed variables, takes
  `envPrefix` and `envDir: false`, and rejects an empty prefix
- `setupWorker()` writes the files before it resolves the config
The config gets the `tsconfig` option of Vite 8.3 again: the path of the
TypeScript configuration, resolved from the root. Builds pass it to
rolldown, and to the native transform and resolve plugins, as upstream
does. `build.ts` no longer reads it without the type.

Without the option, rolldown and the native plugins find the
`tsconfig.json` of each module themselves, so its JSX settings,
`experimentalDecorators` and `paths` (with `resolve.tsconfigPaths`)
applied to builds already. The option selects another file, e.g.
`tsconfig.app.json`, with what it extends.

The warning that `resolve.tsconfigPaths` has no effect is now for the
previews only, which still resolve with the JavaScript resolver and do
not read tsconfig. Builds resolve with the native resolver, which
supports it.

Tests:

- upstream's test that the top-level `tsconfig` applies to the rolldown
  options, commented out in stage 1
- builds in Node apply the `tsconfig.json` of the project, and the file
  of the `tsconfig` option (the test fails without the option)
- the warning of `resolve.tsconfigPaths`, for the previews only
Builds bundled the CommonJS packages that the manifest rebuilds into ES
modules for the previews, e.g. react, in their development form: the
manifest had only the ES modules, bundled with `process.env.NODE_ENV`
set to `"development"`.

With the `build` option, the manifest now also has the original files of
those packages, and builds bundle them for production, as Vite does:

- The original files, with the original `package.json`, are in
  `.vrowzer-cjs/` inside the package, so that they stay CommonJS.
- The rewritten `package.json` gives the `development` condition of the
  previews the ES module as before, and the other conditions the
  original files. Its targets stay inside the package: the native
  resolver of builds rejects a target outside it ("Invalid exports
  target"), which the ES modules of the previews have.
- Without the option, the manifest does not change. The option is part
  of the cache key of the automatic manifest, and the `--build` flag of
  `scripts/generate-manifest.ts` sets it.

Tests:

- vite-plugin: the exports for builds, and the manifests with and
  without the option
- vite-dev-server: a build in Node resolves this layout, and bundles only
  the production branch of a CommonJS package
The warnings of `build()` were only messages.

- The warnings of rolldown keep their code, plugin, module, location and
  code frame. Vite's `onRollupLog()` passes only the message of a warning
  to the logger, so a new plugin of the builder wraps
  `build.rolldownOptions.onLog`. It calls the `onLog` of the config
  first, then Vite's handler, and adds the details to the warning that
  Vite logged. The warnings that Vite ignores, the error of an
  unresolved import and the user's `onLog` stay as they are.
- The errors that Vite logs without failing the build, e.g. a CSS
  `@import` that is not found, are returned with the warnings. The
  collecting logger records them for `hasErrorLogged()`.
- The warning of the chunks larger than `build.chunkSizeWarningLimit` is
  back, with the message of rolldown's reporter, for the minified builds
  of apps. The reporter checks the sizes when it writes the outputs,
  which the builder does not do.

Tests: the logger, the plugin and the size check, and a build in Node
that keeps the code, the module and the location of an `EVAL` warning
(the test fails without the plugin).
A new plugin of the builder reports, in the modules of the project,
what builds do not support yet, instead of leaving broken outputs:

- warnings, with the module and the location:
  - `import.meta.glob()`, which the output keeps. It includes the
    template strings with variables of `new URL(..., import.meta.url)`,
    which `vite:asset-import-meta-url` turns into it.
  - dynamic imports with variables in their relative paths, whose files
    the build does not include. `/* @vite-ignore */` suppresses it, as
    in Vite.
  - `new Worker(new URL(...))`, whose file the build emits as an asset
    without bundling or transforming it
- an error for the `?worker` and `?sharedworker` imports, which failed
  with "default is not exported" before

It runs after the other plugins, so that it sees JavaScript. Strings and
comments are left out, and the dependencies are not checked.

Tests: the plugin, and builds in Node that report the warnings with the
module and the line, and the error of `?worker`.
… and CSS Modules

The fixture `integration/build/` gets a third project, with:

- `.env`, `.env.development`, `.env.production` and `.env.local`, with
  variables with and without the client prefix, and an expanded one
- a `tsconfig.json` with `verbatimModuleSyntax`, which keeps a type-only
  import with a side effect. The previews do not read tsconfig, so they
  still run without the side effect.
- CSS Modules with `composes`
- a CommonJS dependency in the layout of the manifest of
  `@vrowzer/vite-plugin` with the build option, with branches for
  production and development

`build.integration-test.ts` checks, in the browser:

- the variables of the `.env` files in the preview (development) and in
  the build (production). The variables without the prefix are not
  exposed, and no env file is in the outputs.
- tsconfig applies to the build. The build Worker reads it from the
  snapshot.
- the build bundles the production branch of the CommonJS dependency,
  and the preview takes its ES module for development
- CSS Modules in the preview and in the build: a scoped class, and
  `composes`
- the warnings keep their code, module and location, and report
  `import.meta.glob()`
- vrowzer README:
  - the `.env` files that builds and previews read, the client prefix,
    and when the previews read them
  - tsconfig, which builds apply and the previews do not read yet
  - CSS Modules
  - the CommonJS dependencies built for production, and that a
    development build cannot resolve them yet
  - the details of `warnings`, the errors logged without failing the
    build, and the warning about large chunks
  - what the build warns about: `import.meta.glob()`, variables in the
    relative paths of dynamic imports and `new URL()`, and the Workers
    of the project, with `?worker` failing the build
  - browsers: the build is tested in Chromium. In WebKit, which does not
    support COEP `credentialless`, the page is not cross-origin
    isolated, and Vrowzer does not start. Firefox is not tested.
- vite-plugin README: the manifest of `build: true` has the original
  files of the CommonJS packages.
@kazupon
kazupon added this pull request to stack #76 October 2, 2026 17:53
@kazupon
kazupon merged commit 6fad561 into main Oct 2, 2026
4 checks passed
@kazupon
kazupon deleted the feat/build-rest branch October 2, 2026 18:10
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.

Add vrowzer.build() for browser-side production builds

1 participant