Skip to content

Replace Parcel with Vite for frontend builds - #4205

Closed
Mpdreamz wants to merge 6 commits into
fix/parallel-s3-syncfrom
fix/slow-docs-preview
Closed

Mpdreamz wants to merge 6 commits into
fix/parallel-s3-syncfrom
fix/slow-docs-preview

Conversation

@Mpdreamz

@Mpdreamz Mpdreamz commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

The frontend toolchain moves from Parcel to Vite. This eliminates the Windows TEMP workaround Parcel required, adds `node_modules` caching to the bootstrap action, and fixes a class of MSBuild false-positive errors caused by Vite chunk names that contain `Error` and `Warning` (EUI icon names).

Affects: Site UI, Automation

Prompt summary: #3884 replaced Parcel with Vite but went stale. The ask was to rebase it, regenerate `package-lock.json`, and add the `SkipNpmBuild`/`DesignTimeBuild` MSBuild hooks that prevent npm from running in IDE or `--no-build` contexts.

Why

Parcel required a Windows-specific workaround (redirecting `TEMP` to the same drive) and had no incremental-build hooks, so every `dotnet build` paid a full `npm ci` + `npm run build`. The MSBuild `Exec` task scanned Vite output for error/warning patterns and misclassified chunk filenames like `chunk-EuiIconWarning-xxx.js` and `chunk-EuiIconError-xxx.js` as build diagnostics, failing the build even when the exit code was 0.

What

Vite build pipeline

`vite.config.ts` replaces `.parcelrc` and `.postcssrc`. It configures `@tailwindcss/vite`, `@vitejs/plugin-react`, and `vite-plugin-svgr`, and produces the same five output artefacts (`main.js`, `styles.css`, `assembler.css`, `isolated.css`, `codex.css`) under `_static/`. The `watch` and `build` npm scripts call `vite` instead of `parcel`.

MSBuild integration

`SkipNpmBuild` and `DesignTimeBuild` conditions on `NpmInstall` and `NpmRunBuild` prevent npm from running in IDE design-time evaluations or when building with `--no-build`. A new `EnsureSkipNpmBuildArtifacts` target errors early when `SkipNpmBuild=true` but any of the five `_static/` artefacts are absent. `IgnoreStandardErrorWarningFormat="true"` on the `npm run build` exec stops MSBuild from misclassifying Vite chunk names as build errors. `EmbedGeneratedAssets` uses a recursive `_static/**/*.js` glob to pick up Vite's `assets/` subdirectory.

Incremental builds

`CleanGeneratedStaticAssets` runs before `NpmRunBuild` to remove stale `chunk-` and `asset-` files from previous Vite runs. It deliberately does not delete the stable named outputs (`main.js`, `styles.css`, etc.) — doing so would delete the files that MSBuild checks against the declared `Outputs` for incremental up-to-date detection, forcing a full `npm run build` on every `dotnet build` even when no source files changed.

CI and bootstrap

`node_modules` is cached in `bootstrap/action.yml` keyed on both `package-lock.json` and `.nvmrc` so a Node version bump invalidates the cache. The Windows TEMP workaround steps are removed from `ci.yml` and `release.yml`. The `<script defer>` tag becomes `<script type="module">` in `_Head.cshtml` to match Vite's ES module output.

Live reload

`ParcelWatchService` is renamed to `FrontendWatchService` and `DocumentationWebHost` is updated to use it. The `npm run watch` command now runs `vite` in watch mode.

Verify

```bash
cd src/Elastic.Documentation.Site
npm ci && npm run build && npm run test && npm run compile:check

dotnet build src/tooling/docs-builder -c release

Run twice; the second build should skip NpmRunBuild (up-to-date).

dotnet build src/tooling/docs-builder -c release
```

Stack: 4 of 5, on top of #4208. #4210 follows.

🤖 Generated with Claude Code

@Mpdreamz
Mpdreamz requested a review from a team as a code owner September 22, 2026 16:02
@Mpdreamz
Mpdreamz requested a review from akira28 September 22, 2026 16:02

@akira28 akira28 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great stuff

@Mpdreamz Mpdreamz changed the title Speed up the local docs preview workflow Replace Parcel with Vite for frontend builds Sep 22, 2026
@Mpdreamz
Mpdreamz changed the base branch from main to fix/parallel-s3-sync September 22, 2026 16:07
@github-actions

Copy link
Copy Markdown
Contributor

Docs preview (local build)

Handbook preview: https://docs-v3-preview.elastic.dev/elastic/docs-builder/pull/4205/

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes.


What is this? | From workflow: PR Review

Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.

