sanity
Version:
Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches
137 lines (118 loc) • 3.65 kB
text/typescript
import {ResizeObserver} from '@juggle/resize-observer'
import {register as registerESBuild} from 'esbuild-register/dist/node'
import jsdomGlobal from 'jsdom-global'
import {addHook} from 'pirates'
import resolveFrom from 'resolve-from'
import {getStudioEnvironmentVariables} from '../server/getStudioEnvironmentVariables'
const jsdomDefaultHtml = `<!doctype html>
<html>
<head><meta charset="utf-8"></head>
<body></body>
</html>`
export function mockBrowserEnvironment(basePath: string): () => void {
// Guard against double-registering
if (global && global.window && '__mockedBySanity' in global.window) {
return () => {
/* intentional noop */
}
}
const domCleanup = jsdomGlobal(jsdomDefaultHtml, {url: 'http://localhost:3333/'})
const windowCleanup = () => global.window.close()
const globalCleanup = provideFakeGlobals(basePath)
const cleanupFileLoader = addHook(
(code, filename) => `module.exports = ${JSON.stringify(filename)}`,
{
ignoreNodeModules: false,
exts: getFileExtensions(),
},
)
const {unregister: unregisterESBuild} = registerESBuild({
target: 'node18',
format: 'cjs',
extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs'],
jsx: 'automatic',
define: {
// define the `process.env` global
...getStudioEnvironmentVariables({prefix: 'process.env.', jsonEncode: true}),
// define the `import.meta.env` global
...getStudioEnvironmentVariables({prefix: 'import.meta.env.', jsonEncode: true}),
},
})
return function cleanupBrowserEnvironment() {
unregisterESBuild()
cleanupFileLoader()
globalCleanup()
windowCleanup()
domCleanup()
}
}
const getFakeGlobals = (basePath: string) => ({
__mockedBySanity: true,
requestAnimationFrame: setImmediate,
cancelAnimationFrame: clearImmediate,
requestIdleCallback: setImmediate,
cancelIdleCallback: clearImmediate,
ace: tryGetAceGlobal(basePath),
InputEvent: global.window?.InputEvent,
customElements: global.window?.customElements,
ResizeObserver: global.window?.ResizeObserver || ResizeObserver,
})
function provideFakeGlobals(basePath: string): () => void {
const globalEnv = global as any as Record<string, unknown>
const globalWindow = global.window as Record<string, any>
const fakeGlobals = getFakeGlobals(basePath)
const stubbedGlobalKeys: string[] = []
const stubbedWindowKeys: string[] = []
for (const [rawKey, value] of Object.entries(fakeGlobals)) {
if (typeof value === 'undefined') {
continue
}
const key = rawKey as keyof typeof fakeGlobals
if (!(key in globalEnv)) {
globalEnv[key] = fakeGlobals[key]
stubbedGlobalKeys.push(key)
}
if (!(key in global.window)) {
globalWindow[key] = fakeGlobals[key]
stubbedWindowKeys.push(key)
}
}
return () => {
stubbedGlobalKeys.forEach((key) => {
delete globalEnv[key]
})
stubbedWindowKeys.forEach((key) => {
delete globalWindow[key]
})
}
}
function tryGetAceGlobal(basePath: string) {
// Work around an issue where using the @sanity/code-input plugin would crash
// due to `ace` not being defined on the global due to odd bundling stategy.
const acePath = resolveFrom.silent(basePath, 'ace-builds')
if (!acePath) {
return undefined
}
try {
// eslint-disable-next-line import/no-dynamic-require
return require(acePath)
} catch (err) {
return undefined
}
}
function getFileExtensions() {
return [
'.jpeg',
'.jpg',
'.png',
'.gif',
'.svg',
'.webp',
'.woff',
'.woff2',
'.ttf',
'.eot',
'.otf',
'.css',
]
}