Skip to content

chore: merge dev into main - #169

Merged
TitusKirch merged 24 commits into
mainfrom
dev
Nov 13, 2026
Merged

TitusKirch merged 24 commits into
mainfrom
dev

Conversation

@github-actions

Copy link
Copy Markdown

Automated rollup PR for changes accumulated on dev. Mark ready when you want it to land — and use a merge commit, not squash, so individual feat:/fix: commits stay visible to release-please.

The section row compared every source against every other one on each
client navigation — cubic in the number of sources. duxt.app reached 110,
and sectionsForPath took ~69 ms per call behind Vue proxies. The build now
writes each source's area into resolvedSources, which content pack
resolves through the same module.
useDuxtConfig() was a computed of its own in each of the 34 components
that call it, and every one merged the config, appended the generated
sections and walked every text field again on each navigation. The Nuxt
app now holds one merge and a cache per locale and path; the manifest is
resolved once per locale, and a key whose value did not change keeps its
object, so its readers are not re-rendered.

Workers build of duxt.app: prerender 1156 s -> 777 s, warm client
navigation 454 ms -> 61 ms.
…ctions

Each client navigation downloaded the SQL dumps of all 15 docs versions
and the SQLite WebAssembly module, and kept 4.6 million characters of
them in localStorage. Two lookups did it:

- DuxtVersion and useDuxtVersion re-asked their per-path question for the
  page being left: the header sees a navigation a tick late, after Nuxt
  released that page's payload data. A browser lookup for a path that is
  not the router's destination now answers nothing, and the redundant
  watch on the path, which refetched past the payload, is gone.
- The section-layout middleware queried the collection for the page type
  on every navigation. The server now writes the layout into the payload
  and the client reads it from there, querying only without a payload.

Workers build of duxt.app: first client navigation 293 ms -> 85-120 ms,
no dump or WebAssembly request, localStorage 4.6M -> 306 characters.
Nitro copies the runtime config for every request useRuntimeConfig(event)
is called on, and @nuxtjs/i18n calls it on every request. For each key it
derives an environment variable name character by character and reads
process.env twice. The layer's redirect tables were 713 of duxt.app's
2,043 keys, most of them whole URL paths, and they are build output no
environment variable may override. They now come from a virtual module,
#duxt/redirects, which the content-bundles plugin and the devtools panel
import.

Workers build of duxt.app: prerender 757 s -> 708 s.
Content's parse cache keeps a row for every file it ever parsed: a build
never removes one, only the dev watcher does. readContentCache took them
all for live pages, so a moved or deleted page kept its redirectFrom
rules, was counted by duxt report and shipped in content packs. This
repository's own cache held 100 such rows.

Content rebuilds each collection table from the files that exist and
shares the cache's ids, so a cache row now counts only when that table
still holds it.
Every deploy parsed all 12,232 files from cold, three to six minutes
measured locally against about thirty seconds warm. The cache was left
out because a restored copy fed deleted pages to duxt's own readers,
which they no longer read. Restored with a rolling key, as the OG images
are, namespaced by the lockfile.
wikiMaintenance resolves every link of every page, and each resolution
filtered every row, normalised its path, title and aliases, and walked
the whole manifest for the locale chain again: pages x links x pages.
The rows are now indexed once per row list, the rows of a scope once per
chain, and a scope once per manifest, source and reader, all beside the
objects they describe. A test compares the index with the filter it
replaces, row for row and in order.

Node build of duxt.app: wanted pages 8.0 s -> 6.1 s each.
@TitusKirch
TitusKirch marked this pull request as ready for review November 12, 2026 11:36
node-forge 1.4.3 closes GHSA-6889-vf6x-f3g5, GHSA-w4xr-hqv8-ch44,
GHSA-6cv4-p8m7-4r25 and GHSA-j8rw-jp99-435v; fast-xml-parser 5.12.0
closes GHSA-474x-hrwr-fgx4. Both fall inside their parents' ranges
(listhen ^1.4.0, sitemapd ^5.11.2) and are past the release cooldown,
so the lockfile moves and nothing needs an override or a review.
@rollup/plugin-inject 5.0.5, the latest release and Nitro's, returns a
high-resolution sourcemap for every module its prefilter matches, even
when it injects nothing. One Node build of apps/www mapped 1,248
unchanged modules, 89 million characters, the 24.6 MB Content dump twice
among them; that was the heap spike in Nitro's final server bundle.
Returning no transform when nothing was injected removes it and takes
about 1.2 minutes off the build. Workspace patch only.
The CI gate's Node build died at the default 4 GB, before this branch's
changes too. Even without the plugin-inject maps, prerendering peaks at
about 6 GB, most likely in Content's dump routes. GitHub's runners have
16 GB; an existing NODE_OPTIONS is kept and extended.
nuxt 4.6.4, vue 3.5.45, vitest 5.1.0, playwright-core 1.65.0, wrangler
4.158.0, shiki 4.6.0, mermaid 12.3.0, @lucide/vue 1.70.0, tailwindcss
4.3.5 and the lucide and vscode-icons sets, each past the release
cooldown; newer releases still inside it stay held. oxlint moves from
1.90.0 to 1.93.0 and stays pinned.

