laravel-vite-plugin
Version:
Laravel plugin for Vite.
1,413 lines (1,398 loc) • 61.7 kB
JavaScript
// 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