astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
282 lines (281 loc) • 9.95 kB
JavaScript
import fsMod from "node:fs";
import { dirname, relative } from "node:path";
import { performance } from "node:perf_hooks";
import { fileURLToPath } from "node:url";
import colors from "piccolore";
import { createServer } from "vite";
import { syncFonts } from "../../assets/fonts/sync.js";
import { CONTENT_TYPES_FILE } from "../../content/consts.js";
import { getDataStoreChunkSize, getDataStoreDir, getDataStoreFile } from "../../content/paths.js";
import { globalContentLayer } from "../../content/instance.js";
import { createContentTypesGenerator } from "../../content/index.js";
import { MutableDataStore } from "../../content/mutable-data-store.js";
import { getContentPaths, globalContentConfigObserver } from "../../content/utils.js";
import { syncAstroEnv } from "../../env/sync.js";
import { telemetry } from "../../events/index.js";
import { eventCliSession } from "../../events/session.js";
import { runHookConfigDone, runHookConfigSetup } from "../../integrations/hooks.js";
import { getTimeStat } from "../build/util.js";
import { resolveConfig } from "../config/config.js";
import { loadOrCreateNodeLogger } from "../logger/load.js";
import { createSettings } from "../config/settings.js";
import { createVite } from "../create-vite.js";
import { AstroError, AstroErrorData, AstroUserError, createSafeError, isAstroError } from "../errors/index.js";
import { createRoutesList } from "../routing/create-manifest.js";
import { ensureProcessNodeEnv } from "../util.js";
import { normalizePath } from "../viteUtils.js";
export default async function sync(inlineConfig, { fs, telemetry: _telemetry = false } = {}) {
ensureProcessNodeEnv("production");
const { astroConfig, userConfig } = await resolveConfig(inlineConfig ?? {}, "sync");
const logger = await loadOrCreateNodeLogger(astroConfig, inlineConfig ?? {});
if (_telemetry) {
telemetry.record(eventCliSession("sync", userConfig));
}
let settings = await createSettings(astroConfig, inlineConfig.logLevel, inlineConfig.root);
settings = await runHookConfigSetup({
command: "sync",
settings,
logger
});
await runHookConfigDone({
settings,
logger
});
return await syncInternal({
settings,
logger,
mode: "production",
fs,
force: inlineConfig.force,
command: "sync"
});
}
/**
* Clears the content layer and content collection cache, forcing a full rebuild.
*/
export async function clearContentLayerCache({ settings, logger, fs = fsMod, isDev }) {
if (getDataStoreChunkSize(settings) !== undefined) {
const dataStore = getDataStoreDir(settings, isDev);
if (fs.existsSync(dataStore)) {
logger.debug("content", "clearing data store");
await fs.promises.rm(dataStore, {
force: true,
recursive: true
});
await fs.promises.mkdir(dataStore, { recursive: true });
logger.warn("content", "data store cleared (force)");
}
} else {
const dataStore = getDataStoreFile(settings, isDev);
if (fs.existsSync(dataStore)) {
logger.debug("content", "clearing data store");
await fs.promises.rm(dataStore, { force: true });
logger.warn("content", "data store cleared (force)");
}
}
}
/**
* Generates TypeScript types for all Astro modules. This sets up a `src/env.d.ts` file for type inferencing,
* and defines the `astro:content` module for the Content Collections API.
*
* @experimental The JavaScript API is experimental
*/
export async function syncInternal({ mode, logger, fs = fsMod, settings, skip, force, command, watcher }) {
const isDev = command === "dev";
if (force) {
await clearContentLayerCache({
settings,
logger,
fs,
isDev
});
}
const timerStart = performance.now();
if (!skip?.content) {
// Create the Vite server once and keep it alive for both content config loading
// and content layer sync. This is needed because loaders may use dynamic imports
// which require the Vite server to be running. See https://github.com/withastro/astro/issues/12689
const tempViteServer = await createTempViteServer(settings, {
mode,
fs,
logger
});
try {
await syncContentCollections(settings, {
fs,
logger,
viteServer: tempViteServer
});
settings.timer.start("Sync content layer");
let store;
try {
const chunkSize = getDataStoreChunkSize(settings);
if (chunkSize !== undefined) {
const dataStoreDir = getDataStoreDir(settings, isDev);
store = await MutableDataStore.fromDir(dataStoreDir, chunkSize, logger);
} else {
const dataStoreFile = getDataStoreFile(settings, isDev);
store = await MutableDataStore.fromFile(dataStoreFile);
}
} catch (err) {
logger.error("content", err.message);
}
if (!store) {
logger.error("content", "Failed to load content store");
return;
}
const contentLayer = globalContentLayer.init({
settings,
logger,
store,
watcher
});
if (watcher) {
contentLayer.watchContentConfig();
}
await contentLayer.sync();
if (!skip?.cleanup) {
// Free up memory (usually in builds since we only need to use this once)
contentLayer.dispose();
}
settings.timer.end("Sync content layer");
} finally {
await tempViteServer.close();
}
} else {
const paths = getContentPaths(settings.config, fs, settings.config.legacy?.collectionsBackwardsCompat);
if (paths.config.exists || paths.liveConfig.exists) {
// We only create the reference, without a stub to avoid overriding the
// already generated types
settings.injectedTypes.push({ filename: CONTENT_TYPES_FILE });
}
}
syncAstroEnv(settings);
syncFonts(settings);
writeInjectedTypes(settings, fs);
logger.info("types", `Generated ${colors.dim(getTimeStat(timerStart, performance.now()))}`);
}
function getTsReference(type, value) {
return `/// <reference ${type}=${JSON.stringify(value)} />`;
}
const CLIENT_TYPES_REFERENCE = getTsReference("types", "astro/client");
function writeInjectedTypes(settings, fs) {
const references = [];
for (const { filename, content } of settings.injectedTypes) {
const filepath = fileURLToPath(new URL(filename, settings.dotAstroDir));
fs.mkdirSync(dirname(filepath), { recursive: true });
if (content) {
fs.writeFileSync(filepath, content, "utf-8");
}
references.push(normalizePath(relative(fileURLToPath(settings.dotAstroDir), filepath)));
}
const astroDtsContent = `${CLIENT_TYPES_REFERENCE}\n${references.map((reference) => getTsReference("path", reference)).join("\n")}`;
if (references.length === 0) {
fs.mkdirSync(settings.dotAstroDir, { recursive: true });
}
fs.writeFileSync(fileURLToPath(new URL("./types.d.ts", settings.dotAstroDir)), astroDtsContent, "utf-8");
}
/**
* Creates a temporary Vite server for use during content sync operations.
* This server is needed to load the content config and for loaders that use dynamic imports.
*/
async function createTempViteServer(settings, { mode, logger, fs }) {
const routesList = await createRoutesList({
settings,
fsMod: fs
}, logger, { dev: true });
const tempViteServer = await createServer(await createVite({
server: {
middlewareMode: true,
hmr: false,
watch: null,
ws: false
},
// Astro and integrations add `optimizeDeps.include` entries, which keep the dependency optimizer
// running here. A separate cache keeps its output from replacing the optimized dependencies
// of a dev server running in the same project: https://github.com/withastro/astro/issues/18449
cacheDir: fileURLToPath(new URL("./vite-sync/", settings.config.cacheDir)),
optimizeDeps: { noDiscovery: true },
ssr: { external: [] },
logLevel: "silent"
}, {
routesList,
settings: {
...settings,
// Prevent mutation by vite plugins during sync
buildOutput: undefined,
// Sync causes font resources and style hashes to be duplicated
injectedCsp: {
fontResources: new Set(),
styleHashes: []
}
},
logger,
mode,
command: "build",
fs,
sync: true
}));
// Patch `hot.send` to bubble up error events
// `hot.on('error')` does not fire for some reason
const hotSend = tempViteServer.environments.client.hot.send;
tempViteServer.environments.client.hot.send = (payload) => {
if (payload.type === "error") {
throw payload.err;
}
return hotSend(payload);
};
return tempViteServer;
}
/**
* Generate content collection types, and then returns the process exit signal.
*
* A non-zero process signal is emitted in case there's an error while generating content collection types.
*
* This should only be used when the callee already has an `AstroSetting`; otherwise, use `sync()` instead.
* @internal
*
* @param {SyncOptions} options
* @param {AstroSettings} settings Astro settings
* @param {typeof fsMod} options.fs The file system
* @param {LogOptions} options.logging Logging options
* @return {Promise<ProcessExit>}
*/
async function syncContentCollections(settings, { logger, fs, viteServer }) {
try {
const contentTypesGenerator = await createContentTypesGenerator({
contentConfigObserver: globalContentConfigObserver,
logger,
fs,
settings,
viteServer
});
await contentTypesGenerator.init();
const contentConfig = globalContentConfigObserver.get();
if (contentConfig.status === "error") {
throw contentConfig.error;
}
} catch (e) {
const safeError = createSafeError(e);
if (isAstroError(e)) {
throw e;
}
let configFile;
try {
const contentPaths = getContentPaths(settings.config, fs, settings.config.legacy?.collectionsBackwardsCompat);
if (contentPaths.config.exists) {
const matches = /\/(src\/.+)/.exec(contentPaths.config.url.href);
if (matches) {
configFile = matches[1];
}
}
} catch {}
const hint = AstroUserError.is(e) ? e.hint : AstroErrorData.GenerateContentTypesError.hint(configFile);
throw new AstroError({
...AstroErrorData.GenerateContentTypesError,
hint,
message: AstroErrorData.GenerateContentTypesError.message(safeError.message),
location: safeError.loc
}, { cause: e });
}
}