Initial commit

This commit is contained in:
yuding
2025-12-03 12:00:46 +08:00
commit 5763b764a3
5365 changed files with 1483113 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
import type { Plugin } from 'vite';
import type { PluginConfiguration } from './interface';
/**
* Inject the CSS compiled with JS.
*
* @return {Plugin}
*/
export default function cssInjectedByJsPlugin({ cssAssetsFilterFunction, dev: { enableDev, removeStyleCode, removeStyleCodeFunction }, injectCode, injectCodeFunction, injectionCodeFormat, jsAssetsFilterFunction, preRenderCSSCode, relativeCSSInjection, styleId, suppressUnusedCssWarning, topExecutionPriority, useStrictCSP, }?: PluginConfiguration | undefined): Plugin[];

View File

@@ -0,0 +1,31 @@
import type { InjectCode, InjectCodeFunction } from './utils';
import type { OutputAsset, OutputChunk } from 'rollup';
import type { BuildOptions } from 'vite';
import type { ModuleFormat } from 'rollup';
export interface DevOptions {
enableDev?: boolean;
removeStyleCode?: (id: string) => string;
removeStyleCodeFunction?: (id: string) => void;
}
export interface BaseOptions {
dev?: DevOptions;
injectCode?: InjectCode;
injectCodeFunction?: InjectCodeFunction;
injectionCodeFormat?: ModuleFormat;
styleId?: string | (() => string);
topExecutionPriority?: boolean;
useStrictCSP?: boolean;
}
export interface PluginConfiguration extends BaseOptions {
cssAssetsFilterFunction?: (chunk: OutputAsset) => boolean;
jsAssetsFilterFunction?: (chunk: OutputChunk) => boolean;
preRenderCSSCode?: (cssCode: string) => string;
relativeCSSInjection?: boolean;
suppressUnusedCssWarning?: boolean;
}
export interface CSSInjectionConfiguration extends BaseOptions {
cssToInject: string;
}
export interface BuildCSSInjectionConfiguration extends CSSInjectionConfiguration {
buildOptions: BuildOptions;
}

View File

@@ -0,0 +1,26 @@
import type { OutputBundle, OutputChunk } from 'rollup';
import type { BuildCSSInjectionConfiguration, PluginConfiguration } from './interface';
interface InjectCodeOptions {
styleId?: string | (() => string);
useStrictCSP?: boolean;
attributes?: {
[key: string]: string;
} | undefined;
}
export type InjectCode = (cssCode: string, options: InjectCodeOptions) => string;
export type InjectCodeFunction = (cssCode: string, options: InjectCodeOptions) => void;
export declare function buildCSSInjectionCode({ buildOptions, cssToInject, injectCode, injectCodeFunction, injectionCodeFormat, styleId, useStrictCSP, }: BuildCSSInjectionConfiguration): Promise<OutputChunk | null>;
export declare function resolveInjectionCode(cssCode: string, injectCode: ((cssCode: string, options: InjectCodeOptions) => string) | undefined, injectCodeFunction: ((cssCode: string, options: InjectCodeOptions) => void) | undefined, { styleId, useStrictCSP, attributes }: InjectCodeOptions): string;
export declare function removeLinkStyleSheets(html: string, cssFileName: string): string;
export declare function warnLog(msg: string): void;
export declare function debugLog(msg: string): void;
export declare function extractCss(bundle: OutputBundle, cssName: string): string;
export declare function concatCssAndDeleteFromBundle(bundle: OutputBundle, cssAssets: string[]): string;
export declare function buildJsCssMap(bundle: OutputBundle, jsAssetsFilterFunction?: PluginConfiguration['jsAssetsFilterFunction']): Record<string, string[]>;
export declare function getJsTargetBundleKeys(bundle: OutputBundle, jsAssetsFilterFunction?: PluginConfiguration['jsAssetsFilterFunction']): string[];
export declare function relativeCssInjection(bundle: OutputBundle, assetsWithCss: Record<string, string[]>, buildCssCode: (css: string) => Promise<OutputChunk | null>, topExecutionPriorityFlag: boolean): Promise<void>;
export declare function globalCssInjection(bundle: OutputBundle, cssAssets: string[], buildCssCode: (css: string) => Promise<OutputChunk | null>, jsAssetsFilterFunction: PluginConfiguration['jsAssetsFilterFunction'], topExecutionPriorityFlag: boolean): Promise<void>;
export declare function buildOutputChunkWithCssInjectionCode(jsAssetCode: string, cssInjectionCode: string, topExecutionPriorityFlag: boolean): string;
export declare function clearImportedCssViteMetadataFromBundle(bundle: OutputBundle, unusedCssAssets: string[]): void;
export declare function isCSSRequest(request: string): boolean;
export {};

