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
185 lines (153 loc) • 5.09 kB
text/typescript
import {type UserViteConfig} from '@sanity/cli'
import {constants as fsConstants} from 'fs'
import fs from 'fs/promises'
import path from 'path'
import readPkgUp from 'read-pkg-up'
import {build} from 'vite'
import {debug as serverDebug} from './debug'
import {extendViteConfigWithUserConfig, finalizeViteConfig, getViteConfig} from './getViteConfig'
import {writeSanityRuntime} from './runtime'
import {generateWebManifest} from './webManifest'
const debug = serverDebug.extend('static')
export interface ChunkModule {
name: string
originalLength: number
renderedLength: number
}
export interface ChunkStats {
name: string
modules: ChunkModule[]
}
export interface StaticBuildOptions {
cwd: string
basePath: string
outputDir: string
minify?: boolean
profile?: boolean
sourceMap?: boolean
vite?: UserViteConfig
}
export async function buildStaticFiles(
options: StaticBuildOptions,
): Promise<{chunks: ChunkStats[]}> {
const {
cwd,
outputDir,
sourceMap = false,
minify = true,
basePath,
vite: extendViteConfig,
} = options
debug('Writing Sanity runtime files')
await writeSanityRuntime({cwd, reactStrictMode: false, watch: false, basePath})
debug('Resolving vite config')
const mode = 'production'
let viteConfig = await getViteConfig({
cwd,
basePath,
outputDir,
minify,
sourceMap,
mode,
})
// Extend Vite configuration with user-provided config
if (extendViteConfig) {
viteConfig = await extendViteConfigWithUserConfig(
{command: 'build', mode},
viteConfig,
extendViteConfig,
)
viteConfig = finalizeViteConfig(viteConfig)
}
// Copy files placed in /static to the built /static
debug('Copying static files from /static to output dir')
const staticPath = path.join(outputDir, 'static')
await copyDir(path.join(cwd, 'static'), staticPath)
// Write favicons, not overwriting ones that already exist, to static folder
debug('Writing favicons to output dir')
const faviconBasePath = `${basePath.replace(/\/+$/, '')}/static`
await writeFavicons(faviconBasePath, staticPath)
debug('Bundling using vite')
const bundle = await build(viteConfig)
debug('Bundling complete')
// For typescript only - this shouldn't ever be the case given we're not watching
if (Array.isArray(bundle) || !('output' in bundle)) {
return {chunks: []}
}
const stats: ChunkStats[] = []
bundle.output.forEach((chunk) => {
if (chunk.type !== 'chunk') {
return
}
stats.push({
name: chunk.name,
modules: Object.entries(chunk.modules).map(([rawFilePath, chunkModule]) => {
const filePath = rawFilePath.startsWith('\x00')
? rawFilePath.slice('\x00'.length)
: rawFilePath
return {
name: path.isAbsolute(filePath) ? path.relative(cwd, filePath) : filePath,
originalLength: chunkModule.originalLength,
renderedLength: chunkModule.renderedLength,
}
}),
})
})
return {chunks: stats}
}
async function copyDir(srcDir: string, destDir: string, skipExisting?: boolean): Promise<void> {
await fs.mkdir(destDir, {recursive: true})
for (const file of await tryReadDir(srcDir)) {
const srcFile = path.resolve(srcDir, file)
if (srcFile === destDir) {
continue
}
const destFile = path.resolve(destDir, file)
const stat = await fs.stat(srcFile)
if (stat.isDirectory()) {
await copyDir(srcFile, destFile, skipExisting)
} else if (skipExisting) {
await fs.copyFile(srcFile, destFile, fsConstants.COPYFILE_EXCL).catch(skipIfExistsError)
} else {
await fs.copyFile(srcFile, destFile)
}
}
}
async function tryReadDir(dir: string): Promise<string[]> {
try {
const content = await fs.readdir(dir)
return content
} catch (err) {
if (err.code === 'ENOENT') {
return []
}
throw err
}
}
function skipIfExistsError(err: Error & {code: string}) {
if (err.code === 'EEXIST') {
return
}
throw err
}
async function writeFavicons(basePath: string, destDir: string): Promise<void> {
const sanityPkgPath = (await readPkgUp({cwd: __dirname}))?.path
const faviconsPath = sanityPkgPath
? path.join(path.dirname(sanityPkgPath), 'static', 'favicons')
: undefined
if (!faviconsPath) {
throw new Error('Unable to resolve `sanity` module root')
}
await fs.mkdir(destDir, {recursive: true})
await copyDir(faviconsPath, destDir, true)
await writeWebManifest(basePath, destDir)
// Copy the /static/favicon.ico to /favicon.ico as well, because some tools/browsers
// blindly expects it to be there before requesting the HTML containing the actual path
await fs.copyFile(path.join(destDir, 'favicon.ico'), path.join(destDir, '..', 'favicon.ico'))
}
async function writeWebManifest(basePath: string, destDir: string): Promise<void> {
const content = JSON.stringify(generateWebManifest(basePath), null, 2)
await fs
.writeFile(path.join(destDir, 'manifest.webmanifest'), content, 'utf8')
.catch(skipIfExistsError)
}