Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: utilize timestamps in build and renderDocument for AUS #6964

Merged
merged 3 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/sanity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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 "/"', () => {
Expand Down Expand Up @@ -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 = `<html lang="en">
<head><meta charSet="utf-8" /><title>Sanity Studio</title></head>
<body><div id="sanity"/></body>
</html>`

const output = `<html lang="en">
<head><meta charSet="utf-8" ><title>Sanity Studio</title><script type="application/json" id="__imports">{"imports":{"react":"https://example.com/react","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/"}}</script>${TIMESTAMPED_IMPORTMAP_INJECTOR_SCRIPT}</head>
<body><div id="sanity"></div></body>
</html>`

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(
<html lang="en">
Expand All @@ -88,26 +146,22 @@ describe('addImportMapToHtml', () => {
</body>
</html>,
)
const output = addImportMapToHtml(input, importMap)
const output = `<html lang="en"><head><meta charSet="utf-8"><title>Sanity Studio</title><script type="application/json" id="__imports">{"imports":{"react":"https://example.com/react"}}</script>${TIMESTAMPED_IMPORTMAP_INJECTOR_SCRIPT}</head><body><div id="sanity"></div></body></html>`

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>',
)
expect(addTimestampedImportMapScriptToHtml(input, importMap)).toBe(output)
})

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>'
const output = `<html><head><script type="application/json" id="__imports">{"imports":{"react":"https://example.com/react"}}</script>${TIMESTAMPED_IMPORTMAP_INJECTOR_SCRIPT}</head>foo<div>bar</div>baz</html>`

expect(addImportMapToHtml(input, importMap)).toBe(output)
expect(addTimestampedImportMapScriptToHtml(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>'
const output = `<html><head><script type="application/json" id="__imports">{"imports":{"react":"https://example.com/react"}}</script>${TIMESTAMPED_IMPORTMAP_INJECTOR_SCRIPT}</head><body><script src="index.js"></script></body></html>`

expect(addImportMapToHtml(input, importMap)).toBe(output)
expect(addTimestampedImportMapScriptToHtml(input, importMap)).toBe(output)
})
})
36 changes: 36 additions & 0 deletions packages/sanity/src/_internal/cli/server/constants.ts
Original file line number Diff line number Diff line change
@@ -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 = `<script>
// auto-generated script to add import map with timestamp
const importsJson = document.getElementById('__imports')?.textContent;
const { imports = {}, ...rest } = importsJson ? JSON.parse(importsJson) : {};
const importMapEl = document.createElement('script');
importMapEl.type = 'importmap';
const newTimestamp = \`/t\${Math.floor(Date.now() / 1000)}\`;
importMapEl.textContent = JSON.stringify({
imports: Object.fromEntries(
Object.entries(imports).map(([specifier, path]) => {
try {
const url = new URL(path);
if (/^sanity-cdn\\.[a-zA-Z]+$/.test(url.hostname)) {
url.pathname = url.pathname.replace(/\\/t\\d+/, newTimestamp);
}
return [specifier, url.toString()];
} catch {
return [specifier, path];
}
})
),
...rest,
});
document.head.appendChild(importMapEl);
</script>`
8 changes: 5 additions & 3 deletions packages/sanity/src/_internal/cli/server/renderDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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,
)
Expand All @@ -237,7 +238,7 @@ function getDocumentHtml(
/**
* @internal
*/
export function addImportMapToHtml(
export function addTimestampedImportMapScriptToHtml(
html: string,
importMap?: {imports?: Record<string, string>},
): string {
Expand All @@ -261,8 +262,9 @@ export function addImportMapToHtml(

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 8 additions & 5 deletions scripts/uploadBundles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ async function copyPackages() {

interface ManifestPackage {
default: string
versions: string[]
versions: {version: string; timestamp: number}[]
}

interface Manifest {
Expand All @@ -111,7 +111,9 @@ async function updateManifest(newVersions: Map<string, string>) {
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)

Expand All @@ -122,7 +124,7 @@ async function updateManifest(newVersions: Map<string, string>) {
[dirName]: {
...initial.packages[dirName],
default: value,
versions: [...(initial.packages[dirName]?.versions || []), value],
versions: [...(initial.packages[dirName]?.versions || []), {version: value, timestamp}],
ricokahler marked this conversation as resolved.
Show resolved Hide resolved
},
},
}
Expand All @@ -135,8 +137,9 @@ async function updateManifest(newVersions: Map<string, string>) {
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',
},
}

Expand Down
Loading