UNPKG

laravel-vite-plugin

Version:
1,412 lines (1,399 loc) 57 kB
// src/index.ts import fs6 from "fs"; import os from "os"; import { fileURLToPath } from "url"; import path5 from "path"; import { globSync } from "tinyglobby"; import colors from "picocolors"; import { loadEnv, createLogger, defaultAllowedOrigins } from "vite"; import fullReload from "vite-plugin-full-reload"; // src/fonts/plugin.ts import fs5 from "fs"; import path4 from "path"; // src/fonts/config.ts import fs from "fs"; import path from "path"; import { glob } from "tinyglobby"; var FORMATS = [ { type: "woff2", extension: ".woff2" }, { type: "woff", extension: ".woff" }, { type: "ttf", extension: ".ttf" }, { type: "otf", extension: ".otf" }, { type: "eot", extension: ".eot" } ]; var FORMAT_PREFERENCE = FORMATS.map((f) => f.type); var FORMAT_MAP = Object.fromEntries( FORMATS.map((f) => [f.extension, f.type]) ); var SUPPORTED_EXTENSIONS = FORMATS.map((f) => f.extension); var SUPPORTED_GLOB = `*.{${SUPPORTED_EXTENSIONS.map((ext) => ext.slice(1)).join(",")}}`; var DEFAULT_WEIGHT = 400; var DEFAULT_STYLE = "normal"; var WEIGHT_PATTERNS = [ ["extrabold", 800], ["ultrabold", 800], ["semibold", 600], ["demibold", 600], ["extralight", 200], ["ultralight", 200], ["hairline", 100], ["thin", 100], ["light", 300], ["regular", 400], ["normal", 400], ["medium", 500], ["black", 900], ["heavy", 900], ["bold", 700] ]; function splitStem(stem) { return stem.split(/[-_]/).filter(Boolean); } function stripStyleSuffix(segment) { return segment.replace(/(?:italic|it|oblique)$/i, ""); } function inferWeightFromFilename(filePath) { const stem = path.basename(filePath, path.extname(filePath)); const segments = splitStem(stem); for (let i = segments.length - 1; i >= 0; i--) { const raw = segments[i]; const stripped = stripStyleSuffix(raw); const candidate = stripped || raw; const lc = candidate.toLowerCase(); const match = WEIGHT_PATTERNS.find(([pattern]) => lc === pattern) ?? WEIGHT_PATTERNS.find( ([pattern]) => lc.length > pattern.length && lc.endsWith(pattern) ); if (match) { return match[1]; } const numMatch = candidate.match(/(?:^|[^\d])([1-9]00)$/); if (numMatch) { return parseInt(numMatch[1], 10); } } return DEFAULT_WEIGHT; } function inferStyleFromFilename(filePath) { const stem = path.basename(filePath, path.extname(filePath)); const segments = splitStem(stem); for (let i = segments.length - 1; i >= 0; i--) { const seg = segments[i]; const lc = seg.toLowerCase(); if (["it", "italic"].includes(lc) || /italic$/i.test(seg)) { return "italic"; } if (lc.endsWith("it") && lc.length > 2) { const prefix = lc.slice(0, -2); if (WEIGHT_PATTERNS.some(([pattern]) => prefix === pattern || prefix.endsWith(pattern))) { return "italic"; } } if (lc === "oblique" || /oblique$/i.test(seg)) { return "oblique"; } } return DEFAULT_STYLE; } function inferLocalVariantFromFilename(filePath) { return { weight: inferWeightFromFilename(filePath), style: inferStyleFromFilename(filePath) }; } function looksLikeVariableFontFilename(filePath) { const stem = path.basename(filePath, path.extname(filePath)); return /\[.+\]/.test(stem); } async function discoverFromGlob(family, src, projectRoot) { const files = await glob(src, { cwd: projectRoot, absolute: true }); const supported = files.filter((f) => SUPPORTED_EXTENSIONS.includes(path.extname(f).toLowerCase())); if (supported.length === 0) { throw new Error( `laravel-vite-plugin: Local font "${family}" shorthand src "${src}" matched no supported font files.` ); } return supported; } async function discoverFromDirectory(family, src, absoluteSrc) { const files = await glob(`**/${SUPPORTED_GLOB}`, { cwd: absoluteSrc, absolute: true }); if (files.length === 0) { throw new Error( `laravel-vite-plugin: Local font "${family}" directory "${src}" contains no supported font files.` ); } return files; } async function discoverFontFiles(family, src, projectRoot) { const absoluteSrc = path.isAbsolute(src) ? src : path.resolve(projectRoot, src); if (/[*?{]/.test(src)) { return discoverFromGlob(family, src, projectRoot); } if (fs.existsSync(absoluteSrc) && fs.statSync(absoluteSrc).isDirectory()) { return discoverFromDirectory(family, src, absoluteSrc); } if (fs.existsSync(absoluteSrc) && fs.statSync(absoluteSrc).isFile()) { return [absoluteSrc]; } throw new Error( `laravel-vite-plugin: Local font "${family}" shorthand src "${src}" does not exist (resolved to "${absoluteSrc}").` ); } function rejectVariableFontFiles(family, files) { for (const file of files) { if (looksLikeVariableFontFilename(file)) { throw new Error( `laravel-vite-plugin: Local font "${family}" shorthand discovered a variable font file "${path.basename(file)}". Variable fonts require explicit "variants" with a weight range instead of shorthand "src".` ); } } } function groupFilesByVariant(files) { const groups = /* @__PURE__ */ new Map(); for (const file of files) { const { weight, style } = inferLocalVariantFromFilename(file); const key = `${weight}:${style}`; if (!groups.has(key)) { groups.set(key, { weight, style, files: [] }); } groups.get(key).files.push({ source: file, format: inferFormat(file) }); } for (const group of groups.values()) { group.files.sort( (a, b) => FORMAT_PREFERENCE.indexOf(a.format) - FORMAT_PREFERENCE.indexOf(b.format) ); } return Array.from(groups.values()).sort((a, b) => { const wA = typeof a.weight === "number" ? a.weight : parseInt(String(a.weight), 10); const wB = typeof b.weight === "number" ? b.weight : parseInt(String(b.weight), 10); if (wA !== wB) { return wA - wB; } return a.style.localeCompare(b.style); }); } async function resolveLocalShorthandVariants(definition, localConfig, projectRoot) { const discoveredFiles = await discoverFontFiles(definition.family, localConfig.src, projectRoot); discoveredFiles.sort(); rejectVariableFontFiles(definition.family, discoveredFiles); return groupFilesByVariant(discoveredFiles); } function familyToSlug(family) { return family.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""); } function buildResolvedFamily(definition, variants) { return { family: definition.family, alias: definition.alias, variable: definition.variable, display: definition.display, optimizedFallbacks: definition.optimizedFallbacks, fallbacks: definition.fallbacks, preload: definition.preload, provider: definition.provider, variants }; } function inferFormat(filePath) { const ext = path.extname(filePath).toLowerCase(); const format = FORMAT_MAP[ext]; if (!format) { throw new Error( `laravel-vite-plugin: Unsupported font file format "${ext}" for file "${filePath}". Supported formats: ${SUPPORTED_EXTENSIONS.join(", ")}` ); } return format; } function throwIfEmptyString(value, message) { if (typeof value !== "string" || value.trim() === "") { throw new Error(message); } } function validateFontDefinition(definition) { throwIfEmptyString( definition.family, "laravel-vite-plugin: Font family name must be a non-empty string." ); throwIfEmptyString( definition.alias, `laravel-vite-plugin: Font "${definition.family}" has an invalid or empty alias.` ); if (definition.variable !== void 0) { throwIfEmptyString( definition.variable, `laravel-vite-plugin: Font "${definition.family}" has an invalid or empty variable name.` ); if (!definition.variable.startsWith("--")) { throw new Error( `laravel-vite-plugin: Font "${definition.family}" variable "${definition.variable}" must start with "--".` ); } } if (definition.provider !== "local") { if (definition.styles.includes("oblique")) { throw new Error( `laravel-vite-plugin: Font "${definition.family}" uses provider "${definition.provider}", which does not support the "oblique" style. Use "italic" instead, or load the family with the local() provider.` ); } return; } const localConfig = definition._local; if (!localConfig) { throw new Error( `laravel-vite-plugin: Local font "${definition.family}" must specify either "src" or "variants".` ); } if ("src" in localConfig && "variants" in localConfig) { throw new Error( `laravel-vite-plugin: Local font "${definition.family}" cannot specify both "src" and "variants".` ); } if ("src" in localConfig) { if (typeof localConfig.src !== "string" || localConfig.src.trim() === "") { throw new Error( `laravel-vite-plugin: Local font "${definition.family}" has an invalid or empty "src".` ); } return; } const variants = localConfig.variants; if (!variants || variants.length === 0) { throw new Error( `laravel-vite-plugin: Local font "${definition.family}" must specify at least one variant.` ); } for (const v of variants) { const sources = Array.isArray(v.src) ? v.src : [v.src]; if (sources.length === 0 || sources.some((s) => typeof s !== "string" || s.trim() === "")) { throw new Error( `laravel-vite-plugin: Local font "${definition.family}" has a variant with an invalid or empty src.` ); } } } function mergeFontDefinitions(fonts) { const byAlias = /* @__PURE__ */ new Map(); const result = []; for (const font of fonts) { const existing = byAlias.get(font.alias); if (!existing) { const clone = { ...font }; if (font._local) { clone._local = "variants" in font._local ? { variants: [...font._local.variants] } : { ...font._local }; } byAlias.set(font.alias, clone); result.push(clone); continue; } if (existing.provider !== font.provider) { throw new Error( `laravel-vite-plugin: Cannot merge font definitions for alias "${font.alias}": provider mismatch ("${existing.provider}" vs "${font.provider}").` ); } if (existing.variable !== font.variable) { throw new Error( `laravel-vite-plugin: Cannot merge font definitions for alias "${font.alias}": variable mismatch ("${existing.variable}" vs "${font.variable}").` ); } if (existing.display !== font.display) { throw new Error( `laravel-vite-plugin: Cannot merge font definitions for alias "${font.alias}": display mismatch ("${existing.display}" vs "${font.display}").` ); } if (JSON.stringify(existing.fallbacks) !== JSON.stringify(font.fallbacks)) { throw new Error( `laravel-vite-plugin: Cannot merge font definitions for alias "${font.alias}": fallbacks mismatch.` ); } if (JSON.stringify(existing.preload) !== JSON.stringify(font.preload)) { throw new Error( `laravel-vite-plugin: Cannot merge font definitions for alias "${font.alias}": preload mismatch.` ); } const weightSet = new Set(existing.weights.map(String)); for (const w of font.weights) { if (!weightSet.has(String(w))) { existing.weights.push(w); weightSet.add(String(w)); } } const styleSet = new Set(existing.styles); for (const s of font.styles) { if (!styleSet.has(s)) { existing.styles.push(s); styleSet.add(s); } } const subsetSet = new Set(existing.subsets); for (const s of font.subsets) { if (!subsetSet.has(s)) { existing.subsets.push(s); subsetSet.add(s); } } if (existing._local && font._local) { if ("variants" in existing._local && "variants" in font._local) { existing._local.variants.push(...font._local.variants); } else { throw new Error( `laravel-vite-plugin: Cannot merge font definitions for alias "${font.alias}": incompatible local font shapes (one uses "src" and the other uses "variants").` ); } } } return result; } function validateFontsConfig(fonts) { const merged = mergeFontDefinitions(fonts); const aliases = /* @__PURE__ */ new Set(); const variables = /* @__PURE__ */ new Set(); for (const font of merged) { validateFontDefinition(font); if (aliases.has(font.alias)) { throw new Error( `laravel-vite-plugin: Duplicate font alias "${font.alias}". Each alias must be unique. Use the "alias" option to disambiguate.` ); } aliases.add(font.alias); if (variables.has(font.variable)) { throw new Error( `laravel-vite-plugin: Duplicate CSS variable "${font.variable}". Use the "variable" option to set a unique variable name.` ); } variables.add(font.variable); } return merged; } function resolveLocalExplicitVariants(definition, localConfig, projectRoot) { const variants = []; for (const v of localConfig.variants) { const sources = Array.isArray(v.src) ? v.src : [v.src]; const files = []; for (const src of sources) { const absolutePath = path.isAbsolute(src) ? src : path.resolve(projectRoot, src); if (!fs.existsSync(absolutePath)) { throw new Error( `laravel-vite-plugin: Local font file not found: "${src}" (resolved to "${absolutePath}") for font "${definition.family}".` ); } files.push({ source: absolutePath, format: inferFormat(absolutePath) }); } files.sort( (a, b) => FORMAT_PREFERENCE.indexOf(a.format) - FORMAT_PREFERENCE.indexOf(b.format) ); const firstSrc = Array.isArray(v.src) ? v.src[0] : v.src; const inferred = inferLocalVariantFromFilename(firstSrc); variants.push({ weight: v.weight ?? inferred.weight, style: v.style ?? inferred.style, files }); } return variants; } async function resolveLocalVariants(definition, projectRoot) { const localConfig = definition._local; return "variants" in localConfig ? resolveLocalExplicitVariants(definition, localConfig, projectRoot) : resolveLocalShorthandVariants(definition, localConfig, projectRoot); } async function resolveLocalFont(definition, projectRoot) { return buildResolvedFamily(definition, await resolveLocalVariants(definition, projectRoot)); } // src/fonts/css.ts function generateSrc(files, filePathMap) { return files.map((file) => { const url = filePathMap.get(file.source) ?? file.source; return `url("${url}") format("${file.format}")`; }).join(",\n "); } function generateFontFace(family, filePathMap) { const rules = []; for (const variant of family.variants) { const rangedFiles = variant.files.filter((f) => f.unicodeRange); const nonRangedFiles = variant.files.filter((f) => !f.unicodeRange); for (const file of rangedFiles) { const fileSrc = `url("${filePathMap.get(file.source) ?? file.source}") format("${file.format}")`; rules.push([ "@font-face {", ` font-family: "${family.family}";`, ` font-style: ${variant.style};`, ` font-weight: ${String(variant.weight)};`, ` font-display: ${family.display};`, ` src: ${fileSrc};`, ` unicode-range: ${file.unicodeRange};`, "}" ].join("\n")); } if (nonRangedFiles.length > 0) { const src = generateSrc(nonRangedFiles, filePathMap); rules.push([ "@font-face {", ` font-family: "${family.family}";`, ` font-style: ${variant.style};`, ` font-weight: ${String(variant.weight)};`, ` font-display: ${family.display};`, ` src: ${src};`, "}" ].join("\n")); } } return rules.join("\n\n"); } function generateFallbackFontFace(fallbackFamily, metrics) { return [ "@font-face {", ` font-family: "${fallbackFamily}";`, ` src: local("${metrics.localFont}");`, ` ascent-override: ${metrics.ascentOverride};`, ` descent-override: ${metrics.descentOverride};`, ` line-gap-override: ${metrics.lineGapOverride};`, ` size-adjust: ${metrics.sizeAdjust};`, "}" ].join("\n"); } function generateFontClassForFamily(family) { return `.${family.variable.replace(/^--/, "")} { font-family: var(${family.variable}); }`; } function generateFontClasses(families) { return families.map((f) => generateFontClassForFamily(f)).join("\n\n"); } function familyVariableDeclaration(family) { const parts = [`"${family.family}"`]; if (family.optimizedFallbacks) { parts.push(`"${family.family} fallback"`); } if (family.fallbacks.length > 0) { parts.push(...family.fallbacks); } return `${family.variable}: ${parts.join(", ")};`; } function generateCssVariables(families) { const lines = families.map((f) => ` ${familyVariableDeclaration(f)}`).join("\n"); return [":root {", lines, "}"].join("\n"); } function generateCssVariablesMap(families) { const map = {}; for (const family of families) { map[family.alias] = familyVariableDeclaration(family); } return map; } function buildFamilyCss(family, filePathMap, fallbackMap) { let css = generateFontFace(family, filePathMap); if (family.optimizedFallbacks && fallbackMap?.has(family.alias)) { const fb = fallbackMap.get(family.alias); css += "\n\n" + generateFallbackFontFace(fb.fallbackFamily, fb.metrics); } return css; } function generateFamilyStyles(families, filePathMap, fallbackMap) { const familyStyles = {}; for (const family of families) { familyStyles[family.alias] = buildFamilyCss(family, filePathMap, fallbackMap) + "\n\n" + generateFontClassForFamily(family); } return { familyStyles, variables: generateCssVariablesMap(families) }; } function generateFontCss(families, filePathMap, fallbackMap) { const parts = families.map((f) => buildFamilyCss(f, filePathMap, fallbackMap)); parts.push(generateCssVariables(families)); parts.push(generateFontClasses(families)); return parts.join("\n\n") + "\n"; } // src/fonts/types.ts var FORMAT_MIME = { woff2: "font/woff2", woff: "font/woff", ttf: "font/ttf", otf: "font/otf", eot: "application/vnd.ms-fontobject" }; // src/fonts/manifest.ts function variantKey(weight, style) { return `${weight}:${style}`; } function shouldPreload(family, variant) { if (family.preload === false || family.preload === true) { return family.preload; } return family.preload.some( (sel) => String(sel.weight) === String(variant.weight) && (sel.style ?? "normal") === variant.style ); } function resolveEntries(families, pathMap, pathKey) { const preloads = []; const familyEntries = {}; for (const family of families) { const variants = {}; for (const variant of family.variants) { const files = variant.files.map((f) => ({ [pathKey]: pathMap.get(f.source), format: f.format, unicodeRange: f.unicodeRange })); const key = variantKey(variant.weight, variant.style); if (variants[key]) { variants[key].files.push(...files); } else { variants[key] = { files }; } if (shouldPreload(family, variant)) { for (const f of variant.files) { if (f.format === "woff2") { preloads.push({ alias: family.alias, family: family.family, weight: variant.weight, style: variant.style, [pathKey]: pathMap.get(f.source), as: "font", type: FORMAT_MIME[f.format], crossorigin: "anonymous" }); } } } } familyEntries[family.alias] = { family: family.family, variable: family.variable, fallbackFamily: family.optimizedFallbacks ? `${family.family} fallback` : void 0, fallbacks: family.fallbacks.length > 0 ? family.fallbacks : void 0, variants }; } const seen = /* @__PURE__ */ new Set(); const deduped = preloads.filter((p) => { const key = p.file ?? p.url ?? ""; if (seen.has(key)) { return false; } seen.add(key); return true; }); return { preloads: deduped, familyEntries }; } function buildManifest(families, cssFile, filePathMap, familyStyles, variables) { const { preloads, familyEntries } = resolveEntries(families, filePathMap, "file"); return { version: 1, style: { file: cssFile, familyStyles, variables }, preloads, families: familyEntries }; } function buildDevManifest(families, inlineCss, urlMap, familyStyles, variables) { const { preloads, familyEntries } = resolveEntries(families, urlMap, "url"); return { version: 1, style: { inline: inlineCss, familyStyles, variables }, preloads, families: familyEntries }; } // src/fonts/cache.ts import fs2 from "fs"; import path2 from "path"; import { createHash } from "crypto"; var DEFAULT_CACHE_DIR = "node_modules/.cache/laravel-vite-plugin/fonts"; function resolveCacheDir(projectRoot, cacheDir) { const dir = cacheDir ?? path2.resolve(projectRoot, DEFAULT_CACHE_DIR); if (!fs2.existsSync(dir)) { fs2.mkdirSync(dir, { recursive: true }); } return dir; } function cacheKey(input) { return createHash("sha256").update(input).digest("hex").slice(0, 16); } function readCache(cacheDir, key) { const filePath = path2.join(cacheDir, key); return fs2.existsSync(filePath) ? fs2.readFileSync(filePath) : void 0; } function readCacheText(cacheDir, key) { return readCache(cacheDir, key)?.toString("utf-8"); } function writeCache(cacheDir, key, data) { fs2.writeFileSync(path2.join(cacheDir, key), data); } async function fetchOrThrow(url, headers) { const response = await fetch(url, { headers }); if (!response.ok) { throw new Error( `laravel-vite-plugin: Failed to fetch "${url}": ${response.status} ${response.statusText}` ); } return response; } async function fetchAndCache(url, cacheDir, headers) { const key = cacheKey(url); const cached = readCache(cacheDir, key); if (cached) { return cached; } const response = await fetchOrThrow(url, headers); const buffer = Buffer.from(await response.arrayBuffer()); writeCache(cacheDir, key, buffer); return buffer; } async function fetchTextAndCache(url, cacheDir, headers) { const key = cacheKey(url + ":text"); const cached = readCacheText(cacheDir, key); if (cached) { return cached; } const response = await fetchOrThrow(url, headers); const text = await response.text(); writeCache(cacheDir, key, text); return text; } // src/fonts/css-parser.ts var FORMAT_ALIASES = { truetype: "ttf", opentype: "otf", "embedded-opentype": "eot", ...Object.fromEntries(FORMATS.map((f) => [f.type, f.type])) }; function parseFontFaceCss(css) { const results = []; const ruleRegex = /@font-face\s*\{([^}]+)\}/g; let match; while ((match = ruleRegex.exec(css)) !== null) { const block = match[1]; const face = parseFontFaceBlock(block); if (face) { results.push(face); } } return results; } function parseFontFaceBlock(block) { const family = extractDescriptor(block, "font-family"); const style = extractDescriptor(block, "font-style"); const weight = extractDescriptor(block, "font-weight"); const src = extractDescriptor(block, "src"); const unicodeRange = extractDescriptor(block, "unicode-range"); const display = extractDescriptor(block, "font-display"); if (!family || !src) { return null; } const cleanFamily = family.replace(/['"]/g, "").trim(); const parsedSrc = parseSrcDescriptor(src); if (parsedSrc.length === 0) { return null; } return { family: cleanFamily, style: style ?? "normal", weight: parseWeight(weight ?? "400"), src: parsedSrc, unicodeRange: unicodeRange ?? void 0, display: display ?? void 0 }; } function extractDescriptor(block, name) { const match = new RegExp(`${name}\\s*:\\s*([^;]+)`, "i").exec(block); return match ? match[1].trim() : null; } function parseSrcDescriptor(src) { const results = []; const urlRegex = /url\(["']?([^"')]+)["']?\)\s*format\(["']?([^"')]+)["']?\)/g; let match; while ((match = urlRegex.exec(src)) !== null) { const url = match[1]; const format = normalizeFormat(match[2]); if (format) { results.push({ url, format }); } } if (results.length === 0) { const simpleUrlRegex = /url\(["']?([^"')]+)["']?\)/g; while ((match = simpleUrlRegex.exec(src)) !== null) { const url = match[1]; const format = inferFormatFromUrl(url); if (format) { results.push({ url, format }); } } } return results; } function parseWeight(weight) { const trimmed = weight.trim(); return /^\d+$/.test(trimmed) ? parseInt(trimmed, 10) : trimmed; } function normalizeFormat(format) { return FORMAT_ALIASES[format.toLowerCase()] ?? null; } function inferFormatFromUrl(url) { const ext = url.match(/\.([^.]+)$/)?.[1]; return ext ? normalizeFormat(ext) : null; } // src/fonts/providers/resolve-remote.ts var WOFF2_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"; function buildCss2Url(baseUrl, definition) { const family = definition.family.replace(/ /g, "+"); const weights = definition.weights; const styles = definition.styles; const hasItalic = styles.includes("italic"); const axes = hasItalic ? ["ital", "wght"] : ["wght"]; const tuples = /* @__PURE__ */ new Set(); for (const weight of weights) { for (const style of styles) { if (hasItalic) { const ital = style === "italic" ? "1" : "0"; tuples.add(`${ital},${weight}`); } else { tuples.add(`${weight}`); } } } const axisStr = axes.join(","); const tupleStr = [...tuples].sort().join(";"); return `${baseUrl}?family=${family}:${axisStr}@${tupleStr}&display=${definition.display}&subset=${definition.subsets.join(",")}`; } async function resolveRemoteVariants(definition, cacheDir, baseUrl) { const url = buildCss2Url(baseUrl, definition); const css = await fetchTextAndCache(url, cacheDir, { "User-Agent": WOFF2_USER_AGENT }); const faces = parseFontFaceCss(css); if (faces.length === 0) { throw new Error( `laravel-vite-plugin: ${definition.provider} returned no @font-face rules for "${definition.family}". Check the family name and requested weights/styles.` ); } const variants = []; for (const face of faces) { const files = []; for (const src of face.src) { await fetchAndCache(src.url, cacheDir); files.push({ source: `${cacheDir}/${cacheKey(src.url)}`, format: src.format, unicodeRange: face.unicodeRange }); } variants.push({ weight: face.weight, style: face.style, files }); } return variants; } async function resolveRemoteFont(definition, cacheDir, baseUrl) { const variants = await resolveRemoteVariants(definition, cacheDir, baseUrl); return buildResolvedFamily(definition, variants); } // src/fonts/providers/resolve-fontsource.ts import fs3 from "fs"; import path3 from "path"; import { createRequire } from "module"; function buildCssFilePaths(definition, packageDir, packageName) { const paths = []; for (const weight of definition.weights) { for (const style of definition.styles) { for (const subset of definition.subsets) { const cssFileName = style === "italic" ? `${subset}-${weight}-italic.css` : `${subset}-${weight}.css`; const cssFilePath = path3.join(packageDir, cssFileName); if (!fs3.existsSync(cssFilePath)) { throw new Error( `laravel-vite-plugin: Fontsource CSS file not found: "${cssFileName}" in package "${packageName}" for font "${definition.family}". Check that weight ${weight}, style "${style}", and subset "${subset}" are available.` ); } paths.push(cssFilePath); } } } return paths; } function resolveFontsourceVariants(definition, projectRoot) { const packageName = definition._fontsource?.package ?? `@fontsource/${familyToSlug(definition.family)}`; let packageDir; try { const require2 = createRequire(path3.join(projectRoot, "package.json")); packageDir = path3.dirname( require2.resolve(`${packageName}/package.json`) ); } catch { throw new Error( `laravel-vite-plugin: Fontsource package "${packageName}" not found. Install it with: npm install ${packageName}` ); } const variants = []; const cssFilePaths = buildCssFilePaths(definition, packageDir, packageName); for (const cssFilePath of cssFilePaths) { const faces = parseFontFaceCss(fs3.readFileSync(cssFilePath, "utf-8")); for (const face of faces) { const files = face.src.map((src) => { const absolutePath = path3.resolve(path3.dirname(cssFilePath), src.url); if (!fs3.existsSync(absolutePath)) { throw new Error( `laravel-vite-plugin: Font file referenced by Fontsource not found: "${absolutePath}" for font "${definition.family}".` ); } return { source: absolutePath, format: src.format, unicodeRange: face.unicodeRange }; }); variants.push({ weight: face.weight, style: face.style, files }); } } if (variants.length === 0) { throw new Error( `laravel-vite-plugin: No font variants resolved from Fontsource package "${packageName}" for font "${definition.family}".` ); } return variants; } function resolveFontsourceFont(definition, projectRoot) { return buildResolvedFamily(definition, resolveFontsourceVariants(definition, projectRoot)); } // src/fonts/fallback.ts var FALLBACK_METRICS = { "sans-serif": { localFont: "Arial", ascent: 1854, descent: -434, lineGap: 67, unitsPerEm: 2048, xWidthAvg: 904 }, serif: { localFont: "Times New Roman", ascent: 1825, descent: -443, lineGap: 87, unitsPerEm: 2048, xWidthAvg: 832 }, monospace: { localFont: "Courier New", ascent: 1705, descent: -615, lineGap: 0, unitsPerEm: 2048, xWidthAvg: 1229 } }; function resolveFallbackCategory(category) { const validCategories = ["sans-serif", "serif", "monospace"]; return validCategories.includes(category) ? category : "sans-serif"; } async function generateFallbackMetrics(fontSource) { try { const fontaine = await import("fontaine"); const metrics = await fontaine.readMetrics(fontSource); if (!metrics) { return void 0; } const { ascent, descent, lineGap, unitsPerEm, xWidthAvg, category } = metrics; if (ascent == null || descent == null || lineGap == null || unitsPerEm == null) { return void 0; } const fallback = FALLBACK_METRICS[resolveFallbackCategory(category)]; const sizeAdjust = xWidthAvg ? xWidthAvg / unitsPerEm / (fallback.xWidthAvg / fallback.unitsPerEm) : 1; const adjustedEm = unitsPerEm * sizeAdjust; return { localFont: fallback.localFont, ascentOverride: `${(ascent / adjustedEm * 100).toFixed(2)}%`, descentOverride: `${(Math.abs(descent) / adjustedEm * 100).toFixed(2)}%`, lineGapOverride: `${(lineGap / adjustedEm * 100).toFixed(2)}%`, sizeAdjust: `${(sizeAdjust * 100).toFixed(2)}%` }; } catch { return void 0; } } // src/fonts/dev-server.ts import fs4 from "fs"; var FONT_ROUTE_PREFIX = "/__laravel_vite_plugin__/fonts"; function buildDevUrlMap(families, devServerUrl) { const urlMap = /* @__PURE__ */ new Map(); for (const family of families) { for (const variant of family.variants) { for (const file of variant.files) { if (!urlMap.has(file.source)) { const hash = cacheKey(file.source); const ext = file.format === "woff2" ? ".woff2" : `.${file.format}`; urlMap.set(file.source, `${devServerUrl}${FONT_ROUTE_PREFIX}/${hash}${ext}`); } } } } return urlMap; } function createFontMiddleware() { let lookup = /* @__PURE__ */ new Map(); function update(families) { const newLookup = /* @__PURE__ */ new Map(); for (const family of families) { for (const variant of family.variants) { for (const file of variant.files) { const hash = cacheKey(file.source); newLookup.set(hash, { source: file.source, format: file.format }); } } } lookup = newLookup; } function middleware(req, res, next) { if (!req.url?.startsWith(FONT_ROUTE_PREFIX + "/")) { return next(); } const fileName = req.url.slice(FONT_ROUTE_PREFIX.length + 1); const hash = fileName.replace(/\.[^.]+$/, ""); const entry = lookup.get(hash); if (!entry) { res.statusCode = 404; res.end("Font not found"); return; } if (!fs4.existsSync(entry.source)) { res.statusCode = 404; res.end("Font file not found on disk"); return; } const mime = FORMAT_MIME[entry.format] ?? "application/octet-stream"; res.setHeader("Content-Type", mime); res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Cache-Control", "no-store"); const stream = fs4.createReadStream(entry.source); stream.on("error", (err) => { if (!res.headersSent) { res.statusCode = 500; } res.destroy(err); }); res.on("close", () => { stream.destroy(); }); stream.pipe(res); } return { middleware, update }; } // src/fonts/plugin.ts var REMOTE_CSS_URLS = { google: "https://fonts.googleapis.com/css2", bunny: "https://fonts.bunny.net/css2" }; async function resolveFontFamilies(fonts, projectRoot, cacheDir) { const families = []; for (const definition of fonts) { const remoteUrl = REMOTE_CSS_URLS[definition.provider]; if (remoteUrl) { families.push( await resolveRemoteFont(definition, cacheDir, remoteUrl) ); } switch (definition.provider) { case "fontsource": families.push(resolveFontsourceFont(definition, projectRoot)); break; case "local": families.push(await resolveLocalFont(definition, projectRoot)); break; } } return families; } async function buildFallbackMap(families) { const fallbackMap = /* @__PURE__ */ new Map(); for (const family of families) { if (!family.optimizedFallbacks) { continue; } const firstFile = family.variants[0]?.files[0]; if (!firstFile) { continue; } const metrics = await generateFallbackMetrics(firstFile.source); if (metrics) { fallbackMap.set(family.alias, { fallbackFamily: `${family.family} fallback`, metrics }); } } return fallbackMap; } function emitFontAssets(families, emitFile) { const fileRefMap = /* @__PURE__ */ new Map(); for (const family of families) { for (const variant of family.variants) { for (const file of variant.files) { if (fileRefMap.has(file.source)) { continue; } const source = fs5.readFileSync(file.source); const slug = familyToSlug(family.family); const ext = file.format === "woff2" ? ".woff2" : `.${file.format}`; const name = `${slug}-${variant.weight}-${variant.style}${ext}`; const ref = emitFile({ type: "asset", name, source }); fileRefMap.set(file.source, ref); } } } return fileRefMap; } function assertFileRefsResolved(families, fileRefMap) { for (const family of families) { for (const variant of family.variants) { for (const file of variant.files) { if (!fileRefMap.has(file.source)) { throw new Error( `laravel-vite-plugin: Missing emitted asset for font "${family.family}" (source "${file.source}").` ); } } } } } function resolveFontsPlugin(fonts, hotFile, buildDirectory) { if (!fonts || fonts.length === 0) { return []; } const mergedFonts = validateFontsConfig(fonts); let resolvedConfig; let resolvedFamilies = []; let cacheDir; let hotManifestPath; let fontsFileRefMap; let fontsFallbackMap; return [{ name: "laravel:fonts", enforce: "post", configResolved(config) { resolvedConfig = config; cacheDir = resolveCacheDir(config.root); hotManifestPath = path4.resolve( path4.dirname(hotFile), "fonts-manifest.dev.json" ); }, async buildStart() { if (resolvedConfig.command !== "build") { return; } resolvedFamilies = await resolveFontFamilies(mergedFonts, resolvedConfig.root, cacheDir); if (resolvedFamilies.length === 0) { return; } fontsFileRefMap = emitFontAssets(resolvedFamilies, (opts) => this.emitFile(opts)); fontsFallbackMap = await buildFallbackMap(resolvedFamilies); }, generateBundle() { if (resolvedConfig.command !== "build" || resolvedFamilies.length === 0) { return; } assertFileRefsResolved(resolvedFamilies, fontsFileRefMap); const relativeFilePathMap = /* @__PURE__ */ new Map(); const absoluteFilePathMap = /* @__PURE__ */ new Map(); for (const [source, ref] of fontsFileRefMap) { const fileName = this.getFileName(ref); relativeFilePathMap.set(source, fileName); absoluteFilePathMap.set(source, `/${buildDirectory}/${fileName}`); } const finalCss = generateFontCss(resolvedFamilies, absoluteFilePathMap, fontsFallbackMap); const { familyStyles, variables } = generateFamilyStyles(resolvedFamilies, absoluteFilePathMap, fontsFallbackMap); const cssRef = this.emitFile({ type: "asset", name: "fonts.css", source: finalCss }); const cssFileName = this.getFileName(cssRef); const manifest = buildManifest(resolvedFamilies, cssFileName, relativeFilePathMap, familyStyles, variables); this.emitFile({ type: "asset", fileName: "fonts-manifest.json", source: JSON.stringify(manifest, null, 2) }); }, configureServer(server) { const projectRoot = resolvedConfig.root; const fontMiddleware = createFontMiddleware(); server.middlewares.use(fontMiddleware.middleware); server.httpServer?.once("listening", async () => { try { resolvedFamilies = await resolveFontFamilies(mergedFonts, projectRoot, cacheDir); if (resolvedFamilies.length === 0) { return; } const devServerUrl = fs5.existsSync(hotFile) ? fs5.readFileSync(hotFile, "utf-8").trim() : `http://localhost:${server.config.server.port ?? 5173}`; fontMiddleware.update(resolvedFamilies); const fallbackMap = await buildFallbackMap(resolvedFamilies); const urlMap = buildDevUrlMap(resolvedFamilies, devServerUrl); const css = generateFontCss(resolvedFamilies, urlMap, fallbackMap); const { familyStyles, variables } = generateFamilyStyles(resolvedFamilies, urlMap, fallbackMap); const manifest = buildDevManifest(resolvedFamilies, css, urlMap, familyStyles, variables); const hotManifestDir = path4.dirname(hotManifestPath); if (!fs5.existsSync(hotManifestDir)) { fs5.mkdirSync(hotManifestDir, { recursive: true }); } fs5.writeFileSync(hotManifestPath, JSON.stringify(manifest, null, 2)); } catch (e) { server.config.logger.error(`[laravel:fonts] ${e.message}`); } }); const onExit = () => { try { fs5.rmSync(hotManifestPath, { force: true }); } catch { } }; process.on("exit", onExit); server.httpServer?.once("close", () => { onExit(); process.removeListener("exit", onExit); }); } }]; } // src/index.ts var exitHandlersBound = false; var refreshPaths = [ "app/Livewire/**", "app/View/Components/**", "lang/**", "resources/lang/**", "resources/views/**", "routes/**" ].filter((path6) => fs6.existsSync(path6.replace(/\*\*$/, ""))); var logger = createLogger("info", { prefix: "[laravel-vite-plugin]" }); function laravel(config) { const pluginConfig = resolvePluginConfig(config); return [ resolveLaravelPlugin(pluginConfig), ...resolveAssetPlugin(pluginConfig.assets), ...resolveFontsPlugin(pluginConfig.fonts, pluginConfig.hotFile, pluginConfig.buildDirectory), ...resolveFullReloadConfig(pluginConfig) ]; } function resolveLaravelPlugin(pluginConfig) { let viteDevServerUrl; let resolvedConfig; let userConfig; const defaultAliases = { "@": "/resources/js" }; return { name: "laravel", enforce: "post", config: (config, { command, mode }) => { userConfig = config; const ssr = !!userConfig.build?.ssr; const env = loadEnv(mode, userConfig.envDir || process.cwd(), ""); const assetUrl = env.ASSET_URL ?? ""; const serverConfig = command === "serve" ? resolveDevelopmentEnvironmentServerConfig(pluginConfig.detectTls) ?? resolveEnvironmentServerConfig(env) : void 0; ensureCommandShouldRunInEnvironment(command, env); return { base: userConfig.base ?? (command === "build" ? resolveBase(pluginConfig, assetUrl) : ""), publicDir: userConfig.publicDir ?? false, build: { manifest: userConfig.build?.manifest ?? (ssr ? false : "manifest.json"), ssrManifest: userConfig.build?.ssrManifest ?? (ssr ? "ssr-manifest.json" : false), outDir: userConfig.build?.outDir ?? resolveOutDir(pluginConfig, ssr), rolldownOptions: { input: userConfig.build?.rolldownOptions?.input ?? userConfig.build?.rollupOptions?.input ?? resolveInput(pluginConfig, ssr) }, assetsInlineLimit: userConfig.build?.assetsInlineLimit ?? 0 }, server: { origin: userConfig.server?.origin ?? "http://__laravel_vite_placeholder__.test", cors: userConfig.server?.cors ?? { origin: userConfig.server?.origin ?? [ defaultAllowedOrigins, ...env.APP_URL ? [env.APP_URL] : [], // * (APP_URL="http://my-app.tld") /^https?:\/\/.*\.test(:\d+)?$/ // Valet / Herd (SCHEME://*.test:PORT) ] }, ...process.env.LARAVEL_SAIL ? { host: userConfig.server?.host ?? "0.0.0.0", port: userConfig.server?.port ?? (env.VITE_PORT ? parseInt(env.VITE_PORT) : 5173), strictPort: userConfig.server?.strictPort ?? true } : void 0, ...serverConfig ? { host: userConfig.server?.host ?? serverConfig.host, hmr: userConfig.server?.hmr === false ? false : { ...serverConfig.hmr, ...userConfig.server?.hmr === true ? {} : userConfig.server?.hmr }, https: userConfig.server?.https ?? serverConfig.https } : void 0 }, resolve: { alias: Array.isArray(userConfig.resolve?.alias) ? [ ...userConfig.resolve?.alias ?? [], ...Object.keys(defaultAliases).map((alias) => ({ find: alias, replacement: defaultAliases[alias] })) ] : { ...defaultAliases, ...userConfig.resolve?.alias } }, ssr: { noExternal: noExternalInertiaHelpers(userConfig) } }; }, configResolved(config) { resolvedConfig = config; }, transform(code) { if (resolvedConfig.command === "serve") { code = code.replace(/http:\/\/__laravel_vite_placeholder__\.test/g, viteDevServerUrl); return pluginConfig.transformOnServe(code, viteDevServerUrl); } }, configureServer(server) { const envDir = resolvedConfig.envDir || process.cwd(); const appUrl = loadEnv(resolvedConfig.mode, envDir, "APP_URL").APP_URL ?? "undefined"; server.httpServer?.once("listening", () => { const address = server.httpServer?.address(); const isAddressInfo = (x) => typeof x === "object"; if (isAddressInfo(address)) { viteDevServerUrl = userConfig.server?.origin ? userConfig.server.origin : resolveDevServerUrl(address, server.config, userConfig); const hotFileParentDirectory = path5.dirname(pluginConfig.hotFile); if (!fs6.existsSync(hotFileParentDirectory)) { fs6.mkdirSync(hotFileParentDirectory, { recursive: true }); setTimeout(() => { logger.info(`Hot file directory created ${colors.dim(fs6.realpathSync(hotFileParentDirectory))}`, { clear: true, timestamp: true }); }, 200); } fs6.writeFileSync(pluginConfig.hotFile, `${viteDevServerUrl}${server.config.base.replace(/\/$/, "")}`); setTimeout(() => { server.config.logger.info(` ${colors.red(`${colors.bold("LARAVEL")} ${laravelVersion()}`)} ${colors.dim("plugin")} ${colors.bold(`v${pluginVersion()}`)}`); server.config.logger.info(""); server.config.logger.info(` ${colors.green("\u279C")} ${colors.bold("APP_URL")}: ${colors.cyan(appUrl.replace(/:(\d+)/, (_, port) => `:${colors.bold(port)}`))}`); if (typeof resolvedConfig.server.https === "object" && typeof resolvedConfig.server.https.key === "string") { if (resolvedConfig.server.https.key.startsWith(herdMacConfigPath()) || resolvedConfig.server.https.key.startsWith(herdWindowsConfigPath())) { server.config.logger.info(` ${colors.green("\u279C")} Using Herd certificate to secure Vite.`); } if (resolvedConfig.server.https.key.startsWith(valetMacConfigPath()) || resolvedConfig.server.https.key.startsWith(valetLinuxConfigPath())) { server.config.logger.info(` ${colors.green("\u279C")} Using Valet certificate to secure Vite.`); } } }, 100); } }); if (!exitHandlersBound) { const clean = () => { if (fs6.existsSync(pluginConfig.hotFile)) { fs6.rmSync(pluginConfig.hotFile); } }; process.on("exit", clean); process.on("SIGINT", () => process.exit()); process.on("SIGTERM", () => process.exit()); process.on("SIGHUP", () => process.exit()); exitHandlersBound = true; } return () => server.middlewares.use((req, res, next) => { if (req.url === "/index.html") { res.statusCode = 404; res.end( fs6.readFileSync(path5.join(dirname(), "dev-server-index.html")).toString().replace(/{{ APP_URL }}/g, appUrl) ); } next(); }); } }; } function ensureCommandShouldRunInEnvironment(command, env) { if (command === "build" || env.LARAVEL_BYPASS_ENV_CHECK === "1") { return; } if (typeof env.LARAVEL_VAPOR !== "undefined") { throw Error("You should not run the Vite HMR server on Vapor. You should build your assets for production instead. To disable this ENV check you may set LARAVEL_BYPASS_ENV_CHECK=1"); } if (typeof env.LARAVEL_FORGE !== "undefined") { throw Error("You should not run the Vite HMR server in your Forge deployment script. You should build your assets for production instead. To disable this ENV check you may set LARAVEL_BYPASS_ENV_CHECK=1"); } if (typeof env.LARAVEL_ENVOYER !== "undefined") { throw Error("You should not run the Vite HMR server in your Envoyer hook. You should build your assets for production instead. To disable this ENV check you may set LARAVEL_BYPASS_ENV_CHECK=1"); } if (typeof env.CI !== "undefined") { throw Error("You should not run the Vite HMR server in CI environments. You should build your assets for production instead. To disable this ENV check you may set LARAVEL_BYPASS_ENV_CHECK=1"); } } function laravelVersion() { try { const composer = JSON.parse(fs6.readFileSync("composer.lock").toString()); return composer.packages?.find((composerPackage) => composerPackage.name === "laravel/framework")?.version ?? ""; } catch { return ""; } } function pluginVersion() { try { return JSON.parse(fs6.readFileSync(path5.join(dirname(), "../package.json")).toString())?.version; } catch { return ""; } } function resolvePluginConfig(config) { if (typeof config === "undefined") { throw new Error("laravel-vite-plugin: missing configuration."); } if (typeof config === "string" || Array.isArray(config)) { config = { input: config, ssr: config }; } if (typeof config.input === "undefined") { throw new Error('laravel-vite-plugin: missing configuration for "input".'); } if (typeof config.publicDirectory === "string") { config.publicDirectory = config.publicDirectory.trim().replace(/^\/+/, ""); if (config.publicDirectory === "") { throw new Error("laravel-vite-plugin: publicDirectory must be a subdirectory. E.g. 'public'."); } } if (typeof config.buildDirectory === "string") { config.buildDirectory = config.buildDirectory.trim().replace(/^\/+/, "").replace(/\/+$/, ""); if (config.buildDirectory === "") { throw new Error("laravel-vite-plugin: buildDirectory must be a subdirectory. E.g. 'build'."); } } if (typeof config.ssrOutputDirectory === "string") { config.ssrOutputDirectory = config.ssrOutputDirectory.trim().replace(/^\/+/, "").replace(/\/+$/, ""); } if (config.refresh === true) { config.refresh = [{ paths: refreshPaths }]; } return { input: config.input, publicDirectory: config.publicDirectory ?? "public", buildDirectory: config.buildDirectory ?? "build", ssr: config.ssr ?? config.input, ssrOutputDirectory: config.ssrOutputDirectory ?? "bootstrap/ssr", refresh: config.refresh ?? false, hotFile: config.hotFile ??