Skip to content

Commit

Permalink
feat: add CLI options to enable auto-updating studios
Browse files Browse the repository at this point in the history
  • Loading branch information
ricokahler committed Apr 29, 2024
1 parent ea41632 commit 4ba95fd
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 34 deletions.
2 changes: 2 additions & 0 deletions packages/@sanity/cli/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ export interface CliConfig {
graphql?: GraphQLAPIConfig[]

vite?: UserViteConfig

enableAutoUpdates?: boolean
}

export type UserViteConfig =
Expand Down
1 change: 1 addition & 0 deletions packages/sanity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@
"module-alias": "^2.2.2",
"nano-pubsub": "^3.0.0",
"nanoid": "^3.1.30",
"node-html-parser": "^6.1.13",
"observable-callback": "^1.0.1",
"oneline": "^1.0.3",
"open": "^8.4.0",
Expand Down
36 changes: 36 additions & 0 deletions packages/sanity/src/_internal/cli/actions/build/buildAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import path from 'node:path'
import {promisify} from 'node:util'

import chalk from 'chalk'
import {info} from 'log-symbols'
import {noopLogger} from '@sanity/telemetry'
import rimrafCallback from 'rimraf'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand All @@ -16,12 +17,38 @@ import {BuildTrace} from './build.telemetry'

const rimraf = promisify(rimrafCallback)

// TODO: replace this with a manifest somewhere
const AUTO_UPDATES_IMPORTMAP = {
imports: {
'react': 'https://studio-bundles.sanity.io/modules/v1/react/18.3.0/bare/react.mjs',
'react-dom': 'https://studio-bundles.sanity.io/modules/v1/react-dom/18.3.0/bare/react-dom.mjs',
'react-dom/server':
'https://studio-bundles.sanity.io/modules/v1/react-dom/18.3.0/bare/react-dom_server.mjs',
'react-dom/client':
'https://studio-bundles.sanity.io/modules/v1/react-dom/18.3.0/bare/react-dom.mjs',
'react/jsx-runtime':
'https://studio-bundles.sanity.io/modules/v1/react/18.3.0/bare/react_jsx-runtime.mjs',
'sanity': 'https://studio-bundles.sanity.io/modules/v1/sanity/3.39.0/bare/sanity.mjs',
'sanity/presentation':
'https://studio-bundles.sanity.io/modules/v1/sanity/3.39.0/bare/presentation.mjs',
'sanity/desk': 'https://studio-bundles.sanity.io/modules/v1/sanity/3.39.0/bare/desk.mjs',
'sanity/router': 'https://studio-bundles.sanity.io/modules/v1/sanity/3.39.0/bare/router.mjs',
'sanity/_singletons':
'https://studio-bundles.sanity.io/modules/v1/sanity/3.39.0/bare/_singletons.mjs',
'sanity/structure':
'https://studio-bundles.sanity.io/modules/v1/sanity/3.39.0/bare/structure.mjs',
'styled-components':
'https://studio-bundles.sanity.io/modules/v1/styled-components/6.1.8/bare/styled-components.mjs',
},
}

export interface BuildSanityStudioCommandFlags {
'yes'?: boolean
'y'?: boolean
'minify'?: boolean
'stats'?: boolean
'source-maps'?: boolean
'enable-auto-updates'?: boolean
}