Three follow-ups keep the gate green:

- @nuxt/cli 4.0.1 could not start `nuxt dev` against nuxt 4.6.4's
  devServer host; 4.0.3, inside nuxt's own range, can.
- vue resolves to 3.5.45 everywhere. Two copies left NuxtLink without
  a Nuxt instance during the prerender (NUXT_E1001).
- oxlint 1.93 rejects the raw escape byte in the CLI tests' colour
  stripper; node:util's stripVTControlCharacters replaces it.

nuxt 4.6.4 moves archiver's readdir-glob to minimatch 10.2.6, so the
review of GHSA-g3hv-75wg-m249 gains those paths. It is the caller the
review already names, matching build-configuration patterns only.
.nvmrc, the content-bundles example image and the agent files name the
same patch release. CI keeps selecting the newest Node 26.
jsdom is the test DOM of the layer and the site, agents the Cloudflare
MCP provider of apps/www, oxfmt stays pinned and reformats nothing, and
wrangler 4.159 has passed the release cooldown since the minor update.
TypeScript stays on 6 until Volar supports 7.
check:wiki and check:wikilinks rename a fixture page and assert that the
next content pack parses it. Content keeps a parse-cache row for every
file it ever parsed, so a row for that path from an earlier run - on a
developer machine, or in a CI run that restored part of .data - made the
pack skip the parse and the check fail. The renamed page now carries a
per-run comment, so its checksum is always new.
… root

DuxtVersion and useDuxtVersion skip asking whether a page exists in
other versions when the page is its source's root, which every version
has. The unprefixed source's root is /, not its empty prefix, so the
landing page asked anyway: on a cold server it restored every version's
collection before its first byte. Measured on duxt.app: 16 collection
restores and 23 s down to 2 and 2.8 s.
build:cf already pins NUXT_PUBLIC_I18N_BASE_URL and NUXT_SITE_URL so a
local .env cannot decide the origin of prerendered pages. The Node build
did not, so a developer's .env leaked localhost into the prerendered
wanted pages and check:seo failed locally while CI passed.
Content restores a dump one INSERT at a time, each its own commit, so on
a disk every row waits for an fsync. A cold docs page restores every
version's collection and took over 20 s on disk against under 2 in
memory, which pushed check:seo and check:interactive past their 30 s
budget for a first response.
Content answers each server-side queryCollection() through an internal
request, and @nuxtjs/i18n initialises its context on every request with
useRuntimeConfig(event), which clones the whole runtime config and maps
every key to an environment variable. A docs page made 26 such queries,
so 27 copies per page and about a third of its render time.

A Nitro plugin registered before i18n's hands /__nuxt_content requests
the config Nitro resolved once at startup. A request with a Cloudflare
context keeps Nitro's per-request behaviour.

Node build of duxt.app: 27 copies per page down to 1, warm render
515 ms -> 320 ms, prerender 90 s -> 66 s.
@hono/node-server 2.1.6 closes GHSA-qv5m-9pm6-hqp3 and
GHSA-jpvv-r467-5p4w. It reaches the layer through the MCP SDK, whose
range (^2.0.5) already allows it, and it is past the release cooldown,
so only the lockfile moves.
The wiki layer builds on core features that first ship in 0.13.0: build
contributions through duxt.build.ts, page types and the ./unstable export,
which 0.12.0 does not have. With >=0.4.0 a site could install it next to
an older core without a peer warning and only fail at build time. The
range stays wide rather than ^0.x, as for the other companion layers.

Refs #161
The test builds a whole Nuxt app (up to 120 s), prepares it (30 s) and
polls five times for up to 30 s each, inside a 180 s limit that was
smaller than the sum of its own steps. Under CI's parallel package tests
it timed out on PR #169 while every step was still inside its budget.
@TitusKirch
TitusKirch merged commit 336280e into main Nov 13, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant