diff --git a/packages/sanity/package.json b/packages/sanity/package.json index 997e4bafccd..0d45c483062 100644 --- a/packages/sanity/package.json +++ b/packages/sanity/package.json @@ -272,6 +272,7 @@ "@types/configstore": "^5.0.1", "@types/connect-history-api-fallback": "^1.5.2", "@types/debug": "^4.1.12", + "@types/jsdom": "^20.0.0", "@types/lodash": "^4.14.149", "@types/log-symbols": "^2.0.0", "@types/node": "^18.19.8", diff --git a/packages/sanity/src/_internal/cli/server/__tests__/renderDocument.test.tsx b/packages/sanity/src/_internal/cli/server/__tests__/renderDocument.test.tsx index 5a5ef2f9231..6301278762d 100644 --- a/packages/sanity/src/_internal/cli/server/__tests__/renderDocument.test.tsx +++ b/packages/sanity/src/_internal/cli/server/__tests__/renderDocument.test.tsx @@ -1,7 +1,9 @@ import {describe, expect, it} from '@jest/globals' +import {JSDOM} from 'jsdom' import {renderToStaticMarkup} from 'react-dom/server' -import {_prefixUrlWithBasePath, addImportMapToHtml} from '../renderDocument' +import {TIMESTAMPED_IMPORTMAP_INJECTOR_SCRIPT} from '../constants' +import {_prefixUrlWithBasePath, addTimestampedImportMapScriptToHtml} from '../renderDocument' describe('_prefixUrlWithBasePath', () => { describe('when basePath is default value of "/"', () => { @@ -69,13 +71,69 @@ describe('_prefixUrlWithBasePath', () => { }) }) -describe('addImportMapToHtml', () => { +describe('addTimestampedImportMapScriptToHtml', () => { const importMap = { imports: { react: 'https://example.com/react', }, } + it('takes the import map from the `#__imports` script tag synchronously creates an importmap', () => { + const importMapWithSanityTimestamps = { + ...importMap, + imports: { + ...importMap.imports, + 'sanity': 'https://sanity-cdn.work/v1/modules/sanity/default/%5E3.40.0/t12345', + 'sanity/': 'https://sanity-cdn.work/v1/modules/sanity/default/%5E3.40.0/t12345/', + '@sanity/vision': + 'https://sanity-cdn.work/v1/modules/@sanity__vision/default/%5E3.40.0/t12345', + '@sanity/vision/': + 'https://sanity-cdn.work/v1/modules/@sanity__vision/default/%5E3.40.0/t12345/', + }, + } + + const input = ` + Sanity Studio +
+ ` + + const output = ` + Sanity Studio${TIMESTAMPED_IMPORTMAP_INJECTOR_SCRIPT} +
+ ` + + expect(addTimestampedImportMapScriptToHtml(input, importMapWithSanityTimestamps)).toBe(output) + + const {document} = new JSDOM(output, {runScripts: 'dangerously'}).window + const staticImportMap = JSON.parse(document.querySelector('#__imports')?.textContent as string) + const runtimeImportMap = JSON.parse( + document.querySelector('script[type="importmap"]')?.textContent as string, + ) + + expect(runtimeImportMap).toMatchObject({ + imports: { + 'react': 'https://example.com/react', + 'sanity': expect.stringMatching( + /^https:\/\/sanity-cdn\.work\/v1\/modules\/sanity\/default\/%5E3\.40\.0\/t\d+$/, + ), + 'sanity/': expect.stringMatching( + // notice the trailing slash here + /^https:\/\/sanity-cdn\.work\/v1\/modules\/sanity\/default\/%5E3\.40\.0\/t\d+\/$/, + ), + '@sanity/vision': expect.stringMatching( + /^https:\/\/sanity-cdn\.work\/v1\/modules\/@sanity__vision\/default\/%5E3\.40\.0\/t\d+$/, + ), + '@sanity/vision/': expect.stringMatching( + // notice the trailing slash here + /^https:\/\/sanity-cdn\.work\/v1\/modules\/@sanity__vision\/default\/%5E3\.40\.0\/t\d+\/$/, + ), + }, + }) + + // ensures that the timestamps have actually been replaced + expect(staticImportMap).not.toEqual(runtimeImportMap) + }) + 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( @@ -88,26 +146,22 @@ describe('addImportMapToHtml', () => { , ) - const output = addImportMapToHtml(input, importMap) + const output = `Sanity Studio${TIMESTAMPED_IMPORTMAP_INJECTOR_SCRIPT}
` - expect(output).toBe( - 'Sanity Studio
', - ) + expect(addTimestampedImportMapScriptToHtml(input, importMap)).toBe(output) }) it('creates an element if none exist', () => { const input = 'foo
bar
baz' - const output = - 'foo
bar
baz' + const output = `${TIMESTAMPED_IMPORTMAP_INJECTOR_SCRIPT}foo
bar
baz` - expect(addImportMapToHtml(input, importMap)).toBe(output) + expect(addTimestampedImportMapScriptToHtml(input, importMap)).toBe(output) }) it('creates a to the document if one does not exist', () => { const input = '' - const output = - '' + const output = `${TIMESTAMPED_IMPORTMAP_INJECTOR_SCRIPT}` - expect(addImportMapToHtml(input, importMap)).toBe(output) + expect(addTimestampedImportMapScriptToHtml(input, importMap)).toBe(output) }) }) diff --git a/packages/sanity/src/_internal/cli/server/constants.ts b/packages/sanity/src/_internal/cli/server/constants.ts new file mode 100644 index 00000000000..ebc2a9521b8 --- /dev/null +++ b/packages/sanity/src/_internal/cli/server/constants.ts @@ -0,0 +1,36 @@ +/** + * This script takes the import map from the `#__imports` script tag, + * modifies relevant URLs that match the sanity-cdn hostname by replacing + * the existing timestamp in the sanity-cdn URLs with a new runtime timestamp, + * and injects the modified import map back into the HTML. + * + * This will be injected into the HTML of the user's bundle. + * + * Note that this is in a separate constants file to prevent "Cannot access + * before initialization" errors. + */ +export const TIMESTAMPED_IMPORTMAP_INJECTOR_SCRIPT = `` diff --git a/packages/sanity/src/_internal/cli/server/renderDocument.ts b/packages/sanity/src/_internal/cli/server/renderDocument.ts index 2a47f997af1..7b6817fb773 100644 --- a/packages/sanity/src/_internal/cli/server/renderDocument.ts +++ b/packages/sanity/src/_internal/cli/server/renderDocument.ts @@ -17,6 +17,7 @@ import {createElement} from 'react' import {renderToStaticMarkup} from 'react-dom/server' import {getAliases} from './aliases' +import {TIMESTAMPED_IMPORTMAP_INJECTOR_SCRIPT} from './constants' import {debug as serverDebug} from './debug' import {type SanityMonorepo} from './sanityMonorepo' @@ -226,7 +227,7 @@ function getDocumentHtml( }) debug('Rendering document component using React') - const result = addImportMapToHtml( + const result = addTimestampedImportMapScriptToHtml( renderToStaticMarkup(createElement(Document, {...defaultProps, ...props, css})), importMap, ) @@ -237,7 +238,7 @@ function getDocumentHtml( /** * @internal */ -export function addImportMapToHtml( +export function addTimestampedImportMapScriptToHtml( html: string, importMap?: {imports?: Record}, ): string { @@ -261,8 +262,9 @@ export function addImportMapToHtml( headEl.insertAdjacentHTML( 'beforeend', - ``, + ``, ) + headEl.insertAdjacentHTML('beforeend', TIMESTAMPED_IMPORTMAP_INJECTOR_SCRIPT) return root.outerHTML } diff --git a/packages/sanity/src/_internal/cli/util/getAutoUpdatesImportMap.ts b/packages/sanity/src/_internal/cli/util/getAutoUpdatesImportMap.ts index 44bf39f57e1..6e8f2884dd9 100644 --- a/packages/sanity/src/_internal/cli/util/getAutoUpdatesImportMap.ts +++ b/packages/sanity/src/_internal/cli/util/getAutoUpdatesImportMap.ts @@ -17,11 +17,13 @@ const MODULES_HOST = * @internal */ export function getAutoUpdateImportMap(version: string): AutoUpdatesImportMap { + const timestamp = `t${Math.floor(Date.now() / 1000)}` + const autoUpdatesImports = { - 'sanity': `${MODULES_HOST}/v1/modules/sanity/default/${version}`, - 'sanity/': `${MODULES_HOST}/v1/modules/sanity/default/${version}/`, - '@sanity/vision': `${MODULES_HOST}/v1/modules/@sanity__vision/default/${version}`, - '@sanity/vision/': `${MODULES_HOST}/v1/modules/@sanity__vision/default/${version}/`, + 'sanity': `${MODULES_HOST}/v1/modules/sanity/default/${version}/${timestamp}`, + 'sanity/': `${MODULES_HOST}/v1/modules/sanity/default/${version}/${timestamp}/`, + '@sanity/vision': `${MODULES_HOST}/v1/modules/@sanity__vision/default/${version}/${timestamp}`, + '@sanity/vision/': `${MODULES_HOST}/v1/modules/@sanity__vision/default/${version}/${timestamp}/`, } return autoUpdatesImports diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a616f834b02..2987730e6ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1881,6 +1881,9 @@ importers: '@types/debug': specifier: ^4.1.12 version: 4.1.12 + '@types/jsdom': + specifier: ^20.0.0 + version: 20.0.1 '@types/lodash': specifier: ^4.14.149 version: 4.17.0 diff --git a/scripts/uploadBundles.ts b/scripts/uploadBundles.ts index d11f8936705..de62852561f 100644 --- a/scripts/uploadBundles.ts +++ b/scripts/uploadBundles.ts @@ -91,7 +91,7 @@ async function copyPackages() { interface ManifestPackage { default: string - versions: string[] + versions: {version: string; timestamp: number}[] } interface Manifest { @@ -111,7 +111,9 @@ async function updateManifest(newVersions: Map) { console.log('Existing manifest not found', error) } - // Add the new version to the manifest + const timestamp = Math.floor(Date.now() / 1000) + + // Add the new version to the manifest with timestamp const newManifest = Array.from(newVersions).reduce((initial, [key, value]) => { const dirName = cleanDirName(key) @@ -122,7 +124,7 @@ async function updateManifest(newVersions: Map) { [dirName]: { ...initial.packages[dirName], default: value, - versions: [...(initial.packages[dirName]?.versions || []), value], + versions: [...(initial.packages[dirName]?.versions || []), {version: value, timestamp}], }, }, } @@ -135,8 +137,9 @@ async function updateManifest(newVersions: Map) { destination: 'modules/v1/manifest-v1.json', contentType: 'application/json', metadata: { - // 10 seconds - cacheControl: 'public, max-age=10', + // no-cache to help with consistency across pods when this manifest + // is downloaded in the module-server + cacheControl: 'no-cache, max-age=0', }, }