UNPKG

astro

Version:

Astro is a modern site builder with web best practices, performance, and DX front-of-mind.

240 lines (239 loc) • 8.81 kB
import { mkdirSync, writeFileSync } from "node:fs"; import { readFile } from "node:fs/promises"; import { isAbsolute } from "node:path"; import { fileURLToPath } from "node:url"; import { removeTrailingForwardSlash } from "@astrojs/internal-helpers/path"; import { collectErrorMetadata } from "../../core/errors/dev/utils.js"; import { AstroError, AstroErrorData, isAstroError } from "../../core/errors/index.js"; import { formatErrorMessage } from "../../core/messages.js"; import { getClientOutputDirectory } from "../../prerender/utils.js"; import { CACHE_DIR, DEFAULTS, RESOLVED_VIRTUAL_MODULE_ID, URL_PREFIX, VIRTUAL_MODULE_ID } from "./constants.js"; import { createMinifiableCssRenderer } from "./implementations/css-renderer.js"; import { createDataCollector } from "./implementations/data-collector.js"; import { createAstroErrorHandler } from "./implementations/error-handler.js"; import { createCachedFontFetcher } from "./implementations/font-fetcher.js"; import { createCapsizeFontMetricsResolver } from "./implementations/font-metrics-resolver.js"; import { createFontTypeExtractor } from "./implementations/font-type-extractor.js"; import { createXxHasher } from "./implementations/hasher.js"; import { createRequireLocalProviderUrlResolver } from "./implementations/local-provider-url-resolver.js"; import { createBuildRemoteFontProviderModResolver, createDevServerRemoteFontProviderModResolver } from "./implementations/remote-font-provider-mod-resolver.js"; import { createRemoteFontProviderResolver } from "./implementations/remote-font-provider-resolver.js"; import { createFsStorage } from "./implementations/storage.js"; import { createSystemFallbacksProvider } from "./implementations/system-fallbacks-provider.js"; import { createLocalUrlProxyContentResolver, createRemoteUrlProxyContentResolver } from "./implementations/url-proxy-content-resolver.js"; import { createUrlProxy } from "./implementations/url-proxy.js"; import { orchestrate } from "./orchestrate.js"; function fontsPlugin({ settings, sync, logger }) { if (!settings.config.experimental.fonts) { return { name: "astro:fonts:fallback", resolveId(id) { if (id === VIRTUAL_MODULE_ID) { return RESOLVED_VIRTUAL_MODULE_ID; } }, load(id) { if (id === RESOLVED_VIRTUAL_MODULE_ID) { return { code: "" }; } } }; } const baseUrl = removeTrailingForwardSlash(settings.config.base) + URL_PREFIX; let fontFileDataMap = null; let consumableMap = null; let isBuild; let fontFetcher = null; let fontTypeExtractor = null; const cleanup = () => { consumableMap = null; fontFileDataMap = null; fontFetcher = null; }; async function initialize({ cacheDir, modResolver, cssRenderer }) { const { root } = settings.config; const hasher = await createXxHasher(); const errorHandler = createAstroErrorHandler(); const remoteFontProviderResolver = createRemoteFontProviderResolver({ root, modResolver, errorHandler }); const pathsToWarn = /* @__PURE__ */ new Set(); const localProviderUrlResolver = createRequireLocalProviderUrlResolver({ root, intercept: (path) => { if (path.startsWith(fileURLToPath(settings.config.publicDir))) { if (pathsToWarn.has(path)) { return; } pathsToWarn.add(path); logger.warn( "assets", `Found a local font file ${JSON.stringify(path)} in the \`public/\` folder. To avoid duplicated files in the build output, move this file into \`src/\`` ); } } }); const storage = createFsStorage({ base: cacheDir }); const systemFallbacksProvider = createSystemFallbacksProvider(); fontFetcher = createCachedFontFetcher({ storage, errorHandler, fetch, readFile }); const fontMetricsResolver = createCapsizeFontMetricsResolver({ fontFetcher, cssRenderer }); fontTypeExtractor = createFontTypeExtractor({ errorHandler }); const res = await orchestrate({ families: settings.config.experimental.fonts, hasher, remoteFontProviderResolver, localProviderUrlResolver, storage, cssRenderer, systemFallbacksProvider, fontMetricsResolver, fontTypeExtractor, createUrlProxy: ({ local, ...params }) => { const dataCollector = createDataCollector(params); const contentResolver = local ? createLocalUrlProxyContentResolver({ errorHandler }) : createRemoteUrlProxyContentResolver(); return createUrlProxy({ base: baseUrl, contentResolver, hasher, dataCollector, fontTypeExtractor }); }, defaults: DEFAULTS }); fontFileDataMap = res.fontFileDataMap; consumableMap = res.consumableMap; } return { name: "astro:fonts", config(_, { command }) { isBuild = command === "build"; }, async buildStart() { if (isBuild) { await initialize({ cacheDir: new URL(CACHE_DIR, settings.config.cacheDir), modResolver: createBuildRemoteFontProviderModResolver(), cssRenderer: createMinifiableCssRenderer({ minify: true }) }); } }, async configureServer(server) { await initialize({ // In dev, we cache fonts data in .astro so it can be easily inspected and cleared cacheDir: new URL(CACHE_DIR, settings.dotAstroDir), modResolver: createDevServerRemoteFontProviderModResolver({ server }), cssRenderer: createMinifiableCssRenderer({ minify: false }) }); const localPaths = [...fontFileDataMap.values()].filter(({ url }) => isAbsolute(url)).map((v) => v.url); server.watcher.on("change", (path) => { if (localPaths.includes(path)) { logger.info("assets", "Font file updated"); server.restart(); } }); server.watcher.on("unlink", (path) => { if (localPaths.includes(path)) { logger.warn( "assets", `The font file ${JSON.stringify(path)} referenced in your config has been deleted. Restore the file or remove this font from your configuration if it is no longer needed.` ); } }); server.middlewares.use(URL_PREFIX, async (req, res, next) => { if (!req.url) { return next(); } const hash = req.url.slice(1); const associatedData = fontFileDataMap?.get(hash); if (!associatedData) { return next(); } res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"); res.setHeader("Pragma", "no-cache"); res.setHeader("Expires", 0); try { const data = await fontFetcher.fetch({ hash, ...associatedData }); res.setHeader("Content-Length", data.length); res.setHeader("Content-Type", `font/${fontTypeExtractor.extract(hash)}`); res.end(data); } catch (err) { logger.error("assets", "Cannot download font file"); if (isAstroError(err)) { logger.error( "SKIP_FORMAT", formatErrorMessage(collectErrorMetadata(err), logger.level() === "debug") ); } res.statusCode = 500; res.end(); } }); }, resolveId(id) { if (id === VIRTUAL_MODULE_ID) { return RESOLVED_VIRTUAL_MODULE_ID; } }, load(id) { if (id === RESOLVED_VIRTUAL_MODULE_ID) { return { code: `export const fontsData = new Map(${JSON.stringify(Array.from(consumableMap?.entries() ?? []))})` }; } }, async buildEnd() { if (sync || settings.config.experimental.fonts.length === 0) { cleanup(); return; } try { const dir = getClientOutputDirectory(settings); const fontsDir = new URL("." + baseUrl, dir); try { mkdirSync(fontsDir, { recursive: true }); } catch (cause) { throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause }); } if (fontFileDataMap) { logger.info("assets", "Copying fonts..."); await Promise.all( Array.from(fontFileDataMap.entries()).map(async ([hash, associatedData]) => { const data = await fontFetcher.fetch({ hash, ...associatedData }); try { writeFileSync(new URL(hash, fontsDir), data); } catch (cause) { throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause }); } }) ); } } finally { cleanup(); } } }; } export { fontsPlugin };