UNPKG

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
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 }); } }