View File

@@ -0,0 +1,122 @@
import { buildCSSInjectionCode, buildJsCssMap, clearImportedCssViteMetadataFromBundle, globalCssInjection, isCSSRequest, relativeCssInjection, removeLinkStyleSheets, resolveInjectionCode, warnLog, } from './utils.js';
/**
* Inject the CSS compiled with JS.
*
* @return {Plugin}
*/
export default function cssInjectedByJsPlugin({ cssAssetsFilterFunction, dev: { enableDev, removeStyleCode, removeStyleCodeFunction } = {}, injectCode, injectCodeFunction, injectionCodeFormat, jsAssetsFilterFunction, preRenderCSSCode, relativeCSSInjection, styleId, suppressUnusedCssWarning, topExecutionPriority, useStrictCSP, } = {}) {
let config;
const topExecutionPriorityFlag = typeof topExecutionPriority == 'boolean' ? topExecutionPriority : true;
const plugins = [
{
apply: 'build',
enforce: 'post',
name: 'vite-plugin-css-injected-by-js',
config(config, env) {
if (env.command === 'build') {
if (!config.build) {
config.build = {};
}
if (relativeCSSInjection == true) {
if (!config.build.cssCodeSplit) {
config.build.cssCodeSplit = true;
warnLog(`[vite-plugin-css-injected-by-js] Override of 'build.cssCodeSplit' option to true, it must be true when 'relativeCSSInjection' is enabled.`);
}
}
}
},
configResolved(_config) {
config = _config;
},
async generateBundle(opts, bundle) {
if (config.build.ssr) {
return;
}
const buildCssCode = (cssToInject) => buildCSSInjectionCode({
buildOptions: config.build,
cssToInject: typeof preRenderCSSCode == 'function' ? preRenderCSSCode(cssToInject) : cssToInject,
injectCode,
injectCodeFunction,
injectionCodeFormat,
styleId,
useStrictCSP,
});
const cssAssetsFilter = (asset) => {
return typeof cssAssetsFilterFunction == 'function' ? cssAssetsFilterFunction(asset) : true;
};
const cssAssets = Object.keys(bundle).filter((i) => bundle[i].type == 'asset' &&
bundle[i].fileName.endsWith('.css') &&
cssAssetsFilter(bundle[i]));
let unusedCssAssets = [];
if (relativeCSSInjection) {
const assetsWithCss = buildJsCssMap(bundle, jsAssetsFilterFunction);
await relativeCssInjection(bundle, assetsWithCss, buildCssCode, topExecutionPriorityFlag);
unusedCssAssets = cssAssets.filter((cssAsset) => !!bundle[cssAsset]);
if (!suppressUnusedCssWarning) {
// With all used CSS assets now being removed from the bundle, navigate any that have not been linked and output
const unusedCssAssetsString = unusedCssAssets.join(',');
unusedCssAssetsString.length > 0 &&
warnLog(`[vite-plugin-css-injected-by-js] Some CSS assets were not included in any known JS: ${unusedCssAssetsString}`);
}
}
else {
const allCssAssets = Object.keys(bundle).filter((i) => bundle[i].type == 'asset' &&
bundle[i].fileName.endsWith('.css'));
unusedCssAssets = allCssAssets.filter(cssAsset => !cssAssets.includes(cssAsset));
await globalCssInjection(bundle, cssAssets, buildCssCode, jsAssetsFilterFunction, topExecutionPriorityFlag);
}
clearImportedCssViteMetadataFromBundle(bundle, unusedCssAssets);
const htmlFiles = Object.keys(bundle).filter((i) => i.endsWith('.html'));
for (const name of htmlFiles) {
const htmlChunk = bundle[name];
let replacedHtml = htmlChunk.source instanceof Uint8Array
? new TextDecoder().decode(htmlChunk.source)
: `${htmlChunk.source}`;
cssAssets.forEach(function replaceLinkedStylesheetsHtml(cssName) {
if (!unusedCssAssets.includes(cssName)) {
replacedHtml = removeLinkStyleSheets(replacedHtml, cssName);
htmlChunk.source = replacedHtml;
}
});
}
},
},
];
if (enableDev) {
warnLog('[vite-plugin-css-injected-by-js] Experimental dev mode activated! Please, for any error open a issue.');
plugins.push({
name: 'vite-plugin-css-injected-by-js-dev',
apply: 'serve',
enforce: 'post',
transform(src, id) {
if (isCSSRequest(id)) {
const defaultRemoveStyleCode = (devId) => `{
(function removeStyleInjected() {
const elementsToRemove = document.querySelectorAll("style[data-vite-dev-id='${devId}']");
elementsToRemove.forEach(element => {
element.remove();
});
})()
}`;
let removeStyleFunction = removeStyleCode || defaultRemoveStyleCode;
if (removeStyleCodeFunction) {
removeStyleFunction = (id) => `(${removeStyleCodeFunction})("${id}")`;
}
// removeStyleFunction is called before since the function that inject the CSS doesn't handle the update case required in dev mode.
let injectionCode = src.replace('__vite__updateStyle(__vite__id, __vite__css)', ';\n' +
removeStyleFunction(id) +
';\n' +
resolveInjectionCode('__vite__css', injectCode, injectCodeFunction, {
attributes: { type: 'text/css', ['data-vite-dev-id']: id },
}));
injectionCode = injectionCode.replace('__vite__removeStyle(__vite__id)', removeStyleFunction(id));
return {
code: injectionCode,
map: null,
};
}
},
});
}
return plugins;
}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@@ -0,0 +1,230 @@
import { build } from 'vite';
const cssInjectedByJsId = '\0vite/all-css';
const defaultInjectCode = (cssCode, { styleId, useStrictCSP, attributes }) => {
let attributesInjection = '';
for (const attribute in attributes) {
attributesInjection += `elementStyle.setAttribute('${attribute}', '${attributes[attribute]}');`;
}
return `try{if(typeof document != 'undefined'){var elementStyle = document.createElement('style');${typeof styleId == 'string' && styleId.length > 0 ? `elementStyle.id = '${styleId}';` : ''}${useStrictCSP ? `elementStyle.nonce = document.head.querySelector('meta[property=csp-nonce]')?.content;` : ''}${attributesInjection}elementStyle.appendChild(document.createTextNode(${cssCode}));document.head.appendChild(elementStyle);}}catch(e){console.error('vite-plugin-css-injected-by-js', e);}`;
};
export async function buildCSSInjectionCode({ buildOptions, cssToInject, injectCode, injectCodeFunction, injectionCodeFormat = 'iife', styleId, useStrictCSP, }) {
let { minify, target } = buildOptions;
const generatedStyleId = typeof styleId === 'function' ? styleId() : styleId;
const res = await build({
root: '',
configFile: false,
logLevel: 'error',
plugins: [
injectionCSSCodePlugin({
cssToInject,
styleId: generatedStyleId,
injectCode,
injectCodeFunction,
useStrictCSP,
}),
],
build: {
write: false,
target,
minify,
assetsDir: '',
rollupOptions: {
input: {
['all-css']: cssInjectedByJsId,
},
output: {
format: injectionCodeFormat,
manualChunks: undefined,
},
},
},
});
const _cssChunk = Array.isArray(res) ? res[0] : res;
if (!('output' in _cssChunk))
return null;
return _cssChunk.output[0];
}
export function resolveInjectionCode(cssCode, injectCode, injectCodeFunction, { styleId, useStrictCSP, attributes }) {
const injectionOptions = { styleId, useStrictCSP, attributes };
if (injectCodeFunction) {
return `(${injectCodeFunction})(${cssCode}, ${JSON.stringify(injectionOptions)})`;
}
const injectFunction = injectCode || defaultInjectCode;
return injectFunction(cssCode, injectionOptions);
}
function injectionCSSCodePlugin({ cssToInject, injectCode, injectCodeFunction, styleId, useStrictCSP, }) {
return {
name: 'vite:injection-css-code-plugin',
resolveId(id) {
if (id == cssInjectedByJsId) {
return id;
}
},
load(id) {
if (id == cssInjectedByJsId) {
const cssCode = JSON.stringify(cssToInject.trim());
return resolveInjectionCode(cssCode, injectCode, injectCodeFunction, { styleId, useStrictCSP });
}
},
};
}
export function removeLinkStyleSheets(html, cssFileName) {
const removeCSS = new RegExp(`<link rel=".*"[^>]*?href=".*/?${cssFileName}"[^>]*?>`);
return html.replace(removeCSS, '');
}
/* istanbul ignore next -- @preserve */
export function warnLog(msg) {
console.warn(`\x1b[33m \n${msg} \x1b[39m`);
}
/* istanbul ignore next -- @preserve */
export function debugLog(msg) {
console.debug(`\x1b[34m \n${msg} \x1b[39m`);
}
function isJsOutputChunk(chunk) {
return chunk.type == 'chunk' && chunk.fileName.match(/.[cm]?js(?:\?.+)?$/) != null;
}
function defaultJsAssetsFilter(chunk) {
return chunk.isEntry && !chunk.fileName.includes('polyfill');
}
// The cache must be global since execution context is different every entry
const cssSourceCache = {};
export function extractCss(bundle, cssName) {
const cssAsset = bundle[cssName];
if (cssAsset !== undefined && cssAsset.source) {
const cssSource = cssAsset.source;
// We treat these as strings and coerce them implicitly to strings, explicitly handle conversion
cssSourceCache[cssName] =
cssSource instanceof Uint8Array ? new TextDecoder().decode(cssSource) : `${cssSource}`;
}
return cssSourceCache[cssName] ?? '';
}
export function concatCssAndDeleteFromBundle(bundle, cssAssets) {
return cssAssets.reduce(function extractCssAndDeleteFromBundle(previous, cssName) {
const cssSource = extractCss(bundle, cssName);
delete bundle[cssName];
return previous + cssSource;
}, '');
}
export function buildJsCssMap(bundle, jsAssetsFilterFunction) {
const chunksWithCss = {};
const bundleKeys = getJsTargetBundleKeys(bundle, typeof jsAssetsFilterFunction == 'function' ? jsAssetsFilterFunction : () => true);
if (bundleKeys.length === 0) {
throw new Error('Unable to locate the JavaScript asset for adding the CSS injection code. It is recommended to review your configurations.');
}
for (const key of bundleKeys) {
const chunk = bundle[key];
if (chunk.type === 'asset' || !chunk.viteMetadata || chunk.viteMetadata.importedCss.size === 0) {
continue;
}
const chunkStyles = chunksWithCss[key] || [];
chunkStyles.push(...chunk.viteMetadata.importedCss.values());
chunksWithCss[key] = chunkStyles;
}
return chunksWithCss;
}
export function getJsTargetBundleKeys(bundle, jsAssetsFilterFunction) {
if (typeof jsAssetsFilterFunction != 'function') {
const jsAssets = Object.keys(bundle).filter((i) => {
const asset = bundle[i];
return isJsOutputChunk(asset) && defaultJsAssetsFilter(asset);
});
if (jsAssets.length == 0) {
return [];
}
const jsTargetFileName = jsAssets[jsAssets.length - 1];
if (jsAssets.length > 1) {
warnLog(`[vite-plugin-css-injected-by-js] has identified "${jsTargetFileName}" as one of the multiple output files marked as "entry" to put the CSS injection code.` +
'However, if this is not the intended file to add the CSS injection code, you can use the "jsAssetsFilterFunction" parameter to specify the desired output file (read docs).');
if (process.env.VITE_CSS_INJECTED_BY_JS_DEBUG) {
const jsAssetsStr = jsAssets.join(', ');
debugLog(`[vite-plugin-css-injected-by-js] identified js file targets: ${jsAssetsStr}. Selected "${jsTargetFileName}".\n`);
}
}
// This should be always the root of the application
return [jsTargetFileName];
}
const chunkFilter = ([_key, chunk]) => isJsOutputChunk(chunk) && jsAssetsFilterFunction(chunk);
return Object.entries(bundle)
.filter(chunkFilter)
.map(function extractAssetKeyFromBundleEntry([key]) {
return key;
});
}
export async function relativeCssInjection(bundle, assetsWithCss, buildCssCode, topExecutionPriorityFlag) {
for (const [jsAssetName, cssAssets] of Object.entries(assetsWithCss)) {
process.env.VITE_CSS_INJECTED_BY_JS_DEBUG &&
debugLog(`[vite-plugin-css-injected-by-js] Relative CSS: ${jsAssetName}: [ ${cssAssets.join(',')} ]`);
const assetCss = concatCssAndDeleteFromBundle(bundle, cssAssets);
const cssInjectionCode = assetCss.length > 0 ? (await buildCssCode(assetCss))?.code : '';
// We have already filtered these chunks to be RenderedChunks
const jsAsset = bundle[jsAssetName];
jsAsset.code = buildOutputChunkWithCssInjectionCode(jsAsset.code, cssInjectionCode ?? '', topExecutionPriorityFlag);
}
}
const globalCSSCodeEntryCache = new Map();
let previousFacadeModuleId = '';
export async function globalCssInjection(bundle, cssAssets, buildCssCode, jsAssetsFilterFunction, topExecutionPriorityFlag) {
const jsTargetBundleKeys = getJsTargetBundleKeys(bundle, jsAssetsFilterFunction);
if (jsTargetBundleKeys.length == 0) {
throw new Error('Unable to locate the JavaScript asset for adding the CSS injection code. It is recommended to review your configurations.');
}
process.env.VITE_CSS_INJECTED_BY_JS_DEBUG &&
debugLog(`[vite-plugin-css-injected-by-js] Global CSS Assets: [${cssAssets.join(',')}]`);
const allCssCode = concatCssAndDeleteFromBundle(bundle, cssAssets);
let cssInjectionCode = '';
if (allCssCode.length > 0) {
const cssCode = (await buildCssCode(allCssCode))?.code;
if (typeof cssCode == 'string') {
cssInjectionCode = cssCode;
}
}
for (const jsTargetKey of jsTargetBundleKeys) {
const jsAsset = bundle[jsTargetKey];
/**
* Since it creates the assets once sequential builds for the same entry point
* (for example when multiple formats of same entry point are built),
* we need to reuse the same CSS created the first time.
*/
if (jsAsset.facadeModuleId != null && jsAsset.isEntry && cssInjectionCode != '') {
if (jsAsset.facadeModuleId != previousFacadeModuleId) {
globalCSSCodeEntryCache.clear();
}
previousFacadeModuleId = jsAsset.facadeModuleId;
globalCSSCodeEntryCache.set(jsAsset.facadeModuleId, cssInjectionCode);
}
if (cssInjectionCode == '' &&
jsAsset.isEntry &&
jsAsset.facadeModuleId != null &&
typeof globalCSSCodeEntryCache.get(jsAsset.facadeModuleId) == 'string') {
cssInjectionCode = globalCSSCodeEntryCache.get(jsAsset.facadeModuleId);
}
process.env.VITE_CSS_INJECTED_BY_JS_DEBUG &&
debugLog(`[vite-plugin-css-injected-by-js] Global CSS inject: ${jsAsset.fileName}`);
jsAsset.code = buildOutputChunkWithCssInjectionCode(jsAsset.code, cssInjectionCode ?? '', topExecutionPriorityFlag);
}
}
export function buildOutputChunkWithCssInjectionCode(jsAssetCode, cssInjectionCode, topExecutionPriorityFlag) {
const appCode = jsAssetCode.replace(/\/\*\s*empty css\s*\*\//g, '');
jsAssetCode = topExecutionPriorityFlag ? '' : appCode;
jsAssetCode += cssInjectionCode;
jsAssetCode += !topExecutionPriorityFlag ? '' : appCode;
return jsAssetCode;
}
export function clearImportedCssViteMetadataFromBundle(bundle, unusedCssAssets) {
// Required to exclude removed files from manifest.json
for (const key in bundle) {
const chunk = bundle[key];
if (chunk.viteMetadata && chunk.viteMetadata.importedCss.size > 0) {
const importedCssFileNames = chunk.viteMetadata.importedCss;
importedCssFileNames.forEach((importedCssFileName) => {
if (!unusedCssAssets.includes(importedCssFileName) && chunk.viteMetadata) {
chunk.viteMetadata.importedCss = new Set();
}
});
}
}
}
export function isCSSRequest(request) {
const CSS_LANGS_RE = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/;
return CSS_LANGS_RE.test(request);
}