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
JavaScript
;
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