vite-plugin-inline-source
Version:
A vite plugin for inlining files into HTML
165 lines • 6.55 kB
JavaScript
// src/index.ts
import { readFile } from "fs/promises";
import path from "path";
import { minify as minifyCss } from "csso";
import * as esbuild from "esbuild";
import * as sass from "sass";
import { optimize as optimizeSvg } from "svgo";
import { minify as minifyJs } from "terser";
import {
loadEnv
} from "vite";
import z from "zod";
var { compileString: compileSass } = sass;
var InlineSourceOptionsSchema = z.object({
replaceTags: z.array(z.string()).default(["svg", "math"]).describe(
"Tags that should be replaced entirely when inlining elements. The default behavior is to preserve the tags and place the content from the source file inside them."
),
optimizeSvgs: z.boolean().default(true).describe("Whether or not to optimize SVGs using svgo"),
compileSass: z.boolean().default(false).describe("Whether or not to compile SASS using sass"),
optimizeCss: z.boolean().default(false).describe("Whether or not to optimize CSS using csso"),
compileTs: z.boolean().default(true).describe(
"Whether or not to transform TypeScript to JavaScript using esbuild"
),
optimizeJs: z.boolean().default(false).describe("Whether or not to optimize JS using terser"),
svgoOptions: z.object({}).passthrough().default({}),
sassOptions: z.object({}).passthrough().default({}),
cssoOptions: z.object({}).passthrough().default({}),
terserOptions: z.object({}).passthrough().default({})
}).default({});
var PATTERN = /<([A-z0-9-]+)\s+([^>]*?)src\s*=\s*"([^>]*?)"([^>]*?)\s*((\/>)|(>\s*<\/\s*\1\s*>))/gi;
function VitePluginInlineSource(opts) {
const options = InlineSourceOptionsSchema.parse(opts);
let root = "";
async function transformHtml(source, ctx) {
const result = [];
const tokens = source.matchAll(PATTERN);
let prevPos = 0;
for (const token of tokens) {
const [matched, tagName, preAttributes, fileName, postAttributes] = token;
const { index } = token;
const isSvgFile = path.extname(fileName).toLowerCase() === ".svg";
const isSassFile = path.extname(fileName).toLowerCase() === ".scss";
const isCssFile = path.extname(fileName).toLowerCase() === ".css";
const isJsFile = path.extname(fileName).toLowerCase() === ".js";
const isTsFile = path.extname(fileName).toLowerCase() === ".ts";
const isImg = tagName.toLowerCase() === "img";
const shouldInline = /\binline-source\b/.test(
preAttributes + " " + postAttributes
);
if (isImg && !isSvgFile || !shouldInline) {
continue;
}
const filePath = root ? path.join(root, fileName) : fileName;
let fileContent = ctx.server ? (await readFile(`${filePath}`)).toString() : (
// @ts-expect-error don't know these types aren't right
(await ctx.load({ id: `${filePath}?raw` })).ast?.body?.[0].declaration.value
);
if (isSvgFile && options.optimizeSvgs) {
fileContent = optimizeSvg(fileContent, options.svgoOptions).data;
} else if (isCssFile && options.optimizeCss) {
const minifiedCode = minifyCss(fileContent, options.cssoOptions).css;
if (minifiedCode.length === 0 && fileContent.length !== 0) {
throw new Error("Failed to minify CSS");
}
fileContent = minifiedCode;
} else if (isSassFile && options.compileSass) {
const css = compileSass(fileContent, options.sassOptions).css;
fileContent = options.optimizeCss ? minifyCss(css, options.cssoOptions).css : css;
} else if (isJsFile && options.optimizeJs) {
const minifiedCode = (await minifyJs(fileContent, options.terserOptions)).code;
if (minifiedCode) {
fileContent = minifiedCode;
}
} else if (isTsFile && options.compileTs) {
try {
const envVars = loadEnv(env.mode, process.cwd());
const envVarDefines = Object.entries(envVars).reduce((prev, [key, value]) => {
if (key.startsWith("VITE")) prev[`import.meta.env.${key}`] = value;
return prev;
}, {});
const transformResult = await esbuild.transform(fileContent, {
loader: "ts",
define: {
"import.meta.env.MODE": `"${env.mode}"`,
"import.meta.env.BASE_URL": `"${config.base ?? "/"}"`,
"import.meta.env.PROD": `${process.env.NODE_ENV == "production"}`,
"import.meta.env.DEV": `${process.env.NODE_ENV != "production"}`,
"import.meta.env.SSR": `${env.isSsrBuild}`,
...envVarDefines
}
});
fileContent = transformResult.code;
if (options.optimizeJs) {
try {
const minifiedResult = await minifyJs(
fileContent,
options.terserOptions
);
if (minifiedResult.code) {
fileContent = minifiedResult.code;
}
} catch (error) {
console.warn("Failed to minify compiled TypeScript:", error);
}
}
} catch (error) {
console.error("Failed to compile TypeScript:", error);
throw error;
}
}
fileContent = fileContent.replace(/^<!DOCTYPE(.*?[^?])?>/, "");
if (index !== prevPos) {
result.push(source.slice(prevPos, index));
}
if (options.replaceTags.includes(tagName)) {
result.push(
fileContent.replace(
new RegExp(`^<\\s*${tagName}`),
`<${tagName} ${preAttributes.replace(
/inline-source/g,
""
)} ${postAttributes.replace(/inline-source/g, "")}`
)
);
} else {
result.push(
`<${tagName}
${preAttributes.replace(
/inline-source/g,
""
)} ${postAttributes.replace(/inline-source/g, "")}
>${fileContent}</${tagName}>`
);
}
prevPos = index + matched.length;
}
result.push(source.slice(prevPos));
return result.join("");
}
let env;
let config;
return {
name: "vite-plugin-inline-source",
configResolved(_config) {
root = _config.root ?? "";
config = _config;
},
config(_, e) {
env = e;
},
transform(source, id) {
if (id && !id.endsWith(".html")) {
return null;
}
return transformHtml(source, this);
},
transformIndexHtml(source, ctx) {
return transformHtml(source, ctx);
}
};
}
export {
VitePluginInlineSource as default
};
//# sourceMappingURL=index.js.map