Skip to content

feat(view): transitive modulepreload and css resolution for vite pipeline#2133

Merged
bpamiri merged 1 commit intodevelopfrom
peter/vite-pipeline-maturity
Apr 17, 2026
Merged

feat(view): transitive modulepreload and css resolution for vite pipeline#2133
bpamiri merged 1 commit intodevelopfrom
peter/vite-pipeline-maturity

Conversation

@bpamiri
Copy link
Copy Markdown
Collaborator

@bpamiri bpamiri commented Apr 17, 2026

Summary

Closes the asset-pipeline maturity gap called out in the Wheels 4.0 feature audit. Brings production-perf parity with Rails' and Laravel's Vite integrations.

New public API

  • vitePreloadTag(entrypoint, head=true) — emits <link rel="modulepreload"> for a Vite entrypoint and its transitive chunk imports. Useful for Turbo Drive hover-preload patterns (Turbo shipped with Wheels 4.0 via the Hotwire package).

Changed behavior

  • viteScriptTag() and viteStyleTag() now walk the manifest's imports tree transitively:
    • CSS from every transitive chunk is emitted as a <link rel="stylesheet"> (previously only the entry's own CSS).
    • <link rel="modulepreload"> is emitted into <head> for every transitive chunk, so the browser can start fetching shared code before the entry script parses.
  • viteStrictManifest setting (new, default true) — missing manifest entries now throw Wheels.ViteAssetNotFound in production regardless of showErrorInformation. Consistent with Wheels 4.0's deny-by-default security posture. Set false to restore 3.x silent behavior.

Internal refactor (Approach D, the "C lite")

Extracted a private $viteResolveAssets(entrypoint) resolver inside vendor/wheels/view/vite.cfc that walks the manifest once and returns {scripts, styles, preloads}. Existing helpers and the new vitePreloadTag() all become thin callers. Recursive walker uses a visited struct for cycle safety and diamond-dependency dedup. Also extracted $viteHtmlHead(text) as a testable wrapper around $htmlhead.

Test plan

  • Suite: 34 vite specs passing on Lucee 7 + SQLite (up from 18).
  • Full core test run: 3063 pass / 0 fail. The 20 remaining errors are unrelated to this change (Playwright JARs absent on local machine, expected).
  • Resolver correctness tested for: leaf entry, transitive three-level import tree, diamond dependency (dedup), cyclic graph (termination).
  • Strict-mode behavior covered: throws regardless of showErrorInformation; opt-out via set(viteStrictManifest=false) preserves 3.x silent path.
  • vitePreloadTag covered for dev mode, inline head=false, head=true via $viteHtmlHead capture buffer, and strict-miss throw.
  • Adobe CF 2025 spot-check attempted locally but the container has a pre-existing unrelated graphqlclient runtime issue; CI will exercise the full engine matrix.

Docs

🤖 Generated with Claude Code

…line

Closes the "asset-pipeline maturity" gap called out in the Wheels 4.0
feature audit. Adds:

- `$viteResolveAssets(entrypoint)` — private resolver that walks the
  Vite manifest once, following `imports` transitively with a visited
  set for dedup + cycle safety. Returns `{scripts, styles, preloads}`.
- `vitePreloadTag(entrypoint, head=true)` — new view helper that emits
  `<link rel="modulepreload">` for a Vite entrypoint and its transitive
  chunk imports. Useful for Turbo Drive hover-preload patterns in Wheels
  4.0 apps using the Hotwire package.
- `viteScriptTag()` and `viteStyleTag()` now use the resolver so that:
  - transitive chunk CSS is included in stylesheet output (previously
    only the entry's own CSS was emitted)
  - modulepreload links for each transitive import chunk are emitted
    into <head> via a new `$viteHtmlHead()` wrapper (brings production-
    performance parity with Rails/Laravel Vite integrations)
- `viteStrictManifest` setting (default `true`) — missing manifest
  entries now throw `Wheels.ViteAssetNotFound` in production regardless
  of `showErrorInformation`. Apps that need the 3.x silent behavior can
  `set(viteStrictManifest=false)`. Consistent with Wheels 4.0's
  deny-by-default security posture: deploy-time misconfigurations are
  loud instead of quiet 404s.
- `$viteHtmlHead()` — thin wrapper around `$htmlhead` that also writes
  to a request-scoped capture buffer when one is present, giving tests
  a way to assert what's been emitted to <head>. No production
  behavior change.

Tests extend viteSpec with:
- 7 new tests for `$viteResolveAssets` (leaf, CSS, transitive imports,
  diamond dedup, cycle termination, strict + silent miss paths)
- 2 new tests on `viteScriptTag` (transitive CSS, modulepreload via
  capture buffer)
- 1 new test on `viteStyleTag` (transitive CSS)
- 4 new tests for `vitePreloadTag` (dev mode, head=false inline, head=
  true via $viteHtmlHead, strict miss)
- 2 new strict-mode regression tests on `viteAsset`

Suite: 34 vite specs passing on Lucee 7 + SQLite (up from 18). Full core
suite: 3063 pass / 0 fail. The 20 remaining errors are unrelated to this
change (Playwright JARs absent on this machine).

Spec: docs/superpowers/specs/2026-04-16-vite-pipeline-maturity-design.md
Plan: docs/superpowers/plans/2026-04-16-vite-pipeline-maturity.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bpamiri bpamiri merged commit 0428bdf into develop Apr 17, 2026
5 checks passed
@bpamiri bpamiri deleted the peter/vite-pipeline-maturity branch April 17, 2026 04:31
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.

1 participant