vite-plugin-symfony
Version:
A Vite plugin to integrate easily Vite in your Symfony application
314 lines (263 loc) • 10 kB
text/typescript
import { loadEnv, type ResolvedConfig } from "vite";
import os from "node:os";
import path from "node:path";
import type { AddressInfo } from "net";
import { writeFileSync, rmSync, readdirSync } from "fs";
import { join } from "path";
import type { RenderedChunk, OutputChunk, OutputAsset, NormalizedOutputOptions, ExternalOption } from "rollup";
import { resolve, extname, relative } from "path";
import { DevServerUrl, FileInfos, ParsedInputs, HashAlgorithm, VitePluginSymfonyEntrypointsOptions } from "../types";
import { BinaryLike, createHash } from "node:crypto";
import { getInputPath } from "./pathMapping";
export const isWindows = os.platform() === "win32";
export function parseVersionString(str: string): [string, number, number, number] {
const [major, minor, patch] = str.split(".").map((nb) => parseInt(nb));
return [str, major ?? 0, minor ?? 0, patch ?? 0];
}
export function slash(p: string): string {
return p.replace(/\\/g, "/");
}
export function trimSlashes(str: string): string {
return str.replace(/^\/+|\/+$/g, "");
}
export function isSubdirectory(parent: string, child: string) {
parent = path.normalize(parent);
child = path.normalize(child);
if (parent == child) {
return false;
}
const parentDirs = parent.split(path.sep).filter((dir) => dir !== "");
const childDirs = child.split(path.sep).filter((dir) => dir !== "");
return parentDirs.every((dir, i) => childDirs[i] === dir);
}
export function normalizePath(id: string): string {
return path.posix.normalize(isWindows ? slash(id) : id);
}
export function getLegacyName(name: string) {
const ext = extname(name);
const endPos = ext.length !== 0 ? -ext.length : undefined;
name = name.slice(0, endPos) + "-legacy" + ext;
return name;
}
export function isIpv6(address: AddressInfo): boolean {
return (
address.family === "IPv6" ||
// In node >=18.0 <18.4 this was an integer value. This was changed in a minor version.
// See: https://github.com/laravel/vite-plugin/issues/103
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-next-line
address.family === 6
);
}
export const writeJson = (filePath: string, jsonData: any) => {
try {
writeFileSync(filePath, JSON.stringify(jsonData, null, 2));
} catch (err: any) {
throw new Error(`Error writing ${path.basename(filePath)}: ${err.message}`);
}
};
export const emptyDir = (dir: string) => {
const files = readdirSync(dir);
for (const file of files) {
rmSync(join(dir, file), { recursive: true });
}
};
export const INFO_PUBLIC_PATH = "/@vite/info";
/* not imported from vite because we don't want vite in package.json dependencies */
const FS_PREFIX = `/@fs/`;
const VALID_ID_PREFIX = `/@id/`;
const CLIENT_PUBLIC_PATH = `/@vite/client`;
const ENV_PUBLIC_PATH = `/@vite/env`;
const importQueryRE = /(\?|&)import=?(?:&|$)/;
export const isImportRequest = (url: string): boolean => importQueryRE.test(url);
const internalPrefixes = [FS_PREFIX, VALID_ID_PREFIX, CLIENT_PUBLIC_PATH, ENV_PUBLIC_PATH];
const InternalPrefixRE = new RegExp(`^(?:${internalPrefixes.join("|")})`);
export const isInternalRequest = (url: string): boolean => InternalPrefixRE.test(url);
const CSS_LANGS_RE = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/;
const cssModuleRE = new RegExp(`\\.module${CSS_LANGS_RE.source}`);
const commonjsProxyRE = /\?commonjs-proxy/;
const isCSSRequest = (request: string) => CSS_LANGS_RE.test(request);
const polyfillId = "\0vite/legacy-polyfills";
export function resolveDevServerUrl(
address: AddressInfo,
config: ResolvedConfig,
pluginOptions: VitePluginSymfonyEntrypointsOptions,
): DevServerUrl {
if (pluginOptions.originOverride) {
return pluginOptions.originOverride as DevServerUrl;
}
if (config.server?.origin) {
return config.server.origin as DevServerUrl;
}
const configHmrProtocol = typeof config.server.hmr === "object" ? config.server.hmr.protocol : null;
const clientProtocol = configHmrProtocol ? (configHmrProtocol === "wss" ? "https" : "http") : null;
const serverProtocol = config.server.https ? "https" : "http";
const protocol = clientProtocol ?? serverProtocol;
const configHmrHost = typeof config.server.hmr === "object" ? config.server.hmr.host : null;
const configHost = typeof config.server.host === "string" ? config.server.host : null;
const serverAddress = isIpv6(address) ? `[${address.address}]` : address.address;
const host = configHmrHost ?? pluginOptions.viteDevServerHostname ?? configHost ?? serverAddress;
const configHmrClientPort = typeof config.server.hmr === "object" ? config.server.hmr.clientPort : null;
const port = configHmrClientPort ?? address.port;
return `${protocol}://${host}:${port}`;
}
export const isAddressInfo = (x: string | AddressInfo | null | undefined): x is AddressInfo => typeof x === "object";
export const isCssEntryPoint = (chunk: RenderedChunk) => {
if (!chunk.isEntry) {
return false;
}
let isPureCssChunk = true;
const ids = Object.keys(chunk.modules);
for (const id of ids) {
if (!isCSSRequest(id) || cssModuleRE.test(id) || commonjsProxyRE.test(id)) {
isPureCssChunk = false;
}
}
if (isPureCssChunk) {
return chunk?.viteMetadata?.importedCss.size === 1;
}
return false;
};
export const getFileInfos = (
chunk: OutputChunk | OutputAsset,
inputRelPath: string,
pluginOptions: VitePluginSymfonyEntrypointsOptions,
): FileInfos => {
const alg = pluginOptions.sriAlgorithm;
if (chunk.type === "asset") {
if (chunk.fileName.endsWith(".css")) {
return {
css: [chunk.fileName],
hash: alg === false ? null : generateHash(chunk.source, alg),
inputRelPath,
outputRelPath: chunk.fileName,
type: "css",
};
} else {
return {
hash: alg === false ? null : generateHash(chunk.source, alg),
inputRelPath,
outputRelPath: chunk.fileName,
type: "asset",
};
}
} else if (chunk.type === "chunk") {
const { imports, dynamicImports, viteMetadata, fileName } = chunk;
return {
assets: Array.from(viteMetadata?.importedAssets ?? []),
css: Array.from(viteMetadata?.importedCss ?? []),
hash: alg === false ? null : generateHash(chunk.code, alg),
imports: imports,
inputRelPath,
js: [fileName],
outputRelPath: fileName,
preload: [],
dynamic: dynamicImports,
type: "js",
};
}
throw new Error(`Unknown chunktype ${(chunk as OutputChunk).type} for ${(chunk as OutputChunk).fileName}`);
};
function generateHash(source: BinaryLike, alg: HashAlgorithm) {
if (alg === false) {
return null;
}
const hash = createHash(alg).update(source).digest().toString("base64");
return `${alg}-${hash}`;
}
/**
* @description based on vite resolved config get an array of entrypoints and their type "css" | "js"
*/
export const prepareRollupInputs = (config: ResolvedConfig): ParsedInputs => {
const inputParsed: ParsedInputs = {};
for (const [entryName, inputRelPath] of Object.entries(config.build.rollupOptions.input ?? {})) {
const entryAbsolutePath = normalizePath(resolve(config.root, inputRelPath));
const extension = extname(inputRelPath);
const inputType =
[".css", ".scss", ".sass", ".less", ".styl", ".stylus", ".postcss"].indexOf(extension) !== -1 ? "css" : "js";
const entryRelativePath = normalizePath(relative(config.root, entryAbsolutePath));
inputParsed[entryName] = {
inputType,
inputRelPath: entryRelativePath,
};
}
return inputParsed;
};
/**
* @description used when generateBundle.
* if chunk doesn't have a facadeModuleId his inputRelPath can be retrieve with inputRelPath2outputRelPath
*/
export const getInputRelPath = (
chunk: OutputAsset | OutputChunk,
options: NormalizedOutputOptions,
config: ResolvedConfig,
): string => {
if (chunk.type === "asset" || !chunk.facadeModuleId) {
const inputRelPath = getInputPath(chunk.fileName);
if (inputRelPath) {
return inputRelPath;
}
return `_${chunk.fileName}`;
}
if ([polyfillId].indexOf(chunk.facadeModuleId) !== -1) {
return chunk.facadeModuleId.replace(/\0/g, "");
}
let inputRelPath = normalizePath(path.relative(config.root, chunk.facadeModuleId));
/* when we generate legacy files, format === 'system'. after format is other value like 'es' */
if (options.format === "system" && !chunk.name.includes("-legacy")) {
inputRelPath = getLegacyName(inputRelPath);
}
return inputRelPath.replace(/\0/g, "");
};
/**
* vite/src/node/build.ts
*/
export function resolveUserExternal(
user: ExternalOption,
id: string,
parentId: string | null,
isResolved: boolean,
): boolean | null | void {
if (typeof user === "function") {
return user(id, parentId ?? undefined, isResolved);
} else if (Array.isArray(user)) {
return user.some((test) => isExternal(id, test));
} else {
return isExternal(id, user);
}
}
function isExternal(id: string, test: string | RegExp) {
if (typeof test === "string") {
return id === test;
} else {
return test.test(id);
}
}
export function extractExtraEnvVars(
mode: string,
envDir: string,
exposedEnvVars: string[],
define?: Record<string, string>,
) {
const allVars = loadEnv(mode, envDir, "");
const availableKeys = Object.keys(allVars).filter((key) => exposedEnvVars.indexOf(key) !== -1);
const extraDefine = Object.fromEntries(
availableKeys.map((key) => [`import.meta.env.${key}`, JSON.stringify(allVars[key])]),
);
return {
...extraDefine,
...(define ?? {}),
};
}
export function normalizeConfig(config: ResolvedConfig) {
const result = JSON.stringify(config, function (k, v) {
if (k === "plugins" && Array.isArray(v)) {
return v.filter((v) => v.name).map((v) => v.name);
}
if (typeof v === "function") {
return undefined;
}
return v;
});
return result;
}