From 3f7101b972077a8812ed85621cccde038a503968 Mon Sep 17 00:00:00 2001 From: Leo Kettmeir Date: Thu, 27 Aug 2026 10:04:38 +0000 Subject: [PATCH 1/3] fix: render the package page for non-latest versions instead of redirecting (#1538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since #1473, docs are only served for the latest version of a package. The package overview route also requests docs, so visiting `/@scope/pkg@version` for any non-latest version got `docsOnlyForLatestVersion` from the API and 302'd to `/@scope/pkg/doc/all_symbols` — making version pages unreachable from the Versions tab. Now only the actual doc routes (entrypoint/symbol/all_symbols) redirect to the latest version's docs; the package overview page fetches the version's metadata and renders the header and nav for that version, with a note that documentation is only available for the latest version. This also fixes the `TypeError: b.subitems is not iterable` crash in the local symbol search island reported in the issue: since #1518, module docs can contain an `example` section, but the island cast every section to `namespace_section` and iterated `symbol.subitems`. Sections other than `namespace_section` are now skipped when building the search index and when filtering results. Fixes #1537 --- .../package/(_islands)/LocalSymbolSearch.tsx | 57 +++++++++---------- frontend/routes/package/index.tsx | 13 +++++ frontend/utils/data.ts | 37 +++++++++--- 3 files changed, 68 insertions(+), 39 deletions(-) diff --git a/frontend/routes/package/(_islands)/LocalSymbolSearch.tsx b/frontend/routes/package/(_islands)/LocalSymbolSearch.tsx index 4bc869385..10ef38a4f 100644 --- a/frontend/routes/package/(_islands)/LocalSymbolSearch.tsx +++ b/frontend/routes/package/(_islands)/LocalSymbolSearch.tsx @@ -14,11 +14,7 @@ import { import { Highlight, type Position } from "@orama/highlight"; import { api, path } from "../../../utils/api.ts"; import { useMacLike } from "../../../utils/os.ts"; -import type { - AllSymbolsCtx, - AllSymbolsItemCtx, - SectionContentNamespaceSectionCtx, -} from "@deno/doc/html-types"; +import type { AllSymbolsCtx, AllSymbolsItemCtx } from "@deno/doc/html-types"; import { renderToString } from "preact-render-to-string"; import { AllSymbols } from "../../../components/doc/AllSymbols.tsx"; @@ -113,11 +109,8 @@ export function LocalSymbolSearch( for (const entrypoint of searchContent.value!.entrypoints) { for (const kindGroup of entrypoint.module_doc.sections.sections) { - for ( - const symbol - of (kindGroup.content as SectionContentNamespaceSectionCtx) - .content - ) { + if (kindGroup.content.kind !== "namespace_section") continue; + for (const symbol of kindGroup.content.content) { searchItems.push({ name: symbol.name, symbolName: symbol.name, @@ -202,32 +195,34 @@ export function LocalSymbolSearch( .map((entrypoint) => { const filteredSections = entrypoint.module_doc.sections.sections .map((kindGroup) => { - const filteredContent = - (kindGroup.content as SectionContentNamespaceSectionCtx).content - .map((symbol) => { - const symbolMatches = hitNames.has(symbol.name); - const matchingSubitems = symbol.subitems.filter((subitem) => - hitNames.has(subitem.title) - ); - - if (!symbolMatches && matchingSubitems.length === 0) { - return null; - } - - return { - ...symbol, - subitems: symbolMatches - ? symbol.subitems - : matchingSubitems, - }; - }) - .filter(Boolean); + const content = kindGroup.content; + if (content.kind !== "namespace_section") return null; + + const filteredContent = content.content + .map((symbol) => { + const symbolMatches = hitNames.has(symbol.name); + const matchingSubitems = symbol.subitems.filter((subitem) => + hitNames.has(subitem.title) + ); + + if (!symbolMatches && matchingSubitems.length === 0) { + return null; + } + + return { + ...symbol, + subitems: symbolMatches + ? symbol.subitems + : matchingSubitems, + }; + }) + .filter(Boolean); if (filteredContent.length === 0) return null; return { ...kindGroup, - content: { ...kindGroup.content, content: filteredContent }, + content: { ...content, content: filteredContent }, }; }) .filter(Boolean); diff --git a/frontend/routes/package/index.tsx b/frontend/routes/package/index.tsx index 5ffefec6b..31fa568fe 100644 --- a/frontend/routes/package/index.tsx +++ b/frontend/routes/package/index.tsx @@ -44,6 +44,19 @@ export default define.page(function PackagePage( showProvenanceBadge /> ) + : data.selectedVersion + ? ( +
+ Documentation is only available for the{" "} + + latest version + {" "} + of a package. +
+ ) : (
This package has not published{" "} diff --git a/frontend/utils/data.ts b/frontend/utils/data.ts index 8286bdca5..4ac66d74e 100644 --- a/frontend/utils/data.ts +++ b/frontend/utils/data.ts @@ -127,14 +127,35 @@ export async function packageDataWithDocs( if (pkgDocsResp.code === "scopeNotFound") return null; if (pkgDocsResp.code === "packageNotFound") return null; if (pkgDocsResp.code === "docsOnlyForLatestVersion") { - // Docs are only served for the latest version; redirect to the - // canonical (versionless) docs URL for the latest version. - return new Response(null, { - status: 302, - headers: { - Location: `/@${scope}/${pkg}/doc${compileDocsRequestPath(docs)}`, - }, - }); + if ("entrypoint" in docs || "all_symbols" in docs) { + // Docs are only served for the latest version; redirect to the + // canonical (versionless) docs URL for the latest version. + return new Response(null, { + status: 302, + headers: { + Location: `/@${scope}/${pkg}/doc${compileDocsRequestPath(docs)}`, + }, + }); + } + + // The package overview page of a non-latest version: render it + // without docs instead of redirecting away from the version. + const pkgVersionResp = await state.api.get( + path`/scopes/${scope}/packages/${pkg}/versions/${version!}`, + ); + if (!pkgVersionResp.ok) { + if (pkgVersionResp.code === "packageVersionNotFound") return null; + if (pkgVersionResp.code === "scopeNotFound") return null; + if (pkgVersionResp.code === "packageNotFound") return null; + assertOk(pkgVersionResp); + } + return { + ...data, + kind: "content", + selectedVersion: pkgVersionResp.data, + selectedVersionIsLatestUnyanked: false, + docs: null, + }; } if (pkgDocsResp.code === "entrypointOrSymbolNotFound") { // redirect to all symbols page if there is no default entrypoint From 12b01b02795f78adb32c66a0bab4e9309658e7c9 Mon Sep 17 00:00:00 2001 From: Leo Kettmeir Date: Thu, 27 Aug 2026 10:51:59 +0000 Subject: [PATCH 2/3] ci: use a registry cache for the api docker build (#1539) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `docker-images` job takes ~12 minutes on every merge queue run because its Docker layer cache never hits: GHA caches are branch-scoped, and `merge_group` runs execute on transient `gh-readonly-queue/...` refs, so the cache written there is deleted along with the ref and is never readable by later runs. The only runs that could write a cache readable by everyone (pushes to `main`) skip the build entirely due to the image-reuse optimization — there are currently zero caches on `refs/heads/main`. As a result every merge queue run pays ~7:20 compiling all dependencies from scratch, plus ~2:30 exporting a ~1.4GB GHA cache into a black hole. That garbage has also pushed the repo past the 10GB Actions cache quota (11GB currently), evicting the useful `rust-cache` entries for the `check`/`test` jobs. This switches the build cache to a registry cache stored as a `:buildcache` tag next to the images in Artifact Registry, which is shared across all refs. `image-manifest=true,oci-mediatypes=true` is required for Artifact Registry to accept the cache manifest. No new permissions are needed — the job already pushes image tags to the same registry path. Expected impact: after the first (still cold) run seeds the cache, `docker-images` should drop from ~12 min to ~3–4 min whenever `Cargo.lock`/`Cargo.toml` are unchanged, shortening the merge queue's critical path since `staging` waits on this job. --- .github/workflows/ci.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dca6801ae..94cabd2e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -213,8 +213,13 @@ jobs: file: api/Dockerfile push: true tags: ${{ env.API_IMAGE_ID_BASE }}:${{ github.sha }} - cache-from: type=gha,scope=docker-api - cache-to: type=gha,mode=max,scope=docker-api + # Use a registry cache (not type=gha): GHA caches are branch-scoped, + # and merge_group runs execute on transient refs, so a gha cache + # written there is never readable by later runs. A registry cache is + # shared across all refs. image-manifest/oci-mediatypes are required + # for Artifact Registry to accept the cache manifest. + cache-from: type=registry,ref=${{ env.API_IMAGE_ID_BASE }}:buildcache + cache-to: type=registry,ref=${{ env.API_IMAGE_ID_BASE }}:buildcache,mode=max,image-manifest=true,oci-mediatypes=true - name: Set api_image_id output id: api_image_id From bfa9d511b0442239944afb7518d816b45946ab96 Mon Sep 17 00:00:00 2001 From: Leo Kettmeir Date: Thu, 27 Aug 2026 11:26:42 +0000 Subject: [PATCH 3/3] chore: upgrade deno_graph to 0.111.0 and deno_doc to 0.207.0 (#1535) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #155 Fixes #615 Fixes #886 Fixes #1179 Fixes #1258 Fixes #1478 Upgrades deno_graph 0.110.2 → 0.111.0 and deno_doc 0.206.0 → 0.207.0, picking up the slow-types / d.ts generation fixes from denoland/deno_graph#667 and the docs-side type inference fix from denoland/deno_doc#852: - **#615**: object properties referencing functions get `typeof fn` in the generated d.ts instead of an untyped (implicit `any`) property. - **#886 / #1179**: `[Symbol.iterator]`, `[Symbol.dispose]` and other well-known symbol class members are no longer dropped from the generated d.ts. - **#1258**: spread elements in `as const` arrays are no longer silently dropped from the emitted tuple type (d.ts side, deno_graph) and no longer render as `any[]` on doc pages (docs side, deno_doc). Negative number literals in tuples are also fixed. - **#155**: `Object.freeze(... as const satisfies ...)` and `new`-expressions of non-generic module-level classes no longer error with missing-explicit-type. Note these fixes apply at publish time — already-published versions keep the d.ts/docs generated when they were published. Also: deno_graph 0.111.0 removed the `unstable_text_imports` build option (text imports are now always enabled in graph building), so the three `BuildOptions` sites that passed `false` just drop the field. deno_doc 0.206.0 → 0.207.0 touches only type inference (`src/ts_type.rs`) — no `.hbs` template or ctx struct changes, so no frontend JSX mirroring is needed. `cargo test npm::` (tarball specs) passes unchanged; clippy clean. --- Cargo.lock | 8 ++++---- api/Cargo.toml | 4 ++-- api/src/analysis.rs | 2 -- api/src/api/package.rs | 1 - 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bdfa1fb25..5e270443a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1056,9 +1056,9 @@ dependencies = [ [[package]] name = "deno_doc" -version = "0.206.0" +version = "0.207.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2af46876eb43b3334539f548c8c106f9ad86d330ca5661c04fcdb27fd8e463" +checksum = "3be16593d05e9f94267702f8c45adf0b5d335c584e5f0637238ba73f474282f8" dependencies = [ "anyhow", "cfg-if", @@ -1110,9 +1110,9 @@ dependencies = [ [[package]] name = "deno_graph" -version = "0.110.2" +version = "0.111.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f95ac3cf9d132772c24beabf61d76f6706b7d5a23cca6d3d3f4cf2d30a7ad796" +checksum = "74d39f55639b6bffe4e886d2ea33724606729d3ec9c8e05a48952a56c1cef961" dependencies = [ "async-trait", "boxed_error", diff --git a/api/Cargo.toml b/api/Cargo.toml index ea384b16f..6c488cd6c 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -95,10 +95,10 @@ flate2 = "1" thiserror = "2" async-tar = "0.4.2" async-compression = { version = "0.4", features = ["futures-io", "gzip"] } -deno_graph = "=0.110.2" +deno_graph = "=0.111.0" deno_ast = { version = "0.53.0", features = ["view"] } # sync with frontend/deno.json -deno_doc = { version = "=0.206.0", features = ["comrak"] } +deno_doc = { version = "=0.207.0", features = ["comrak"] } deno_error = "0.7.0" comrak = { version = "0.29.0", default-features = false } ammonia = "4.0.0" diff --git a/api/src/analysis.rs b/api/src/analysis.rs index 4350f0bd2..eadaf9974 100644 --- a/api/src/analysis.rs +++ b/api/src/analysis.rs @@ -164,7 +164,6 @@ async fn analyze_package_inner( skip_dynamic_deps: false, module_info_cacher: Default::default(), unstable_bytes_imports: false, - unstable_text_imports: false, jsr_metadata_store: None, unstable_css_imports: false, unstable_config_imports: false, @@ -710,7 +709,6 @@ async fn rebuild_npm_tarball_inner( skip_dynamic_deps: false, module_info_cacher: Default::default(), unstable_bytes_imports: false, - unstable_text_imports: false, jsr_metadata_store: None, unstable_css_imports: false, unstable_config_imports: false, diff --git a/api/src/api/package.rs b/api/src/api/package.rs index 11a025496..a4209841d 100644 --- a/api/src/api/package.rs +++ b/api/src/api/package.rs @@ -2624,7 +2624,6 @@ async fn analyze_deps_tree( skip_dynamic_deps: false, module_info_cacher: Default::default(), unstable_bytes_imports: false, - unstable_text_imports: false, jsr_metadata_store: None, unstable_css_imports: false,