UNPKG

laravel-vite-plugin

Version:
1,413 lines (1,398 loc) 61.7 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/cache.ts import fs from "fs"; import path from "path"; import { createHash } from "crypto"; var DEFAULT_CACHE_DIR = "node_modules/.cache/laravel-vite-plugin/fonts"; function resolveCacheDir(projectRoot, cacheDir) { const dir = cacheDir ?? path.resolve(projectRoot, DEFAULT_CACHE_DIR); if (!fs.existsSync(dir)) { fs.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 = path.join(cacheDir, key); return fs.existsSync(filePath) ? fs.readFileSync(filePath) : void 0; } function readCacheText(cacheDir, key) { return readCache(cacheDir, key)?.toString("utf-8"); } function writeCache(cacheDir, key, data) { fs.writeFileSync(path.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/config.ts import fs2 from "fs"; import path2 from "path"; import { glob } from "tinyglobby"; var FORMATS = [ { type: "woff2", extension: ".woff2", keyword: "woff2" }, { type: "woff", extension: ".woff", keyword: "woff" }, { type: "ttf", extension: ".ttf", keyword: "truetype" }, { type: "otf", extension: ".otf", keyword: "opentype" }, { type: "eot", extension: ".eot", keyword: "embedded-opentype" } ]; var FORMAT_PREFERENCE = FORMATS.map((f) => f.type); var FORMAT_MAP = Object.fromEntries( FORMATS.map((f) => [f.extension, f.type]) ); var FORMAT_KEYWORD_MAP = Object.fromEntries( FORMATS.map((f) => [f.type, f.keyword]) ); function formatKeyword(format) { return FORMAT_KEYWORD_MAP[format]; } 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 = path2.basename(filePath, path2.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 = path2.basename(filePath, path2.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 = path2.basename(filePath, path2.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(path2.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 = path2.isAbsolute(src) ? src : path2.resolve(projectRoot, src); if (/[*?{]/.test(src)) { return discoverFromGlob(family, src, projectRoot); } if (fs2.existsSync(absoluteSrc) && fs2.statSync(absoluteSrc).isDirectory()) { return discoverFromDirectory(family, src, absoluteSrc); } if (fs2.existsSync(absoluteSrc) && fs2.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 "${path2.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 = path2.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 = path2.isAbsolute(src) ? src : path2.resolve(projectRoot, src); if (!fs2.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("${formatKeyword(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("${formatKeyword(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/dev-server.ts import fs3 from "fs"; // 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/dev-server.ts 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 (!fs3.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 = fs3.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/fallback.ts import { pathToFileURL } from "url"; 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"; } function resolveMetricsSource(fontSource) { return /^[a-z][a-z\d+.-]*:\/\//i.test(fontSource) ? fontSource : pathToFileURL(fontSource); } async function generateFallbackMetrics(fontSource, warn = () => void 0) { let fontaine; try { fontaine = await import("fontaine"); } catch { warn('Optimized font fallbacks require the optional "fontaine" package. Install it, or set "optimizedFallbacks: false" on your fonts to disable the feature.'); return void 0; } try { const metrics = await fontaine.readMetrics(resolveMetricsSource(fontSource)); if (!metrics) { warn(`Unable to read font metrics from [${fontSource}]. Skipping optimized fallback generation for this font.`); return void 0; } const { ascent, descent, lineGap, unitsPerEm, xWidthAvg, category } = metrics; if (ascent == null || descent == null || lineGap == null || unitsPerEm == null) { warn(`Unable to read font metrics from [${fontSource}]. Skipping optimized fallback generation for this font.`); 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 (e) { warn(`Unable to generate an optimized font fallback from [${fontSource}]: ${e instanceof Error ? e.message : String(e)}`); return void 0; } } // 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/providers/resolve-fontsource.ts import fs4 from "fs"; import path3 from "path"; import { createRequire } from "module"; // src/fonts/css-parser.ts var FORMAT_ALIASES = Object.fromEntries( FORMATS.flatMap((format) => [ [format.type, format.type], [format.keyword, format.type] ]) ); function parseFontFaceCss(css) { const results = []; const ruleRegex = /(?:\/\*\s*([\w-]+)\s*\*\/\s*)?@font-face\s*\{([^}]+)\}/g; let match; while ((match = ruleRegex.exec(css)) !== null) { const subset = match[1]; const block = match[2]; const face = parseFontFaceBlock(block); if (face) { results.push(subset ? { ...face, subset } : 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-fontsource.ts function buildCssFilePaths(definition, packageDir, packageName) { const paths = []; for (const weight of definition.weights) { for (const style of definition.styles) { const cssFileName = style === "italic" ? `${weight}-italic.css` : `${weight}.css`; const cssFilePath = path3.join(packageDir, cssFileName); if (!fs4.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} and style "${style}" are available.` ); } paths.push({ fileName: cssFileName, filePath: cssFilePath, weight: String(weight), style }); } } return paths; } function matchesFontsourceSubset(face, subset) { const suffix = `-${subset}-${String(face.weight)}-${face.style}`; return face.src.some((src) => { const stem = path3.basename(src.url).replace(/\.(?:woff2?|ttf|otf|eot)$/i, ""); return stem.endsWith(suffix); }); } 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 cssFile of cssFilePaths) { const faces = parseFontFaceCss(fs4.readFileSync(cssFile.filePath, "utf-8")); const matchedSubsets = /* @__PURE__ */ new Set(); for (const face of faces) { const subset = definition.subsets.find((subset2) => matchesFontsourceSubset(face, subset2)); if (!subset) { continue; } matchedSubsets.add(subset); const files = face.src.map((src) => { const absolutePath = path3.resolve(path3.dirname(cssFile.filePath), src.url); if (!fs4.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 }); } for (const subset of definition.subsets) { if (!matchedSubsets.has(subset)) { throw new Error( `laravel-vite-plugin: Fontsource subset "${subset}" not found in "${cssFile.fileName}" in package "${packageName}" for font "${definition.family}". Check that weight ${cssFile.weight}, style "${cssFile.style}", and subset "${subset}" are available.` ); } } } 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/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 requestedFaces = faces.filter( (face) => !face.subset || definition.subsets.includes(face.subset) ); if (requestedFaces.length === 0) { const available = [...new Set(faces.map((face) => face.subset).filter(Boolean))]; throw new Error( `laravel-vite-plugin: ${definition.provider} returned no @font-face rules matching the requested subsets [${definition.subsets.join(", ")}] for "${definition.family}". Available subsets: [${available.join(", ")}].` ); } const variants = []; for (const face of requestedFaces) { 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/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, warn) { const fallbackMap = /* @__PURE__ */ new Map(); const warned = /* @__PURE__ */ new Set(); const warnOnce = (message) => { if (!warned.has(message)) { warned.add(message); warn(message); } }; 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, warnOnce); if (metrics) { fallbackMap.set(family.alias, { fallbackFamily: `${family.family} fallback`, metrics }); } } for (const family of families) { if (family.optimizedFallbacks && !fallbackMap.has(family.alias)) { family.optimizedFallbacks = false; } } return fallbackMap; } function buildSourceWeightMap(families) { const sourceWeightMap = /* @__PURE__ */ new Map(); for (const family of families) { for (const variant of family.variants) { for (const file of variant.files) { if (!sourceWeightMap.has(file.source)) { sourceWeightMap.set(file.source, /* @__PURE__ */ new Map()); } const styleWeights = sourceWeightMap.get(file.source); if (!styleWeights.has(variant.style)) { styleWeights.set(variant.style, /* @__PURE__ */ new Set()); } styleWeights.get(variant.style).add(String(variant.weight)); } } } return sourceWeightMap; } function buildFontAssetName(family, variant, file, sourceWeightMap) { const slug = familyToSlug(family.family); const ext = file.format === "woff2" ? ".woff2" : `.${file.format}`; const weight = (sourceWeightMap.get(file.source)?.get(variant.style)?.size ?? 0) > 1 ? "variable" : String(variant.weight).replace(/\s+/g, "-"); return `${slug}-${weight}-${variant.style}${ext}`; } function emitFontAssets(families, emitFile) { const fileRefMap = /* @__PURE__ */ new Map(); const sourceWeightMap = buildSourceWeightMap(families); 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 name = buildFontAssetName(family, variant, file, sourceWeightMap); const ref = emitFile({ type: "asset", name, source }); fileRefMap.set(file.source, ref); } } } return fileRefMap; } function resolvePublicAssetUrl(base, fileName) { const normalizedBase = base.endsWith("/") ? base : `${base}/`; return `${normalizedBase}${fileName}`; } function resolveCssAssetUrl(base, fileName, assetsDir) { if (base !== "./" && base !== "") { return resolvePublicAssetUrl(base, fileName); } const relativePath = path4.posix.relative(assetsDir.replace(/\/+$/, ""), fileName); return relativePath.startsWith(".") ? relativePath : `./${relativePath}`; } 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) { 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, (message) => this.warn(message)); }, generateBundle() { if (resolvedConfig.command !== "build" || resolvedFamilies.length === 0) { return; } assertFileRefsResolved(resolvedFamilies, fontsFileRefMap); const relativeFilePathMap = /* @__PURE__ */ new Map(); const cssFilePathMap = /* @__PURE__ */ new Map(); const publicFilePathMap = /* @__PURE__ */ new Map(); for (const [source, ref] of fontsFileRefMap) { const fileName = this.getFileName(ref); relativeFilePathMap.set(source, fileName); cssFilePathMap.set(source, resolveCssAssetUrl(resolvedConfig.base, fileName, resolvedConfig.build.assetsDir)); publicFilePathMap.set(source, resolvePublicAssetUrl(resolvedConfig.base, fileName)); } const finalCss = generateFontCss(resolvedFamilies, cssFilePathMap, fontsFallbackMap); const { familyStyles, variables } = generateFamilyStyles(resolvedFamilies, publicFilePathMap, 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, (message) => server.config.logger.warn(`[laravel:fonts] ${message}`)); 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), ...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