@Mpdreamz

Copy link
Copy Markdown
Member Author

@reakaleek this is basically #3884 reworked, I instructed claude to update yours but it chose not too 😸

@Mpdreamz
Mpdreamz force-pushed the fix/slow-docs-preview branch from 474718c to aea1856 Compare September 22, 2026 16:30
@Mpdreamz
Mpdreamz force-pushed the fix/slow-docs-preview branch from aea1856 to 24957ca Compare September 22, 2026 16:32

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes based on the inline findings.


What is this? | From workflow: PR Review

Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.

<Target Name="EnsureSkipNpmBuildArtifacts"
BeforeTargets="NpmInstall;NpmRunBuild"
Condition="'$(SkipNpmBuild)' == 'true'">
<Error Condition="!Exists('_static/main.js') Or !Exists('_static/styles.css')"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] SkipNpmBuild validation misses build-type CSS artifacts

EnsureSkipNpmBuildArtifacts only checks _static/main.js and _static/styles.css, but _Head.cshtml also requires one of assembler.css, isolated.css, or codex.css based on BuildType. With SkipNpmBuild=true, a partial _static/ can pass this check and later serve pages with missing stylesheet URLs (404), producing broken styling at runtime.

Please validate the required build-type stylesheet(s) in this target as well, not just main.js and styles.css.

Comment thread .github/actions/bootstrap/action.yml Outdated
- uses: actions/cache@v6
with:
path: src/Elastic.Documentation.Site/node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('src/Elastic.Documentation.Site/package-lock.json') }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LOW] node_modules cache key should include Node runtime version

This cache key is based on OS + lockfile hash only. If .nvmrc changes while package-lock.json does not, this key still restores an older node_modules tree built against a different Node version, which can cause native-module ABI mismatches and flaky CI behavior.

Consider adding the Node version (for example via .nvmrc hash) to the key so runtime upgrades invalidate this cache.

@Mpdreamz
Mpdreamz force-pushed the fix/slow-docs-preview branch from 388c2ec to 4bb76be Compare September 24, 2026 11:23

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes due to a verified incremental-build regression in the frontend MSBuild target wiring.


What is this? | From workflow: PR Review

Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.

<OldHashedBundles Include="_static/*.????????*.css" />
</ItemGroup>
<Delete Files="@(OldHashedFiles)" ContinueOnError="true" />
<Delete Files="@(GeneratedStaticFiles);@(ViteGeneratedFiles);@(OldHashedBundles)" ContinueOnError="true" />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Cleaning build outputs before NpmRunBuild defeats incremental builds

NpmRunBuild declares incremental outputs (_static/.build-stamp;_static/main.js;_static/styles.css), but CleanGeneratedStaticAssets runs BeforeTargets="NpmRunBuild" and deletes those generated files first. That makes the target out-of-date on every invocation, so npm run build runs every time even when inputs are unchanged.

I reproduced this by running dotnet msbuild ... -t:NpmRunBuild twice in a row with no source changes; both runs executed Vite.

Please avoid deleting declared outputs in the pre-target cleanup path (for example, clean only stale hashed/chunked artifacts), so no-op builds can skip the frontend rebuild as intended.

@Mpdreamz
Mpdreamz force-pushed the fix/slow-docs-preview branch from 4bb76be to 6d364ff Compare September 24, 2026 12:28

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes based on the inline finding.


What is this? | From workflow: PR Review

Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.

<Target Name="EnsureSkipNpmBuildArtifacts"
BeforeTargets="NpmInstall;NpmRunBuild"
Condition="'$(SkipNpmBuild)' == 'true'">
<Error Condition="!Exists('_static/main.js') Or !Exists('_static/styles.css') Or !Exists('_static/assembler.css') Or !Exists('_static/isolated.css') Or !Exists('_static/codex.css')"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] SkipNpmBuild check still allows incomplete Vite output

EnsureSkipNpmBuildArtifacts verifies only five top-level files, but Vite runtime also needs generated chunk-*/asset-* files referenced by main.js. In SkipNpmBuild=true builds, a partially-populated _static/ directory can pass this check and produce runtime 404s when the browser requests missing chunks/assets.

Please also validate bundle completeness for the skip path (for example by checking for required generated chunk/asset outputs or validating a manifest from the last full build).

@Mpdreamz
Mpdreamz force-pushed the fix/slow-docs-preview branch from 6d364ff to 48f8d80 Compare September 24, 2026 14:09
Mpdreamz and others added 3 commits September 24, 2026 16:14
The `Docs preview (local)` workflow takes ~6m32s; two steps dominate.