export default async function buildSanityStudio(
Expand Down Expand Up @@ -50,6 +77,14 @@ export default async function buildSanityStudio(
return {didCompile: false}
}

const enableAutoUpdates =
flags['enable-auto-updates'] ||
(cliConfig && 'enableAutoUpdates' in cliConfig && cliConfig.enableAutoUpdates)

if (enableAutoUpdates) {
output.print(`${info} Building with auto-updates enabled`)
}

const envVarKeys = getSanityEnvVars()
if (envVarKeys.length > 0) {
output.print(
Expand Down Expand Up @@ -115,6 +150,7 @@ export default async function buildSanityStudio(
sourceMap: Boolean(flags['source-maps']),
minify: Boolean(flags.minify),
vite: cliConfig && 'vite' in cliConfig ? cliConfig.vite : undefined,
importMap: enableAutoUpdates ? AUTO_UPDATES_IMPORTMAP : undefined,
})
trace.log({
outputSize: bundle.chunks
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {describe, expect, it} from '@jest/globals'
import {renderToStaticMarkup} from 'react-dom/server'

import {_prefixUrlWithBasePath} from '../renderDocument'
import {_prefixUrlWithBasePath, addImportMapToHtml} from '../renderDocument'

describe('_prefixUrlWithBasePath', () => {
describe('when basePath is default value of "/"', () => {
Expand Down Expand Up @@ -67,3 +68,46 @@ describe('_prefixUrlWithBasePath', () => {
})
})
})

describe('addImportMapToHtml', () => {
const importMap = {
imports: {
react: 'https://example.com/react',
},
}

it('takes in an existing HTML document and adds the given import map to the end of the head of the document', () => {
const input = renderToStaticMarkup(
<html lang="en">
<head>
<meta charSet="utf-8" />
<title>Sanity Studio</title>
</head>
<body>
<div id="sanity" />
</body>
</html>,
)
const output = addImportMapToHtml(input, importMap)

expect(output).toBe(
'<html lang="en"><head><meta charSet="utf-8"><title>Sanity Studio</title><script type="importmap">{"imports":{"react":"https://example.com/react"}}</script></head><body><div id="sanity"></div></body></html>',
)
})

it('creates an <html> element if none exist', () => {
const input = 'foo<div>bar</div>baz'
const output =
'<html><head><script type="importmap">{"imports":{"react":"https://example.com/react"}}</script></head>foo<div>bar</div>baz</html>'

expect(addImportMapToHtml(input, importMap)).toBe(output)
})

it('creates a <head> to the document if one does not exist', () => {
const input = '<html><body><script src="index.js"></script></body></html>'
const output =
'<html><head><script type="importmap">{"imports":{"react":"https://example.com/react"}}</script></head><body><script src="index.js"></script></body></html>'

expect(addImportMapToHtml(input, importMap)).toBe(output)
})
})
3 changes: 3 additions & 0 deletions packages/sanity/src/_internal/cli/server/buildStaticFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface StaticBuildOptions {
minify?: boolean
profile?: boolean
sourceMap?: boolean
importMap?: {imports?: Record<string, string>}

vite?: UserViteConfig
}
Expand All @@ -45,6 +46,7 @@ export async function buildStaticFiles(
minify = true,
basePath,
vite: extendViteConfig,
importMap,
} = options

debug('Writing Sanity runtime files')
Expand All @@ -59,6 +61,7 @@ export async function buildStaticFiles(
minify,
sourceMap,
mode,
importMap,
})

// Extend Vite configuration with user-provided config
Expand Down
23 changes: 21 additions & 2 deletions packages/sanity/src/_internal/cli/server/getViteConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from 'node:path'
import {type UserViteConfig} from '@sanity/cli'
import viteReact from '@vitejs/plugin-react'
import debug from 'debug'
import {escapeRegExp} from 'lodash'
import readPkgUp from 'read-pkg-up'
import {type ConfigEnv, type InlineConfig, mergeConfig} from 'vite'

Expand Down Expand Up @@ -51,6 +52,8 @@ export interface ViteOptions {
* Mode to run vite in - eg development or production
*/
mode: 'development' | 'production'

importMap?: {imports?: Record<string, string>}
}

/**
Expand All @@ -68,6 +71,7 @@ export async function getViteConfig(options: ViteOptions): Promise<InlineConfig>
server,
minify,
basePath: rawBasePath = '/',
importMap,
} = options

const monorepo = await loadSanityMonorepo(cwd)
Expand Down Expand Up @@ -104,7 +108,7 @@ export async function getViteConfig(options: ViteOptions): Promise<InlineConfig>
sanityFaviconsPlugin({defaultFaviconsPath, customFaviconsPath, staticUrlPath: staticPath}),
sanityDotWorkaroundPlugin(),
sanityRuntimeRewritePlugin(),
sanityBuildEntries({basePath, cwd, monorepo}),
sanityBuildEntries({basePath, cwd, monorepo, importMap}),
],
envPrefix: 'SANITY_STUDIO_',
logLevel: mode === 'production' ? 'silent' : 'info',
Expand All @@ -128,7 +132,7 @@ export async function getViteConfig(options: ViteOptions): Promise<InlineConfig>
emptyOutDir: false, // Rely on CLI to do this

rollupOptions: {
external: [/^sanity(\/.*)?$/, 'react', 'react/jsx-runtime', 'styled-components'],
external: createExternalFromImportMap(importMap),
input: {
sanity: path.join(cwd, '.sanity', 'runtime', 'app.js'),
},
Expand All @@ -139,6 +143,21 @@ export async function getViteConfig(options: ViteOptions): Promise<InlineConfig>
return viteConfig
}

/**
* Generates a Rollup `external` configuration array based on the provided
* import map. We derive externals from the import map because this ensures that
* modules listed in the import map are not bundled into the Rollup output so
* the browser can load these bare specifiers according to the import map.
*/
function createExternalFromImportMap(importMap?: {imports?: Record<string, string>}) {
if (!importMap) return []
const {imports = {}} = importMap

return Object.keys(imports).map((specifier) =>
specifier.endsWith('/') ? new RegExp(`^${escapeRegExp(specifier)}.+`) : specifier,
)
}

/**
* Ensure Sanity entry chunk is always loaded
*
Expand Down
57 changes: 50 additions & 7 deletions packages/sanity/src/_internal/cli/server/renderDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {isMainThread, parentPort, Worker, workerData} from 'node:worker_threads'

import chalk from 'chalk'
import importFresh from 'import-fresh'
import {parse as parseHtml} from 'node-html-parser'
import {createElement} from 'react'
import {renderToStaticMarkup} from 'react-dom/server'

Expand Down Expand Up @@ -41,14 +42,19 @@ interface DocumentProps {
css?: string[]
}

export function renderDocument(options: {
interface RenderDocumentOptions {
monorepo?: SanityMonorepo
studioRootPath: string
props?: DocumentProps
}): Promise<string> {
importMap?: {
imports?: Record<string, string>
}
}

export function renderDocument(options: RenderDocumentOptions): Promise<string> {
return new Promise((resolve, reject) => {
if (!useThreads) {
resolve(getDocumentHtml(options.studioRootPath, options.props))
resolve(getDocumentHtml(options.studioRootPath, options.props, options.importMap))
return
}

Expand Down Expand Up @@ -145,7 +151,7 @@ function renderDocumentFromWorkerData() {
throw new Error('Must be used as a Worker with a valid options object in worker data')
}

const {monorepo, studioRootPath, props} = workerData || {}
const {monorepo, studioRootPath, props, importMap}: RenderDocumentOptions = workerData || {}

if (workerData?.dev) {
// Define `__DEV__` in the worker thread as well
Expand Down Expand Up @@ -191,7 +197,7 @@ function renderDocumentFromWorkerData() {
loader: 'jsx',
})

const html = getDocumentHtml(studioRootPath, props)
const html = getDocumentHtml(studioRootPath, props, importMap)

parentPort.postMessage({type: 'result', html})

Expand All @@ -200,7 +206,11 @@ function renderDocumentFromWorkerData() {
unregisterJs()
}

function getDocumentHtml(studioRootPath: string, props?: DocumentProps): string {
function getDocumentHtml(
studioRootPath: string,
props?: DocumentProps,
importMap?: {imports?: Record<string, string>},
): string {
const Document = getDocumentComponent(studioRootPath)

// NOTE: Validate the list of CSS paths so implementers of `_document.tsx` don't have to
Expand All @@ -216,10 +226,43 @@ function getDocumentHtml(studioRootPath: string, props?: DocumentProps): string
})

debug('Rendering document component using React')
const result = renderToStaticMarkup(createElement(Document, {...defaultProps, ...props, css}))
const result = addImportMapToHtml(
renderToStaticMarkup(createElement(Document, {...defaultProps, ...props, css})),
importMap,
)

return `<!DOCTYPE html>${result}`
}

export function addImportMapToHtml(
html: string,
importMap?: {imports?: Record<string, string>},
): string {
if (!importMap) return html

let root = parseHtml(html)
let htmlEl = root.querySelector('html')
if (!htmlEl) {
const oldRoot = root
root = parseHtml('<html></html>')
htmlEl = root.querySelector('html')!
htmlEl.appendChild(oldRoot)
}

let headEl = htmlEl.querySelector('head')

if (!headEl) {
htmlEl.insertAdjacentHTML('afterbegin', '<head></head>')
headEl = root.querySelector('head')!
}

headEl.insertAdjacentHTML(
'beforeend',
`<script type="importmap">${JSON.stringify(importMap)}</script>`,
)
return root.outerHTML
}

function getDocumentComponent(studioRootPath: string) {
debug('Loading default document component from `sanity` module')
const {DefaultDocument} = __DEV__
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ export function sanityBuildEntries(options: {
cwd: string
monorepo: SanityMonorepo | undefined
basePath: string
importMap?: {imports?: Record<string, string>}
}): Plugin {
const {cwd, monorepo, basePath} = options
const {cwd, monorepo, basePath, importMap} = options

return {
name: 'sanity/server/build-entries',
Expand Down Expand Up @@ -86,6 +87,7 @@ export function sanityBuildEntries(options: {
source: await renderDocument({
monorepo,
studioRootPath: cwd,
importMap,
props: {
basePath,
entryPath,
Expand Down
23 changes: 0 additions & 23 deletions packages/sanity/src/core/components/DefaultDocument.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ const EMPTY_ARRAY: never[] = []
* @beta */
export function DefaultDocument(props: DefaultDocumentProps): ReactElement {
const {entryPath, css = EMPTY_ARRAY, basePath = '/'} = props
const addImportMap = true
return (
<html lang="en">
<head>
Expand All @@ -143,28 +142,6 @@ export function DefaultDocument(props: DefaultDocumentProps): ReactElement {
))}
{/* eslint-disable-next-line react/no-danger */}
<style dangerouslySetInnerHTML={{__html: globalStyles}} />

{addImportMap && (
<script
type="importmap"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: `{
"imports": {
"react": "https://unpkg.com/sanity@3.38.1-canary.107/dist/react.mjs",
"react/jsx-runtime": "https://unpkg.com/sanity@3.38.1-canary.107/dist/react_jsx-runtime.mjs",
"sanity": "https://unpkg.com/sanity@3.38.1-canary.107/dist/index.mjs",
"sanity/presentation": "https://unpkg.com/sanity@3.38.1-canary.107/dist/presentation.mjs",
"sanity/desk": "https://unpkg.com/sanity@3.38.1-canary.107/dist/desk.mjs",
"sanity/router": "https://unpkg.com/sanity@3.38.1-canary.107/dist/router.mjs",
"sanity/_singletons": "https://unpkg.com/sanity@3.38.1-canary.107/dist/_singletons.mjs",
"sanity/structure": "https://unpkg.com/sanity@3.38.1-canary.107/dist/structure.mjs",
"styled-components": "https://unpkg.com/sanity@3.38.1-canary.107/dist/styled-components.mjs"
}
}`,
}}
/>
)}
</head>
<body>
<div id="sanity" />
Expand Down
Loading

0 comments on commit 4ba95fd

Please sign in to comment.