Skip to content

fix(cli): stop global-theme stitching from breaking or dropping js - #17575

Closed
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1787949833-fix-theme-js-string
Closed

fix(cli): stop global-theme stitching from breaking or dropping js#17575
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1787949833-fix-theme-js-string

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Description

resolveThemeFileUrls mishandled js in two ways, both of which only surface at publish time — fern check passes either way.

  1. Presigned string dropped through — a theme declaring js: ./assets/x.js uploads the file to CAS and gets a presigned URL back as a bare string. Only { path } objects were downloaded, so the consumer joined the URL onto its own docs root:
.../<repo>/fern/https:/fdr-prod-content-addressable-storage.s3.../81e47976…  → ENOENT
  1. Themeless js clobbered the consumer's own js — the resolver unconditionally wrote cfg.js = [] when the theme declared no js. js has policy "global" in THEME_FIELD_POLICIES, and [] is not null, so the theme-wins branch of mergeThemeOverride replaced the repo's js: ./assets/header-scroll.js with []. Any repo with global-theme: silently lost every script declared in its own docs.yml; the registered docs config ended up js: {files: [], remote: []} and no <script> was emitted.

Changes Made

  • Download presigned plain-string js entries, matching the css string path.
  • Leave cfg.js untouched when the theme declares none, so the merge keeps the local value.
  • Remote entries ({ url }, non-presigned strings) are still passed through unchanged.

Testing

  • Unit tests added/updated — presigned string + { path } download, themeless js staying absent, and an end-to-end resolveThemeFileUrlsmergeThemeOverride case asserting a consumer keeps js: ./assets/header-scroll.js. The first and last fail on main with exactly the URL-as-path and the [] overwrite.
  • Manual testing completed — reproduced both failure modes against a live multi-repo site on 5.108.0.

Link to Devin session: https://app.devin.ai/sessions/5e37d55a6f354d3c9a3875a38f6ebf44
Open in Devin Desktop: https://app.devin.ai/desktop/session/5e37d55a6f354d3c9a3875a38f6ebf44?variant=devin


Devin Review

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@nitpickybot nitpickybot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AI Review Summary

Small, targeted fix: plain-string presigned js entries are now downloaded, and cfg.js is no longer clobbered with [] when the theme declares none. Logic looks correct; one minor nit about the idx++ counter being incremented inside a .map callback (pre-existing pattern, but the new string branch makes it slightly more subtle).

  • 🔵 2 suggestion(s)

To request another review, comment /ai-review on this pull request.

jsList.map(async (entry) => {
// "..." → shorthand for a local file, uploaded like a css string
if (typeof entry === "string") {
return isPresignedUrl(entry) ? downloadToTemp(entry, tmpDir, idx++) : entry;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 suggestion

idx++ is evaluated synchronously during .map, so ordering is deterministic — fine. Just note that if downloadToTemp uses idx only for uniqueness this is safe; if it's used for stable naming across runs, mixing string and {path} entries in one list still yields consistent indices. No change required, flagging so it stays intentional.

// the consuming repo resolves it as a path relative to its own docs directory.
it("downloads a presigned plain-string JS entry", async () => {
const result = await resolveThemeFileUrls({ js: PRESIGNED_JS }, tmpDir);
expect(result.js).toEqual([path.join(tmpDir, "header-scroll.js")]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 suggestion

The presigned tests assert an exact filename derived from response-content-disposition, but the stubbed headers.get returns null. Worth a quick check that downloadToTemp derives the name from the URL query rather than the response header — otherwise this assertion may be passing for the wrong reason (e.g. falling back to idx-based naming that happens to match).

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Docs Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-08-31T04:07:00Z).

Fixture main PR Delta
docs 261.5s (n=5) 235.2s (35 versions) -26.3s (-10.1%)

Docs generation runs fern generate --docs --preview end-to-end against the benchmark fixture with 35 API versions (each version: markdown processing + OpenAPI-to-IR + FDR upload).
Delta is computed against the nightly baseline on main.
Baseline from nightly run(s) on main (latest: 2026-08-31T04:07:00Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-08-31 13:05 UTC

@nitpickybot nitpickybot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AI Review Summary

Fix looks correct: plain-string presigned js entries are now downloaded, and cfg.js is left untouched when absent. One thing to verify is the idx++ counter semantics inside Promise.all map (pre-existing) and whether the normalization of a single string js into an array is safe downstream.

  • 🔵 1 suggestion(s)

To request another review, comment /ai-review on this pull request.

jsList.map(async (entry) => {
// "..." → shorthand for a local file, uploaded like a css string
if (typeof entry === "string") {
return isPresignedUrl(entry) ? downloadToTemp(entry, tmpDir, idx++) : entry;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 suggestion

idx++ is evaluated synchronously inside the map callback, so it's still deterministic here — but note the mapping now also normalizes a scalar js: "..." into a one-element array even when nothing was downloaded. Confirm every downstream consumer of cfg.js accepts the array form (the tests assert it does, so presumably fine).

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration devin-ai-integration Bot changed the title fix(cli): download global-theme js declared as a plain string fix(cli): stop global-theme stitching from breaking or dropping js Aug 31, 2026
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.

1 participant