#### Fix NUGET_PACKAGES so the cache is actually used

The bootstrap action caches `.nuget/packages` but never sets the
`NUGET_PACKAGES` env var, so `dotnet` ignores the cache and does a full
restore from nuget.org on every run. Adding the var to `bootstrap/action.yml`
fixes this for every consumer at once: `docs-preview-local.yml`,
`assembler-preview.yml`, and external repos that pin to this action.

#### Separate build from validation

Five consecutive `dotnet run` calls each pay ~8 s of MSBuild evaluation,
with the first also absorbing restore and build. An explicit
`dotnet build -c release src/tooling/docs-builder` step runs once before
any `dotnet run` call. All `dotnet run` calls become
`dotnet run --no-build -c release` so subsequent steps share the build
artifact instead of re-evaluating.

The release config also drops the Debug-only `Elastic.Documentation.Api`
project reference and skips the `curb` MSBuild rewrite that runs across
~25 projects in Debug mode.

#### Upload pre-compressed objects to preview S3

`aws s3 sync` ships raw bytes; the 4,029 API operation pages average ~420 KB
each, driving the output tree to ~2 GiB. CloudFront compresses on delivery,
but the upload cost is paid in full on every run.

The upload step now gzips the entire output tree in parallel before syncing,
then passes `--content-encoding gzip` so every object carries the header.
CloudFront forwards pre-compressed objects untouched, preserving brotli
negotiation for clients that ask for it — the flag only disables CloudFront's
own compression pass. Expected upload: ~120 MB instead of ~2 GiB.

One pass with `--content-encoding gzip` is intentional: the flag applies
sync-wide, so splitting into text vs binary would need two passes and would
break `--delete` (each pass deletes keys the other pass owns).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The in-place gzip pass compressed redirects.json alongside all other
output files. The subsequent Deploy redirects step read it as plain
JSON and got gzip bytes instead, breaking the redirects update.

Copy redirects.json to /tmp before compression and point the deploy
step at the uncompressed copy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…index upload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Mpdreamz
Mpdreamz force-pushed the fix/slow-docs-preview branch from 48f8d80 to cc855c2 Compare September 24, 2026 14:14

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes due to a verified incremental-build regression in frontend asset cleanup.


What is this? | From workflow: PR Review

Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.

causing duplicated JS/CSS with different hashes in the final _static folder. -->
<Target Name="CleanOldHashedAssets" BeforeTargets="NpmRunBuild" Condition="'$(IsPublishing)' == 'true'">
<Target Name="CleanGeneratedStaticAssets"
BeforeTargets="NpmRunBuild"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Cleanup runs even when frontend build is skipped, leaving broken chunk references

CleanGeneratedStaticAssets runs BeforeTargets="NpmRunBuild" and deletes chunk-*/asset-* files regardless of whether NpmRunBuild actually executes. Because NpmRunBuild can be skipped by incremental checks, this can leave _static/main.js referencing chunk files that were just deleted.

I reproduced this on the current branch by invoking NpmRunBuild via MSBuild and then checking _static/main.js: it contains 45 ./chunk-* imports, and all 45 referenced files were missing on disk after the target graph ran.

Please make cleanup conditional on the build actually running (for example, move cleanup into NpmRunBuild before the Exec), so no-op builds don't invalidate an otherwise consistent _static output set.

Vite eliminates the Windows TEMP workaround Parcel required and cuts
cold npm ci cost by caching node_modules in the bootstrap action.
SkipNpmBuild and DesignTimeBuild MSBuild hooks prevent redundant npm
runs in IDE and --no-build scenarios. IgnoreStandardErrorWarningFormat
on the npm run build exec prevents MSBuild from misclassifying Vite
chunk names (EuiIconWarning, EuiIconError) as build diagnostics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mpdreamz and others added 2 commits September 29, 2026 12:07
…in SkipNpmBuild guard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes main.js, styles.css, assembler.css, isolated.css, codex.css and their
map files from the pre-build clean target. Deleting declared NpmRunBuild outputs
before the incremental output check defeats MSBuild's up-to-date detection, so
npm run build ran on every dotnet build even when no source files changed.

The clean target now only removes content-hashed chunk-* and asset-* files from
prior Vite runs, which do need cleaning to avoid accumulation across builds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Mpdreamz
Mpdreamz force-pushed the fix/slow-docs-preview branch from cc855c2 to 520f233 Compare September 29, 2026 10:07
@Mpdreamz

Copy link
Copy Markdown
Member Author

Closed to retarget as a standalone PR stacked on #4206. Content preserved in new PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants