Skip to content

feat: Auto-updating Studios now load CSS bundles from the module server alongside JS.#893

Merged
pedrobonamin merged 19 commits intomainfrom
feat/css-over-the-wire
Apr 28, 2026
Merged

feat: Auto-updating Studios now load CSS bundles from the module server alongside JS.#893
pedrobonamin merged 19 commits intomainfrom
feat/css-over-the-wire

Conversation

@pedrobonamin
Copy link
Copy Markdown
Contributor

@pedrobonamin pedrobonamin commented Apr 7, 2026

Description

Add CSS-over-the-wire support to the CLI build pipeline. When auto-updates are enabled, the CLI generates module-server CSS URLs alongside the existing JS import map URLs, and extends the existing <head> timestamp-injector script to also create <link rel="stylesheet"> tags synchronously at page load with a fresh timestamp applied — ensuring the module server resolves CSS to the same version as JS.

Counterparts:

Background: The sanity monorepo is migrating from styled-components to vanilla-extract, which produces static CSS files. These files need to be served from the module server (sanity-cdn.com) alongside JS bundles so auto-updated Studios get matching CSS.

What's in this PR:

  • getAutoUpdatesCssUrls (new, in getAutoUpdatesImportMap.ts) — Generates CSS URLs using the same getModuleUrl() helper as JS, so CSS and JS always resolve via the same module-server URL scheme (legacy and by-app variants). Packages opt in via a cssFile property; packages without it are skipped.
  • Pipeline threadingautoUpdatesCssUrls is threaded through buildStudio.ts / buildApp.tsbuildStaticFiles.tsgetViteConfig.tsplugin-sanity-build-entries.tsrenderDocument.tsrenderDocumentWorker.tsgetDocumentHtml.tsxaddTimestampedImportMapScriptToHtml.ts. Reset to [] when prerelease versions are unresolved (alongside the existing autoUpdatesImports = {} reset).
  • addTimestampedImportMapScriptToHtml — CSS URLs are serialized into the existing #__imports JSON as a css array (or omitted when empty). The existing inline <head> timestamp script is extended to also create <link rel="stylesheet"> elements via document.createElement with fresh timestamps applied via the shared replaceTimestamp helper, and append them to document.head so they're discovered during head parsing.
  • Studio: independent vision versionbuildStudio.ts now reads the installed @sanity/vision version via getLocalPackageVersion() and uses it for vision's CSS URL. If vision isn't installed, no vision CSS URL is generated (avoids 404s).

