UNPKG

@openscript/unplugin-favicons

Version:

Generate favicons for your project with caching for blazing fast rebuilds.

321 lines 15.3 kB
import { join } from "node:path"; import { colorize } from "consola/utils"; import MagicString from "magic-string"; import mime from "mime/lite"; import { findStaticImports, parseStaticImport } from "mlly"; import { PLUGIN_NAME } from "./const.js"; import generateFavicons from "./generate/generate-favicons.js"; import parseHtml from "./parse-html.js"; import updateManifest from "./update-manifest.js"; import consola from "./utils/consola.js"; import findHtmlRspackPlugin from "./utils/find-html-rspack-plugin.js"; import findHtmlWebpackPlugin from "./utils/find-html-webpack-plugin.js"; import formatDuration from "./utils/format-duration.js"; import Oracle from "./utils/oracle.js"; // eslint-disable-next-line sonarjs/cognitive-complexity const unpluginFactory = (options, meta) => { const config = { cache: true, inject: true, ...options }; const oracle = new Oracle(config?.projectRoot); const developer = oracle.guessDeveloper(); let viteCommand; config.favicons = { appDescription: oracle.guessDescription(), appName: oracle.guessAppName(), developerName: developer.name, developerURL: developer.url, version: oracle.guessVersion(), ...config.favicons, }; let base = ""; let frontendFramework; let parsedHtml = []; let runtimeExports; let injectionStatus = "ENABLED"; if (config.inject !== true) { injectionStatus = "DISABLED"; } if (meta.framework === "esbuild") { base = "/"; consola.warn(`html injection in esbuild is not supported, injection was disabled.`); injectionStatus = "NOT_SUPPORTED"; } /** * Called during the `buildStart` phase to add assets to the compilation * and update the HTML returned by favicons for injection later. */ const emitFiles = (context, response) => { const { files, html, images } = response; // Map each image returned from `favicons` into an object containing its // original name and the resolved name (ie: name it will have in the output // bundle). Additionally, emit the image into the Vite context. const emittedImages = images.map((faviconImage) => { const filePath = join(config.outputPath ?? "", faviconImage.name); context.emitFile({ fileName: filePath, source: faviconImage.contents, type: "asset", }); consola.debug(`emit ${colorize("green", String(filePath))}`); return { name: faviconImage.name, resolvedName: filePath }; }); // Map each file returned from `favicons` into an object containing its // original name and the resolved name (ie: name it will have in the output // bundle). Additionally, emit the file into the Vite context. const emittedFiles = files.map((faviconFile) => { const filePath = join(config.outputPath ?? "", faviconFile.name); // If the file from favicons is a manifest, we need to update its file // names to those emitted by Vite rather than the original asset names. // For all other files, we keep the original contents. const source = faviconFile.name.includes("manifest") ? updateManifest(emittedImages, faviconFile.contents) : faviconFile.contents; context.emitFile({ fileName: filePath, source, type: "asset", }); consola.debug(`emit ${colorize("green", filePath)}`); return { name: faviconFile.name, resolvedName: filePath }; }); // Transform paths in emitted HTML tags using the filenames generated by // Vite. parsedHtml = parseHtml([...emittedFiles, ...emittedImages], html, base); runtimeExports = { files: emittedFiles, images: emittedImages, metadata: parsedHtml.map((tag) => tag.fragment).join(""), }; }; const serveMap = new Map([]); /** * Called during the `buildStart` phase to add assets to the compilation * and update the HTML returned by favicons for injection later. */ const serveFiles = (context, response) => { const { files, html, images } = response; // Map each image returned from `favicons` into an object containing its // original name and the resolved name (ie: name it will have in the output // bundle). Additionally, emit the image into the Vite context. const servedImages = images.map((faviconImage) => { const filePath = join(config.outputPath ?? "", faviconImage.name); serveMap.set(filePath, faviconImage.contents); consola.debug(`serve ${colorize("green", String(filePath))}`); return { name: faviconImage.name, resolvedName: filePath }; }); // Map each file returned from `favicons` into an object containing its // original name and the resolved name (ie: name it will have in the output // bundle). Additionally, serve the file into the Vite context. const servedFiles = files.map((faviconFile) => { const filePath = join(config.outputPath ?? "", faviconFile.name); // If the file from favicons is a manifest, we need to update its file // names to those served by Vite rather than the original asset names. // For all other files, we keep the original contents. const source = faviconFile.name.includes("manifest") ? updateManifest(servedImages, faviconFile.contents) : faviconFile.contents; serveMap.set(filePath, source); consola.debug(`serve ${colorize("green", filePath)}`); return { name: faviconFile.name, resolvedName: filePath }; }); // Transform paths in served HTML tags using the filenames generated by // Vite. parsedHtml = parseHtml([...servedFiles, ...servedImages], html, base); runtimeExports = { files: servedFiles, images: servedImages, metadata: parsedHtml.map((tag) => tag.fragment).join(""), }; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any const injectHtmlPlugin = (compilation, plugin) => { if (plugin) { // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access plugin.getHooks(compilation)?.alterAssetTags?.tapPromise(PLUGIN_NAME, async (htmlPluginData) => { // Skip if a custom injectFunction returns false or if // the htmlWebpackPlugin options includes a `favicons: false` flag const isInjectionAllowed = htmlPluginData.plugin.userOptions.favicon !== false && htmlPluginData.plugin.userOptions.favicons !== false; if (!isInjectionAllowed) { return htmlPluginData; } htmlPluginData.assetTags.meta.push(...parsedHtml.map((tag) => { return { attributes: tag.attrs, meta: { plugin: PLUGIN_NAME }, tagName: tag.tag, voidTag: true, }; })); return htmlPluginData; }); } }; return { apply: (_, environment) => { viteCommand = environment.command; return true; }, async buildStart() { const startTime = Date.now(); const response = await generateFavicons({ ...config, favicons: { ...config.favicons, path: config?.favicons?.path ?? config.outputPath, }, }); consola.info(`Generated assets in ${formatDuration(Date.now() - startTime)}.`); if (viteCommand === undefined || viteCommand === "build") { emitFiles(this, response); if (injectionStatus === "DISABLED") { consola.info("Inject is disabled, a webapp html file will be generated."); this.emitFile({ fileName: "webapp.html", source: parsedHtml.map((tag) => tag.fragment).join("\n"), type: "asset", }); } } else if (viteCommand === "serve") { serveFiles(this, response); } }, name: PLUGIN_NAME, order: "post", rollup: { generateBundle(_, bundle) { if (injectionStatus === "ENABLED" && bundle["index.html"] && typeof bundle["index.html"]["source"] === "string") { // eslint-disable-next-line no-param-reassign bundle["index.html"]["source"] = bundle["index.html"]["source"].replace("</head>", `${parsedHtml.map((tag) => tag.fragment).join("\n")}\n</head>`); } }, }, rspack(compiler) { base = "/"; compiler.hooks.make.tapPromise(PLUGIN_NAME, async (compilation) => { if (injectionStatus === "ENABLED") { const rspackPlugin = findHtmlRspackPlugin(compilation); const htmlWebpackPlugin = findHtmlWebpackPlugin(compilation); if (rspackPlugin) { // Hook into the html-webpack-plugin processing and add the html injectHtmlPlugin(compilation, rspackPlugin); } else if (htmlWebpackPlugin) { // Hook into the html-webpack-plugin processing and add the html injectHtmlPlugin(compilation, htmlWebpackPlugin); } else { consola.warn(`No "@rspack/plugin-html" or "html-webpack-plugin" plugin was found, injection was disabled, currently the builtin html is not supported.`); injectionStatus = "NOT_SUPPORTED"; } } }); }, transform(code, id) { const runtimePackageName = `${PLUGIN_NAME}/runtime`; if (!code.includes(runtimePackageName)) { return undefined; } const s = new MagicString(code); const statements = findStaticImports(code).filter((index) => index.specifier === runtimePackageName); if (statements.length === 0) { return undefined; } statements.forEach((index) => { const staticImport = parseStaticImport(index); const generatedCode = `const ${staticImport.defaultImport ?? staticImport.imports} = ${JSON.stringify(runtimeExports)};`; s.overwrite(staticImport.start, staticImport.end, generatedCode); }); if (!s.hasChanged()) { return undefined; } return { code: s.toString(), map: s.generateMap({ includeContent: true, source: id, }), }; }, transformInclude(id) { return id.match(/\.((c|m)?j|t)sx?$/u); }, vite: { configResolved(viteConfig) { base = viteConfig.base; viteConfig.plugins.forEach((plugin) => { if (frontendFramework === undefined && plugin.name.includes("sveltekit")) { frontendFramework = "sveltekit"; config.outputPath = "_app/immutable/assets/unplugin-favicons"; } else if (frontendFramework === undefined && plugin.name.includes("astro")) { frontendFramework = "astro"; } else if (frontendFramework === undefined && plugin.name.includes("vike")) { frontendFramework = "vike"; config.outputPath = "assets/static"; } }); }, configureServer(server) { if (viteCommand === "serve") { return () => { server.middlewares.use((request, response, next) => { const url = request.url?.slice(1); if (serveMap.has(url)) { const source = serveMap.get(url); const extension = url.split(".").pop(); if (source instanceof Buffer) { response.setHeader("Content-Type", extension ? mime.getType(extension) : "application/octet-stream"); } response.end(source); } else { next(); } }); }; } return () => { }; }, generateBundle(_, bundle) { // @see https://github.com/withastro/astro/issues/7695 if (frontendFramework === "astro") { Object.keys(bundle).forEach((key) => { const asset = bundle[key]; if (asset?.fileName?.includes(".astro")) { asset.code = asset.code.replaceAll(/<link rel="icon".*?>/gu, ""); asset.code = asset.code.replace("</head>", `${parsedHtml.map((tag) => tag.fragment).join("")}</head>`); } }); } }, transformIndexHtml: { handler(html) { if (injectionStatus === "ENABLED" && frontendFramework !== "vike") { // eslint-disable-next-line no-param-reassign html = html.replaceAll(/<link rel="icon".*?>/gu, ""); // eslint-disable-next-line no-param-reassign html = html.replace("</head>", `${parsedHtml.map((tag) => tag.fragment).join("")}</head>`); return html; } return html; }, order: "post", }, }, webpack(compiler) { if (compiler.options.output.path?.includes(".next")) { config.outputPath = "static/media/favicons/"; injectionStatus = "NOT_SUPPORTED"; } else { base = "/"; } compiler.hooks.make.tapPromise(PLUGIN_NAME, async (compilation) => { consola.info(compilation); if (injectionStatus === "ENABLED") { // Hook into the html-webpack-plugin processing and add the html injectHtmlPlugin(compilation, findHtmlWebpackPlugin(compilation)); } }); }, }; }; export default unpluginFactory; //# sourceMappingURL=unplugin-factory.js.map