feat: Auto-updating Studios now load CSS bundles from the module server alongside JS.#893
feat: Auto-updating Studios now load CSS bundles from the module server alongside JS.#893pedrobonamin merged 19 commits intomainfrom
Conversation
📦 Bundle Stats —
|
| 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.
93763d5 to
a932b0f
Compare
Coverage Delta
Comparing 10 changed files against main @ Overall Coverage
|
There was a problem hiding this comment.
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
autoUpdatesCssUrlsthrough 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/visionlocal 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.
|
Claude finished @pedrobonamin's task in 2m 29s —— View job Review
1. Semver bug already fixedThe Cursor Bugbot flagged 2. Missing test: vision-not-installed dependency array
3. Changeset summary has a
|
| mockGetAppId.mockReturnValue('my-app-id') | ||
| const output = createMockOutput() | ||
|
|
||
| const {getLocalPackageVersion} = await import('../../../util/getLocalPackageVersion.js') |
There was a problem hiding this comment.
any reason to have dynamic imports vs static imports at the top?
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>
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.
011f1e3 to
0facc20
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.

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:
index.cssbundles and uploads them to GCS.cssextension support to the module serverBackground: 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, ingetAutoUpdatesImportMap.ts) — Generates CSS URLs using the samegetModuleUrl()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 acssFileproperty; packages without it are skipped.autoUpdatesCssUrlsis threaded throughbuildStudio.ts/buildApp.ts→buildStaticFiles.ts→getViteConfig.ts→plugin-sanity-build-entries.ts→renderDocument.ts→renderDocumentWorker.ts→getDocumentHtml.tsx→addTimestampedImportMapScriptToHtml.ts. Reset to[]when prerelease versions are unresolved (alongside the existingautoUpdatesImports = {}reset).addTimestampedImportMapScriptToHtml— CSS URLs are serialized into the existing#__importsJSON as acssarray (or omitted when empty). The existing inline<head>timestamp script is extended to also create<link rel="stylesheet">elements viadocument.createElementwith fresh timestamps applied via the sharedreplaceTimestamphelper, and append them todocument.headso they're discovered during head parsing.buildStudio.tsnow reads the installed@sanity/visionversion viagetLocalPackageVersion()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 oncssFile.addTimestampImportMapScriptToHtml.ts— It now does JS + CSS in one synchronous pass. Review that the sharedreplaceTimestampis applied to both, that thecss = []default handles the "no URLs" case cleanly, and that the link elements are appended todocument.head(so they're discovered by the browser immediately, not after the body loads).buildStudio.tsvision version detection —getLocalPackageVersion('@sanity/vision', workDir)pins vision's CSS URL to its actual installed version instead of the sanity version. Review the fallback (nocssFilewhen vision isn't installed).buildApp.ts—sanityis added to the App SDK auto-update set withcssFile: 'index.css'when it's present.@sanity/sdkand@sanity/sdk-reactintentionally don't havecssFile(they don't ship CSS).buildStudio.tsandbuildApp.tsresetautoUpdatesCssUrls = []alongside the existingautoUpdatesImports = {}reset when prerelease versions are unresolved.Testing
getAutoUpdatesImportMap.test.ts— 4 new tests forgetAutoUpdatesCssUrls: generates URLs for packages withcssFile, skips packages without, returns empty array when nothing hascssFile, uses by-app URL pattern withappId.addTimestampImportMapScriptToHtml.test.ts— 8 tests covering: no-op whenimportMapis undefined, import map JSON injection, importmap script tag injection,cssarray inclusion in#__imports,cssomission when no URLs, no static<link>tags emitted, runtime script creates link tags viacreateElementwithreplaceTimestamp, sharedreplaceTimestampfor 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
#__importsJSON; the existing timestamp injector script is extended to also create<link rel="stylesheet">tags at runtime with refreshed timestamps for Sanity CDN URLs.buildStudionow detects a locally installed@sanity/visionversion (when present) to avoid emitting CSS URLs that would 404, and tests are expanded (newjsdomdev 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.