@sanity/server
Version:
Webserver dedicated to serving Sanity, allowing a smooth development experience
163 lines (135 loc) • 4.35 kB
text/typescript
import path from 'path'
import fs from 'fs/promises'
import {constants as fsConstants} from 'fs'
import {build, InlineConfig} from 'vite'
import {finalizeViteConfig, getViteConfig} from './getViteConfig'
import {generateWebManifest} from './webManifest'
import {writeSanityRuntime} from './runtime'
import {debug as serverDebug} from './debug'
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?: (config: InlineConfig) => InlineConfig
}
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})
debug('Resolving vite config')
let viteConfig = await getViteConfig({
cwd,
basePath,
outputDir,
minify,
sourceMap,
mode: 'production',
})
if (extendViteConfig) {
debug('Extending vite config with user-specified config')
viteConfig = finalizeViteConfig(extendViteConfig(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> {
await fs.mkdir(destDir, {recursive: true})
await copyDir(path.resolve(__dirname, '../static/favicons'), destDir, true)
await writeWebManifest(basePath, destDir)
}
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)
}