UNPKG

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

490 lines (486 loc) • 22.1 kB
"use strict"; var viteReact = require("@vitejs/plugin-react"), debug$4 = require("debug"), path = require("path"), readPkgUp = require("read-pkg-up"), vite = require("vite"), cli = require("../cli.js"); require("resolve-from"); var history = require("connect-history-api-fallback"), fs = require("fs"), fs$1 = require("fs/promises"), chokidar = require("chokidar"), chalk = require("chalk"), importFresh = require("import-fresh"), React = require("react"), server = require("react-dom/server"), worker_threads = require("worker_threads"); function _interopDefaultCompat(e) { return e && typeof e == "object" && "default" in e ? e : { default: e }; } var viteReact__default = /* @__PURE__ */ _interopDefaultCompat(viteReact), debug__default = /* @__PURE__ */ _interopDefaultCompat(debug$4), path__default = /* @__PURE__ */ _interopDefaultCompat(path), readPkgUp__default = /* @__PURE__ */ _interopDefaultCompat(readPkgUp), history__default = /* @__PURE__ */ _interopDefaultCompat(history), fs__default = /* @__PURE__ */ _interopDefaultCompat(fs), fs__default$1 = /* @__PURE__ */ _interopDefaultCompat(fs$1), chokidar__default = /* @__PURE__ */ _interopDefaultCompat(chokidar), chalk__default = /* @__PURE__ */ _interopDefaultCompat(chalk), importFresh__default = /* @__PURE__ */ _interopDefaultCompat(importFresh); const debug$3 = debug__default.default("sanity:server"); function getAliases(opts) { const { monorepo } = opts; if (!(monorepo != null && monorepo.path)) return {}; const aliasesPath = path__default.default.resolve(monorepo.path, "dev/aliases.cjs"), devAliases = require(aliasesPath); return Object.fromEntries( Object.entries(devAliases).map(([key, modulePath]) => [key, path__default.default.resolve(monorepo.path, modulePath)]) ); } function normalizeBasePath(pathName) { return `/${pathName}/`.replace(/^\/+/, "/").replace(/\/+$/, "/"); } async function loadSanityMonorepo(cwd) { let p = cwd; for (; p !== "/"; ) { const readResult = await readPkgUp__default.default({ cwd: p }); if (!readResult) return; if (readResult.packageJson.isSanityMonorepo) return { path: path__default.default.dirname(readResult.path) }; p = path__default.default.dirname(path__default.default.dirname(readResult.path)); } } const debug$2 = debug$3.extend("renderDocument"), useThreads = typeof process.env.JEST_WORKER_ID > "u", hasWarnedAbout = /* @__PURE__ */ new Set(), defaultProps = { entryPath: "./.sanity/runtime/app.js" }, autoGeneratedWarning = ` This file is auto-generated from "sanity dev". Modifications to this file are automatically discarded. `.trim(); function renderDocument(options) { return new Promise((resolve, reject) => { if (!useThreads) { resolve(getDocumentHtml(options.studioRootPath, options.props)); return; } debug$2("Starting worker thread for %s", __filename); const worker = new worker_threads.Worker(__filename, { execArgv: void 0, workerData: { ...options, dev: !1, shouldWarn: !0 }, // eslint-disable-next-line no-process-env env: process.env }); worker.on("message", (msg) => { if (msg.type === "warning") { if (hasWarnedAbout.has(msg.warnKey)) return; Array.isArray(msg.message) ? msg.message.forEach( (warning) => console.warn(`${chalk__default.default.yellow("[warn]")} ${warning}`) ) : console.warn(`${chalk__default.default.yellow("[warn]")} ${msg.message}`), hasWarnedAbout.add(msg.warnKey); return; } if (msg.type === "error") { debug$2("Error from worker: %s", msg.error || "Unknown error"), reject(new Error(msg.error || "Document rendering worker stopped with an unknown error")); return; } msg.type === "result" && (debug$2("Document HTML rendered, %d bytes", msg.html.length), resolve(msg.html)); }), worker.on("error", (err) => { debug$2("Worker errored: %s", err.message), reject(err); }), worker.on("exit", (code) => { code !== 0 && (debug$2("Worker stopped with code %d", code), reject(new Error(`Document rendering worker stopped with exit code ${code}`))); }); }); } function decorateIndexWithAutoGeneratedWarning(template) { return template.replace(/<head/, ` <!-- ${autoGeneratedWarning} --> <head`); } function getPossibleDocumentComponentLocations(studioRootPath) { return [path__default.default.join(studioRootPath, "_document.js"), path__default.default.join(studioRootPath, "_document.tsx")]; } function _prefixUrlWithBasePath(url, basePath) { const normalizedBasePath = basePath.startsWith("/") ? basePath : `/${basePath}`; return url.startsWith("/") ? normalizedBasePath.endsWith("/") ? `${normalizedBasePath.slice(0, -1)}${url}` : `${normalizedBasePath}${url}` : normalizedBasePath.endsWith("/") ? `${normalizedBasePath}${url}` : `${normalizedBasePath}/${url}`; } !worker_threads.isMainThread && worker_threads.parentPort && renderDocumentFromWorkerData(); function renderDocumentFromWorkerData() { var _a; if (!worker_threads.parentPort || !worker_threads.workerData) throw new Error("Must be used as a Worker with a valid options object in worker data"); const { monorepo, studioRootPath, props } = worker_threads.workerData || {}; if ((_a = worker_threads.workerData) != null && _a.dev && (global.__DEV__ = !0), typeof studioRootPath != "string") { worker_threads.parentPort.postMessage({ type: "error", message: "Missing/invalid `studioRootPath` option" }); return; } if (props && typeof props != "object") { worker_threads.parentPort.postMessage({ type: "error", message: "`props` must be an object if provided" }); return; } debug$2("Registering potential aliases"), require("module-alias").addAliases(getAliases({ monorepo })), debug$2("Registering esbuild for node %s", process.version); const { unregister } = require("esbuild-register/dist/node").register({ target: `node${process.version.slice(1)}`, jsx: "automatic", extensions: [".jsx", ".ts", ".tsx", ".mjs"] }); debug$2("Registering esbuild for .js files using jsx loader"); const { unregister: unregisterJs } = require("esbuild-register/dist/node").register({ target: `node${process.version.slice(1)}`, extensions: [".js"], jsx: "automatic", loader: "jsx" }), html = getDocumentHtml(studioRootPath, props); worker_threads.parentPort.postMessage({ type: "result", html }), unregister(), unregisterJs(); } function getDocumentHtml(studioRootPath, props) { var _a; const Document = getDocumentComponent(studioRootPath), css = (_a = props == null ? void 0 : props.css) == null ? void 0 : _a.map((url) => { try { return new URL(url).toString(); } catch { return _prefixUrlWithBasePath(url, props.basePath); } }); return debug$2("Rendering document component using React"), `<!DOCTYPE html>${server.renderToStaticMarkup(React.createElement(Document, { ...defaultProps, ...props, css }))}`; } function getDocumentComponent(studioRootPath) { var _a; debug$2("Loading default document component from `sanity` module"); const { DefaultDocument } = require("sanity"); debug$2("Attempting to load user-defined document component from %s", studioRootPath); const userDefined = tryLoadDocumentComponent(studioRootPath); if (!userDefined) return debug$2("Using default document component"), DefaultDocument; debug$2("Found user defined document component at %s", userDefined.path); const DocumentComp = userDefined.component.default || userDefined.component; if (typeof DocumentComp == "function") return debug$2("User defined document component is a function, assuming valid"), DocumentComp; debug$2("User defined document component did not have a default export"); const userExports = Object.keys(userDefined.component).join(", ") || "None", relativePath = path__default.default.relative(process.cwd(), userDefined.path), typeHint = typeof userDefined.component.default > "u" ? "" : ` (type was ${typeof userDefined.component.default})`, warnKey = `${relativePath}/${userDefined.modified}`; return (_a = worker_threads.parentPort) == null || _a.postMessage({ type: "warning", message: [ `${relativePath} did not have a default export that is a React component${typeHint}`, `Named exports/properties found: ${userExports}`.trim(), 'Using default document component from "sanity".' ], warnKey }), DefaultDocument; } function tryLoadDocumentComponent(studioRootPath) { var _a; const locations = getPossibleDocumentComponentLocations(studioRootPath); for (const componentPath of locations) { debug$2("Trying to load document component from %s", componentPath); try { return { // eslint-disable-next-line import/no-dynamic-require component: importFresh__default.default(componentPath), path: componentPath, // eslint-disable-next-line no-sync modified: Math.floor((_a = fs__default.default.statSync(componentPath)) == null ? void 0 : _a.mtimeMs) }; } catch (err) { if (err.code !== "MODULE_NOT_FOUND") throw debug$2("Failed to load document component: %s", err.message), err; debug$2("Document component not found at %s", componentPath); } } return null; } const entryChunkId = ".sanity/runtime/app.js"; function sanityBuildEntries(options) { const { cwd, monorepo, basePath } = options; return { name: "sanity/server/build-entries", apply: "build", buildStart() { this.emitFile({ type: "chunk", id: entryChunkId, name: "sanity" }); }, async generateBundle(_options, outputBundle) { var _a; const bundle = outputBundle, entryFile = Object.values(bundle).find( (file) => { var _a2; return file.type === "chunk" && file.name === "sanity" && ((_a2 = file.facadeModuleId) == null ? void 0 : _a2.endsWith(entryChunkId)); } ); if (!entryFile) throw new Error(`Failed to find entry file in bundle (${entryChunkId})`); if (entryFile.type !== "chunk") throw new Error("Entry file is not a chunk"); const entryFileName = entryFile.fileName, entryPath = [basePath.replace(/\/+$/, ""), entryFileName].join("/"); let css = []; if ((_a = entryFile.viteMetadata) != null && _a.importedCss) { css = [...entryFile.viteMetadata.importedCss]; for (const key of entryFile.imports) { const entry = bundle[key], importedCss = entry && entry.type === "chunk" ? entry.viteMetadata.importedCss : void 0; importedCss && css.push(...importedCss); } } this.emitFile({ type: "asset", fileName: "index.html", source: await renderDocument({ monorepo, studioRootPath: cwd, props: { basePath, entryPath, css } }) }); } }; } function sanityDotWorkaroundPlugin() { return { name: "sanity/server/dot-workaround", configureServer(server2) { const { root } = server2.config; return () => { const handler = history__default.default({ disableDotRule: !0, rewrites: [ { from: /\/index.html$/, to: ({ parsedUrl }) => { const pathname = parsedUrl.pathname; return pathname && fs__default.default.existsSync(path__default.default.join(root, pathname)) ? pathname : "/index.html"; } } ] }); server2.middlewares.use((req, res, next) => { handler(req, res, next); }); }; } }; } function generateWebManifest(basePath) { return { icons: [ { src: `${basePath}/favicon-192.png`, type: "image/png", sizes: "192x192" }, { src: `${basePath}/favicon-512.png`, type: "image/png", sizes: "512x512" } ] }; } const mimeTypes = { ".ico": "image/x-icon", ".svg": "image/svg+xml", ".png": "image/png" }; function sanityFaviconsPlugin({ defaultFaviconsPath, customFaviconsPath, staticUrlPath }) { const cache = {}; async function getFavicons() { return cache.favicons || (cache.favicons = await fs__default$1.default.readdir(defaultFaviconsPath)), cache.favicons; } async function hasCustomFavicon() { try { return await fs__default$1.default.access(path__default.default.join(customFaviconsPath, "favicon.ico")), !0; } catch { return !1; } } return { name: "sanity/server/sanity-favicons", apply: "serve", configureServer(viteDevServer) { const webManifest = JSON.stringify(generateWebManifest(staticUrlPath), null, 2), webManifestPath = `${staticUrlPath}/manifest.webmanifest`; return () => { viteDevServer.middlewares.use(async (req, res, next) => { var _a; if ((_a = req.url) != null && _a.endsWith(webManifestPath)) { res.writeHead(200, "OK", { "content-type": "application/manifest+json" }), res.write(webManifest), res.end(); return; } const pathName = (req._parsedUrl || new URL(req.url || "/", "http://localhost:3333")).pathname || "", fileName = path__default.default.basename(pathName || ""), icons = await getFavicons(); if (!(pathName.startsWith("/favicon.ico") || icons.includes(fileName) && pathName.includes(staticUrlPath))) { next(); return; } const faviconPath = fileName === "favicon.ico" && await hasCustomFavicon() ? path__default.default.join(customFaviconsPath, "favicon.ico") : path__default.default.join(defaultFaviconsPath, fileName), mimeType = mimeTypes[path__default.default.extname(fileName)] || "application/octet-stream"; res.writeHead(200, "OK", { "content-type": mimeType }), res.write(await fs__default$1.default.readFile(faviconPath)), res.end(); }); }; } }; } function sanityRuntimeRewritePlugin() { return { name: "sanity/server/sanity-runtime-rewrite", apply: "serve", configureServer(viteDevServer) { return () => { viteDevServer.middlewares.use((req, res, next) => { req.url === "/index.html" && (req.url = "/.sanity/runtime/index.html"), next(); }); }; } }; } async function getViteConfig(options) { var _a; const { cwd, mode, outputDir, // default to `true` when `mode=development` sourceMap = options.mode === "development", server: server2, minify, basePath: rawBasePath = "/" } = options, monorepo = await loadSanityMonorepo(cwd), basePath = normalizeBasePath(rawBasePath), sanityPkgPath = (_a = await readPkgUp__default.default({ cwd: __dirname })) == null ? void 0 : _a.path; if (!sanityPkgPath) throw new Error("Unable to resolve `sanity` module root"); const customFaviconsPath = path__default.default.join(cwd, "static"), defaultFaviconsPath = path__default.default.join(path__default.default.dirname(sanityPkgPath), "static", "favicons"), staticPath = `${basePath}static`, viteConfig = { // Define a custom cache directory so that sanity's vite cache // does not conflict with any potential local vite projects cacheDir: "node_modules/.sanity/vite", root: cwd, base: basePath, build: { outDir: outputDir || path__default.default.resolve(cwd, "dist"), sourcemap: sourceMap }, server: { host: server2 == null ? void 0 : server2.host, port: (server2 == null ? void 0 : server2.port) || 3333, strictPort: !0 }, configFile: !1, mode, plugins: [ viteReact__default.default(), sanityFaviconsPlugin({ defaultFaviconsPath, customFaviconsPath, staticUrlPath: staticPath }), sanityDotWorkaroundPlugin(), sanityRuntimeRewritePlugin(), sanityBuildEntries({ basePath, cwd, monorepo }) ], envPrefix: "SANITY_STUDIO_", logLevel: mode === "production" ? "silent" : "info", resolve: { alias: getAliases({ monorepo }) }, define: { // eslint-disable-next-line no-process-env __SANITY_STAGING__: process.env.SANITY_INTERNAL_ENV === "staging", "process.env.MODE": JSON.stringify(mode), ...cli.getStudioEnvironmentVariables({ prefix: "process.env.", jsonEncode: !0 }) } }; return mode === "production" && (viteConfig.build = { ...viteConfig.build, assetsDir: "static", minify: minify ? "esbuild" : !1, emptyOutDir: !1, // Rely on CLI to do this rollupOptions: { input: { sanity: path__default.default.join(cwd, ".sanity", "runtime", "app.js") } } }), viteConfig; } function finalizeViteConfig(config) { var _a, _b; if (typeof ((_b = (_a = config.build) == null ? void 0 : _a.rollupOptions) == null ? void 0 : _b.input) != "object") throw new Error( "Vite config must contain `build.rollupOptions.input`, and it must be an object" ); if (!config.root) throw new Error( "Vite config must contain `root` property, and must point to the Sanity root directory" ); return vite.mergeConfig(config, { build: { rollupOptions: { input: { sanity: path__default.default.join(config.root, ".sanity", "runtime", "app.js") } } } }); } async function extendViteConfigWithUserConfig(env, defaultConfig, userConfig) { let config = defaultConfig; return typeof userConfig == "function" ? (debug__default.default("Extending vite config using user-specified function"), config = await userConfig(config, env)) : typeof userConfig == "object" && (debug__default.default("Merging vite config using user-specified object"), config = vite.mergeConfig(config, userConfig)), config; } const entryModule = ` // This file is auto-generated on 'sanity dev' // Modifications to this file is automatically discarded import {renderStudio} from "sanity" import studioConfig from %STUDIO_CONFIG_LOCATION% renderStudio( document.getElementById("sanity"), studioConfig, {reactStrictMode: %STUDIO_REACT_STRICT_MODE%, basePath: %STUDIO_BASE_PATH%} ) `, noConfigEntryModule = ` // This file is auto-generated on 'sanity dev' // Modifications to this file is automatically discarded import {renderStudio} from "sanity" const studioConfig = {missingConfigFile: true} renderStudio( document.getElementById("sanity"), studioConfig, {reactStrictMode: %STUDIO_REACT_STRICT_MODE%, basePath: %STUDIO_BASE_PATH%} ) `; function getEntryModule(options) { const { reactStrictMode, relativeConfigLocation, basePath } = options; return (relativeConfigLocation ? entryModule : noConfigEntryModule).replace(/%STUDIO_REACT_STRICT_MODE%/, JSON.stringify(!!reactStrictMode)).replace(/%STUDIO_CONFIG_LOCATION%/, JSON.stringify(relativeConfigLocation)).replace(/%STUDIO_BASE_PATH%/, JSON.stringify(basePath || "/")); } const debug$1 = debug$3.extend("config"); async function getSanityStudioConfigPath(studioRootPath) { const configPaths = [ path__default.default.join(studioRootPath, "sanity.config.mjs"), path__default.default.join(studioRootPath, "sanity.config.js"), path__default.default.join(studioRootPath, "sanity.config.ts"), path__default.default.join(studioRootPath, "sanity.config.jsx"), path__default.default.join(studioRootPath, "sanity.config.tsx") ]; debug$1("Looking for configuration file in %d possible locations", configPaths.length); const availableConfigs = (await Promise.all( configPaths.map(async (configPath) => ({ path: configPath, exists: await fileExists(configPath) })) )).filter((config) => config.exists); return debug$1("Found %d available configuration files", availableConfigs.length), availableConfigs.length === 0 ? (console.warn("No `sanity.config.js`/`sanity.config.ts` found - using default studio config"), null) : (availableConfigs.length > 1 && (console.warn("Found multiple potential studio configs:"), availableConfigs.forEach((config) => console.warn(` - ${config.path}`)), console.warn(`Using ${availableConfigs[0].path}`)), availableConfigs[0].path); } function fileExists(filePath) { return fs__default$1.default.stat(filePath).then( () => !0, () => !1 ); } const debug = debug$3.extend("runtime"); async function writeSanityRuntime({ cwd, reactStrictMode, watch, basePath }) { debug("Resolving Sanity monorepo information"); const monorepo = await loadSanityMonorepo(cwd), runtimeDir = path__default.default.join(cwd, ".sanity", "runtime"); debug("Making runtime directory"), await fs__default$1.default.mkdir(runtimeDir, { recursive: !0 }); async function renderAndWriteDocument() { debug("Rendering document template"); const indexHtml = decorateIndexWithAutoGeneratedWarning( await renderDocument({ studioRootPath: cwd, monorepo, props: { entryPath: `/${path__default.default.relative(cwd, path__default.default.join(runtimeDir, "app.js"))}`, basePath: basePath || "/" } }) ); debug("Writing index.html to runtime directory"), await fs__default$1.default.writeFile(path__default.default.join(runtimeDir, "index.html"), indexHtml); } watch && chokidar__default.default.watch(getPossibleDocumentComponentLocations(cwd)).on("all", () => renderAndWriteDocument()), await renderAndWriteDocument(), debug("Writing app.js to runtime directory"); const studioConfigPath = await getSanityStudioConfigPath(cwd), relativeConfigLocation = studioConfigPath ? path__default.default.relative(runtimeDir, studioConfigPath) : null; await fs__default$1.default.writeFile( path__default.default.join(runtimeDir, "app.js"), getEntryModule({ reactStrictMode, relativeConfigLocation, basePath }) ); } exports.debug = debug$3; exports.extendViteConfigWithUserConfig = extendViteConfigWithUserConfig; exports.finalizeViteConfig = finalizeViteConfig; exports.generateWebManifest = generateWebManifest; exports.getViteConfig = getViteConfig; exports.writeSanityRuntime = writeSanityRuntime; //# sourceMappingURL=runtime.js.map