What to review

  • getAutoUpdatesCssUrls — The URL construction (legacy and by-app variants) and the filter on cssFile.
  • Inline runtime script in addTimestampImportMapScriptToHtml.ts — It now does JS + CSS in one synchronous pass. Review that the shared replaceTimestamp is applied to both, that the css = [] default handles the "no URLs" case cleanly, and that the link elements are appended to document.head (so they're discovered by the browser immediately, not after the body loads).
  • buildStudio.ts vision version detectiongetLocalPackageVersion('@sanity/vision', workDir) pins vision's CSS URL to its actual installed version instead of the sanity version. Review the fallback (no cssFile when vision isn't installed).
  • buildApp.tssanity is added to the App SDK auto-update set with cssFile: 'index.css' when it's present. @sanity/sdk and @sanity/sdk-react intentionally don't have cssFile (they don't ship CSS).
  • Prerelease handling — Both buildStudio.ts and buildApp.ts reset autoUpdatesCssUrls = [] alongside the existing autoUpdatesImports = {} reset when prerelease versions are unresolved.

Testing

  • getAutoUpdatesImportMap.test.ts — 4 new tests for getAutoUpdatesCssUrls: generates URLs for packages with cssFile, skips packages without, returns empty array when nothing has cssFile, uses by-app URL pattern with appId.
  • addTimestampImportMapScriptToHtml.test.ts — 8 tests covering: no-op when importMap is undefined, import map JSON injection, importmap script tag injection, css array inclusion in #__imports, css omission when no URLs, no static <link> tags emitted, runtime script creates link tags via createElement with replaceTimestamp, shared replaceTimestamp for both imports and CSS.

Notes for release

N/A – Part of the CSS-over-the-wire feature for auto-updating Studios. This PR enables the CLI side; the CSS bundles themselves are produced by sanity-io/sanity#12590 and served by sanity-io/module-server#140. No user-facing behavior changes until all three pieces land.


Note

Medium Risk
Changes the CLI build pipeline and HTML runtime injection for auto-updated Studios/apps by adding dynamic stylesheet loading, which could impact initial load/styling if URL generation or injection fails. Risk is moderated by added unit tests but touches multiple build/render layers.

Overview
Auto-updates now include CSS over the module server. When auto-updates are enabled, the CLI generates module-server CSS URLs (via new getAutoUpdatesCssUrls) alongside the JS import map, using the same legacy/by-app URL patterns.

These CSS URLs are threaded through the build pipeline into document rendering and embedded into the #__imports JSON; the existing timestamp injector script is extended to also create <link rel="stylesheet"> tags at runtime with refreshed timestamps for Sanity CDN URLs.

buildStudio now detects a locally installed @sanity/vision version (when present) to avoid emitting CSS URLs that would 404, and tests are expanded (new jsdom dev deps) to cover CSS URL generation and the runtime injection behavior.

Reviewed by Cursor Bugbot for commit b0e9a17. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 7, 2026

📦 Bundle Stats — @sanity/cli

Compared against main (cb1e19a5)

@sanity/cli

Metric Value vs main (cb1e19a)
Internal (raw) 2.1 KB -
Internal (gzip) 799 B -
Bundled (raw) 10.94 MB -
Bundled (gzip) 2.05 MB -
Import time 842ms +8ms, +0.9%

bin:sanity

Metric Value vs main (cb1e19a)
Internal (raw) 975 B -
Internal (gzip) 460 B -
Bundled (raw) 9.84 MB -
Bundled (gzip) 1.77 MB -
Import time 2.02s +16ms, +0.8%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — @sanity/cli-core

Compared against main (cb1e19a5)

Metric Value vs main (cb1e19a)
Internal (raw) 93.1 KB -
Internal (gzip) 21.9 KB -
Bundled (raw) 21.62 MB -
Bundled (gzip) 3.42 MB -
Import time 807ms +13ms, +1.7%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — create-sanity

Compared against main (cb1e19a5)

Metric Value vs main (cb1e19a)
Internal (raw) 976 B -
Internal (gzip) 507 B -
Bundled (raw) 50.7 KB -
Bundled (gzip) 12.6 KB -
Import time ❌ ChildProcess denied: node -
Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

@pedrobonamin pedrobonamin force-pushed the feat/css-over-the-wire branch 2 times, most recently from 93763d5 to a932b0f Compare April 21, 2026 07:24
@pedrobonamin pedrobonamin changed the title wip: support static css files for auto updating studios feat: support static css files for auto updating studios Apr 21, 2026
squiggler-app Bot added a commit that referenced this pull request Apr 21, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 21, 2026

Coverage Delta

File Statements
packages/@sanity/cli/src/actions/build/buildApp.ts 95.1% (+ 0.2%)
packages/@sanity/cli/src/actions/build/buildStaticFiles.ts 96.5% (±0%)
packages/@sanity/cli/src/actions/build/buildStudio.ts 96.5% (+ 0.2%)
packages/@sanity/cli/src/actions/build/getAutoUpdatesImportMap.ts 100.0% (±0%)
packages/@sanity/cli/src/actions/build/getViteConfig.ts 100.0% (±0%)
packages/@sanity/cli/src/actions/build/renderDocument.ts 38.5% (±0%)
packages/@sanity/cli/src/actions/build/renderDocumentWorker/addTimestampImportMapScriptToHtml.ts 100.0% (+ 35.3%)
packages/@sanity/cli/src/actions/build/renderDocumentWorker/getDocumentHtml.tsx 94.1% (±0%)
packages/@sanity/cli/src/actions/build/renderDocumentWorker/renderDocumentWorker.ts 100.0% (±0%)
packages/@sanity/cli/src/server/vite/plugin-sanity-build-entries.ts 86.4% (±0%)

Comparing 10 changed files against main @ cb1e19a5755dc28e70e5415eb46df9598b8d32a2

Overall Coverage

Metric Coverage
Statements 84.2% (+ 0.1%)
Branches 74.0% (+ 0.1%)
Functions 84.0% (+ 0.0%)
Lines 84.6% (+ 0.1%)

@pedrobonamin pedrobonamin marked this pull request as ready for review April 21, 2026 12:25
@pedrobonamin pedrobonamin requested a review from a team as a code owner April 21, 2026 12:25
@pedrobonamin pedrobonamin requested review from a team, Copilot and ryanbonial and removed request for a team April 21, 2026 12:25
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds “CSS-over-the-wire” support to the CLI build pipeline so auto-updated Studios can load static CSS bundles (eg index.css) from the module server with timestamps aligned to JS.

Changes:

  • Introduces getAutoUpdatesCssUrls() to generate module-server CSS URLs alongside the existing import map URLs.
  • Threads autoUpdatesCssUrls through the build pipeline and injects CSS URL metadata into #__imports, extending the runtime head script to create <link rel="stylesheet"> tags with fresh timestamps.
  • Updates Studio/App build logic (including @sanity/vision local version detection) and adds tests (incl. jsdom-based runtime verification).

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pnpm-lock.yaml Adds jsdom dependencies used by new tests.
packages/@sanity/cli/src/server/vite/plugin-sanity-build-entries.ts Passes autoUpdatesCssUrls into renderDocument() during build output generation.
packages/@sanity/cli/src/actions/build/renderDocumentWorker/renderDocumentWorker.ts Threads autoUpdatesCssUrls through the render worker into getDocumentHtml().
packages/@sanity/cli/src/actions/build/renderDocumentWorker/getDocumentHtml.tsx Plumbs autoUpdatesCssUrls into HTML post-processing (addTimestampedImportMapScriptToHtml).
packages/@sanity/cli/src/actions/build/renderDocumentWorker/addTimestampImportMapScriptToHtml.ts Injects CSS URL metadata into #__imports and extends runtime head script to create timestamped stylesheet links.
packages/@sanity/cli/src/actions/build/renderDocumentWorker/tests/addTimestampImportMapScriptToHtml.test.ts Adds unit + jsdom runtime tests validating importmap + CSS link injection behavior.
packages/@sanity/cli/src/actions/build/renderDocument.ts Extends render options type to include autoUpdatesCssUrls for worker threading.
packages/@sanity/cli/src/actions/build/getViteConfig.ts Plumbs autoUpdatesCssUrls into the Vite plugin pipeline.
packages/@sanity/cli/src/actions/build/getAutoUpdatesImportMap.ts Adds getAutoUpdatesCssUrls() that derives CSS URLs from the same module URL scheme as JS.
packages/@sanity/cli/src/actions/build/buildStudio.ts Generates CSS URLs (incl. @sanity/vision local version detection) and passes them into static build.
packages/@sanity/cli/src/actions/build/buildStaticFiles.ts Accepts and forwards autoUpdatesCssUrls into Vite config creation.
packages/@sanity/cli/src/actions/build/buildApp.ts Adds optional sanity CSS URL generation for app auto-updates and passes into static build.
packages/@sanity/cli/src/actions/build/tests/getAutoUpdatesImportMap.test.ts Adds tests for getAutoUpdatesCssUrls() (legacy + by-app URL patterns).
packages/@sanity/cli/src/actions/build/tests/buildStudio.appIdWarning.test.ts Updates mocks to include the new getAutoUpdatesCssUrls() export.
packages/@sanity/cli/src/actions/build/tests/buildApp.appIdWarning.test.ts Updates mocks to include the new getAutoUpdatesCssUrls() export.
packages/@sanity/cli/package.json Adds jsdom and @types/jsdom for the new runtime tests.
.changeset/pr-893.md Declares a minor bump for @sanity/cli due to new auto-update CSS support.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@sanity-io sanity-io deleted a comment from claude Bot Apr 21, 2026
@claude
Copy link
Copy Markdown

claude Bot commented Apr 21, 2026

Claude finished @pedrobonamin's task in 2m 29s —— View job


Review

  • Gather context
  • Read changed files
  • Identify issues
  • Post review

1. Semver bug already fixed

The Cursor Bugbot flagged semver.parse(installedVisionVersion) as a runtime crash. This was already fixed in commit b0e9a17 — the current code correctly calls semverParse(installedVisionVersion) on line 80 of buildStudio.ts. No action needed.


2. Missing test: vision-not-installed dependency array

buildStudio.appIdWarning.test.ts has a test for when @sanity/vision is installed locally (line 191), but no corresponding test asserting the dependency array structure when it is not installed. The default mock returns null, which is the not-installed path, but none of the existing tests inspect getAutoUpdatesImportMap.mock.calls[0][0] in that case to verify:

  • @sanity/vision has no cssFile property
  • @sanity/vision uses cleanSanityVersion as its version

Fix this →


3. Changeset summary has a feat: prefix

pr-893.md:

feat: Auto-updating Studios now load CSS bundles from the module server alongside JS.

Changeset summaries should be plain sentences, not conventional commit-prefixed. The feat: prefix appears in the CHANGELOG entry directly. Change to:

Auto-updating Studios now load CSS bundles from the module server alongside JS.

4. Type predicate missing in getAutoUpdatesCssUrls

getAutoUpdatesImportMap.ts, inside getAutoUpdatesCssUrls:

.filter((pkg) => Boolean(pkg.cssFile))
.map((pkg) => `${getModuleUrl(pkg, options)}/${pkg.cssFile}`)

Boolean(pkg.cssFile) does not narrow the type — TypeScript still treats pkg.cssFile as string | undefined in the .map(). This works at runtime because the filter guarantees cssFile is present, but TypeScript won't catch a future refactor that breaks this assumption. Use a type predicate:

.filter((pkg): pkg is Pkg & {cssFile: string} => Boolean(pkg.cssFile))

Fix this →


squiggler-app Bot added a commit that referenced this pull request Apr 21, 2026
@pedrobonamin pedrobonamin changed the title feat: support static css files for auto updating studios feat: Auto-updating Studios now load CSS bundles from the module server alongside JS. Apr 21, 2026
squiggler-app Bot added a commit that referenced this pull request Apr 21, 2026
bjoerge
bjoerge previously approved these changes Apr 22, 2026
Copy link
Copy Markdown
Member

@bjoerge bjoerge left a comment

Choose a reason for hiding this comment

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

LGTM, probably wise to have @binoy14's eyes on this too

@pedrobonamin pedrobonamin requested a review from binoy14 April 22, 2026 15:47
binoy14
binoy14 previously approved these changes Apr 24, 2026
Copy link
Copy Markdown
Contributor

@binoy14 binoy14 left a comment

Choose a reason for hiding this comment

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

Makes senes to me

mockGetAppId.mockReturnValue('my-app-id')
const output = createMockOutput()

const {getLocalPackageVersion} = await import('../../../util/getLocalPackageVersion.js')
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.

any reason to have dynamic imports vs static imports at the top?

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.

not really, updated. And after rebase I had to update also this

@pedrobonamin pedrobonamin dismissed stale reviews from binoy14 and bjoerge via 125d92e April 27, 2026 14:39
squiggler-app Bot added a commit that referenced this pull request Apr 27, 2026
luke added 4 commits April 27, 2026 16:51
Add a new exported function `getAutoUpdatesCssUrls()` that generates
module-server CSS URLs for auto-updated packages. It reuses the existing
`getModuleUrl()` function and appends the CSS filename, ensuring CSS
URLs follow the exact same pattern as JS import map URLs (same host,
same semver range, same timestamp).

Packages opt in by providing a `cssFile` property (e.g., 'index.css').
Packages without `cssFile` are skipped.

This ensures the module server resolves CSS and JS to the same version,
since both URLs share the same timestamp and version range.

Tests cover: legacy URLs, by-app URLs, filtering, and empty results.

Co-authored-by: luke <luke@miriad.systems>
Pass auto-update CSS URLs through the entire build chain:
buildStudio/buildApp → buildStaticFiles → getViteConfig →
sanityBuildEntries → renderDocument → renderDocumentWorker →
getDocumentHtml → addTimestampedImportMapScriptToHtml

CSS URLs are stored in the `#__imports` JSON alongside the import map:
```json
{"imports": {...}, "css": ["https://sanity-cdn.com/.../index.css"]}
```

Both buildStudio and buildApp generate CSS URLs for packages that
declare a `cssFile` property. CSS URLs are reset alongside import map
entries when prerelease versions are detected.

The local CSS produced by Vite during `sanity build` is preserved
as-is — module-server CSS is purely additive.

Tests added for addTimestampedImportMapScriptToHtml covering CSS URL
inclusion, empty arrays, and undefined handling.

Co-authored-by: luke <luke@miriad.systems>
Update the inline runtime script (TIMESTAMPED_IMPORTMAP_INJECTOR_SCRIPT)
to also handle CSS URLs from the `css` array in the `#__imports` JSON.

The script now:
1. Extracts `css` array alongside `imports` from the JSON
2. Uses a shared `replaceTimestamp()` function for both JS and CSS URLs
3. Creates `<link rel="stylesheet">` elements for each CSS URL
4. Appends them to `<head>` after the import map

The shared `replaceTimestamp` function ensures both JS import map URLs
and CSS URLs use the exact same timestamp, guaranteeing the module
server resolves both to the same package version.

Tests added verifying CSS link tag creation and shared timestamp
replacement logic.

Co-authored-by: luke <luke@miriad.systems>
1. Make @sanity/vision CSS conditional on installation — only inject
   CSS <link> tag when vision is actually installed, using its real
   version. Prevents spurious 404s when vision isn't a dependency.

2. Add try-catch around JSON.parse in the runtime injector script —
   malformed JSON now logs a console.warn and falls back to empty
   imports/css instead of breaking Studio load entirely.

3. Improve test coverage — the CSS link tag test now verifies CSS URLs
   are actually stored in the __imports JSON (not just that the script
   text contains certain strings). Added tests for JSON.parse error
   handling and css-not-leaking-into-importmap.

Co-authored-by: luke <luke@miriad.systems>
pedrobonamin and others added 14 commits April 27, 2026 16:51
Instead of creating CSS <link> elements at runtime via JavaScript,
emit static <link rel="stylesheet" data-auto-update-css> tags directly
in the HTML. The browser starts fetching CSS immediately on page load,
before any JavaScript executes.

The runtime script now finds existing link[data-auto-update-css] tags
and replaces their href with a fresh timestamp, rather than creating
new elements. This ensures:
1. No flash of unstyled content — CSS loads with the HTML
2. Timestamps still get refreshed — module server resolves to latest
3. In most cases the resolved version is the same, so the browser
   ends up at the same GCS file (cache hit)

Also includes lint auto-fixes (alphabetical property ordering).

Co-authored-by: luke <luke@miriad.systems>
Now that CSS is loaded via static <link data-auto-update-css> tags
in the HTML (updated by the runtime script via querySelectorAll),
there's no need to store CSS URLs in the #__imports JSON.

Removes:
- css array from the __imports JSON data
- var css = importsData.css || [] from the runtime script
- delete importMapData.css from the runtime script
- Related tests for css-in-JSON behavior

The runtime script now only reads imports from __imports (for the
import map) and updates existing <link> tags found in the DOM.

Co-authored-by: luke <luke@miriad.systems>
- Mock getLocalPackageVersion in buildStudio.appIdWarning.test.ts and add a test
  that covers the vision-installed path (verifies that the vision entry passed
  to getAutoUpdatesImportMap/getAutoUpdatesCssUrls has cssFile set and uses the
  installed vision version).
- Add tests for the HTML fallback branches in addTimestampImportMapScriptToHtml:
  no <html> wrapper and no <head>.
- Rewrite the changeset to describe the user-facing effect rather than the
  internal mechanism.
@pedrobonamin pedrobonamin force-pushed the feat/css-over-the-wire branch from 011f1e3 to 0facc20 Compare April 27, 2026 14:51
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0facc20. Configure here.

Comment thread packages/@sanity/cli/src/actions/build/buildStudio.ts Outdated
@pedrobonamin pedrobonamin merged commit 9fc5e0f into main Apr 28, 2026
54 checks passed
@pedrobonamin pedrobonamin deleted the feat/css-over-the-wire branch April 28, 2026 15:29
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.

4 participants