Skip to content

Commit 289a306

Browse files
[#30] ESM & CJS support and bugfixing (#32)
* WIP * 2.0.0-beta * Require manual tests * 2.0.0-beta-0 * Fix * 2.0.0-beta-1 * Restore logic * 2.0.0-beta-2 * Fix and support Vite 3 * 2.0.0-beta-2.0 * 2.0.0
1 parent ec0392e commit 289a306

File tree

9 files changed

+167
-103
lines changed

9 files changed

+167
-103
lines changed

.prettierrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"printWidth": 80,
2+
"printWidth": 130,
33
"tabWidth": 4,
44
"endOfLine": "auto",
55
"singleQuote": true,

cjs-esm-fixup

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
cat >dist/cjs/package.json <<!EOF
2+
{
3+
"type": "commonjs"
4+
}
5+
!EOF
6+
7+
cat >dist/esm/package.json <<!EOF
8+
{
9+
"type": "module"
10+
}
11+
!EOF

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
{
22
"name": "vite-plugin-css-injected-by-js",
3-
"version": "1.5.1",
3+
"version": "2.0.0",
44
"description": "A Vite plugin that takes the CSS and adds it to the page through the JS. For those who want a single JS file.",
5-
"main": "dist/index.js",
6-
"types": "dist/index.d.ts",
5+
"main": "dist/cjs/index.js",
6+
"module": "dist/esm/index.js",
7+
"exports": {
8+
".": {
9+
"import": "./dist/esm/index.js",
10+
"require": "./dist/cjs/index.js"
11+
}
12+
},
13+
"typings": "dist/esm/declarations/index.d.ts",
14+
"files": [
15+
"dist"
16+
],
717
"scripts": {
8-
"build": "tsc",
18+
"build": "rm -rf dist && tsc -p tsconfig.json && tsc -p tsconfig-cjs.json && ./cjs-esm-fixup",
919
"format": "prettier 'src/index.ts' --write"
1020
},
1121
"repository": {
@@ -22,9 +32,6 @@
2232
"css",
2333
"js"
2434
],
25-
"files": [
26-
"dist/**/*"
27-
],
2835
"peerDependencies": {
2936
"vite": ">2.0.0-0"
3037
},

src/index.ts

Lines changed: 35 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
import {
2-
IndexHtmlTransformContext,
3-
IndexHtmlTransformResult,
4-
Plugin,
5-
} from 'vite';
1+
import { Plugin } from 'vite';
2+
import { OutputAsset, OutputChunk } from 'rollup';
3+
import { buildCSSInjectionCode, removeLinkStyleSheets } from './utils';
64

75
/**
86
* Inject the CSS compiled with JS.
97
*
108
* @return {Plugin}
119
*/
12-
function cssInjectedByJsPlugin(
10+
export default function cssInjectedByJsPlugin(
1311
{ topExecutionPriority, styleId } = {
1412
topExecutionPriority: true,
1513
styleId: '',
@@ -22,87 +20,41 @@ function cssInjectedByJsPlugin(
2220
apply: 'build',
2321
enforce: 'post',
2422
name: 'css-in-js-plugin',
25-
generateBundle(opts, bundle) {
26-
let styleCode = '';
27-
28-
for (const key in bundle) {
29-
if (bundle[key]) {
30-
const chunk = bundle[key];
31-
32-
if (
33-
chunk.type === 'asset' &&
34-
chunk.fileName.includes('.css')
35-
) {
36-
styleCode += chunk.source;
37-
delete bundle[key];
38-
}
23+
async generateBundle(opts, bundle) {
24+
const htmlFiles = Object.keys(bundle).filter((i) => i.endsWith('.html'));
25+
const cssAssets = Object.keys(bundle).filter((i) => bundle[i].type == 'asset' && bundle[i].fileName.endsWith('.css'));
26+
const jsAssets = Object.keys(bundle).filter(
27+
(i) =>
28+
bundle[i].type == 'chunk' &&
29+
bundle[i].fileName.match(/.[cm]?js$/) != null &&
30+
!bundle[i].fileName.includes('polyfill')
31+
);
32+
33+
for (const name of htmlFiles) {
34+
const htmlChunk = bundle[name] as OutputAsset;
35+
let replacedHtml = htmlChunk.source as string;
36+
37+
const allCssCode = cssAssets.reduce(function extractCssCodeAndDeleteFromBundle(previousValue, cssName) {
38+
const cssAsset = bundle[cssName] as OutputAsset;
39+
const result = previousValue + cssAsset.source;
40+
delete bundle[cssName];
41+
replacedHtml = removeLinkStyleSheets(replacedHtml, cssName);
42+
htmlChunk.source = replacedHtml;
43+
return result;
44+
}, '');
45+
46+
if (allCssCode.length > 0) {
47+
cssToInject = allCssCode;
3948
}
4049
}
4150

42-
if (styleCode.length > 0) {
43-
cssToInject = styleCode;
44-
}
45-
46-
for (const key in bundle) {
47-
if (bundle[key]) {
48-
const chunk = bundle[key];
51+
const jsAsset = bundle[jsAssets[0]] as OutputChunk;
4952

50-
if (
51-
chunk.type === 'chunk' &&
52-
chunk.fileName.match(/.[cm]?js$/) != null &&
53-
!chunk.fileName.includes('polyfill')
54-
) {
55-
let topCode: string = '';
56-
let bottomCode: string = '';
57-
if (topExecutionPriority) {
58-
bottomCode = chunk.code;
59-
} else {
60-
topCode = chunk.code;
61-
}
62-
63-
chunk.code = topCode;
64-
chunk.code +=
65-
"(function(){ try {var elementStyle = document.createElement('style'); elementStyle.appendChild(document.createTextNode(";
66-
chunk.code += JSON.stringify(cssToInject.trim());
67-
chunk.code += ')); ';
68-
if (typeof styleId == 'string' && styleId.length > 0) {
69-
chunk.code += ` elementStyle.id = "${styleId}"; `;
70-
}
71-
chunk.code +=
72-
"document.head.appendChild(elementStyle);} catch(e) {console.error('vite-plugin-css-injected-by-js', e);} })();";
73-
chunk.code += bottomCode;
74-
75-
break;
76-
}
77-
}
78-
}
79-
},
80-
transformIndexHtml: {
81-
enforce: 'post',
82-
transform(
83-
html: string,
84-
ctx?: IndexHtmlTransformContext
85-
): IndexHtmlTransformResult {
86-
if (!ctx || !ctx.bundle) return html;
87-
88-
for (const [, value] of Object.entries(ctx.bundle)) {
89-
if (value.fileName.endsWith('.css')) {
90-
// Remove CSS link from HTML generated.
91-
const reCSS = new RegExp(
92-
`<link rel="stylesheet"[^>]*?href=".*/${value.fileName}"[^>]*?>`
93-
);
94-
html = html.replace(reCSS, '');
95-
}
96-
}
97-
98-
return html;
99-
},
53+
const cssInjectionCode = await buildCSSInjectionCode(cssToInject, styleId);
54+
const appCode = jsAsset.code;
55+
jsAsset.code = topExecutionPriority ? '' : appCode;
56+
jsAsset.code += cssInjectionCode ? cssInjectionCode.code : '';
57+
jsAsset.code += !topExecutionPriority ? '' : appCode;
10058
},
10159
};
10260
}
103-
104-
module.exports = cssInjectedByJsPlugin;
105-
106-
cssInjectedByJsPlugin.default = cssInjectedByJsPlugin;
107-
108-
export default cssInjectedByJsPlugin;

src/utils.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import {build, Plugin} from "vite";
2+
import {OutputChunk} from "rollup";
3+
4+
const cssInjectedByJsId = '\0vite/all-css';
5+
6+
export async function buildCSSInjectionCode(cssToInject: string, styleId: string): Promise<OutputChunk | null> {
7+
const res = await build({
8+
root: '',
9+
configFile: false,
10+
logLevel: 'error',
11+
plugins: [injectionCSSCodePlugin(cssToInject, styleId)],
12+
build: {
13+
write: false,
14+
target: 'es2015',
15+
minify: 'esbuild',
16+
assetsDir: '',
17+
rollupOptions: {
18+
input: {
19+
['all-css']: cssInjectedByJsId,
20+
},
21+
output: {
22+
format: 'iife',
23+
manualChunks: undefined,
24+
},
25+
},
26+
},
27+
});
28+
const _cssChunk = Array.isArray(res) ? res[0] : res;
29+
if (!('output' in _cssChunk)) return null;
30+
31+
return _cssChunk.output[0];
32+
}
33+
34+
/**
35+
* @param {string} cssToInject
36+
* @param {string|null} styleId
37+
* @return {Plugin}
38+
*/
39+
function injectionCSSCodePlugin(cssToInject: string, styleId: string | null): Plugin {
40+
return {
41+
name: 'vite:injection-css-code-plugin',
42+
resolveId(id: string) {
43+
if (id == cssInjectedByJsId) {
44+
return id;
45+
}
46+
},
47+
load(id: string) {
48+
if (id == cssInjectedByJsId) {
49+
const cssCode = JSON.stringify(cssToInject.trim());
50+
51+
return `try{var elementStyle = document.createElement('style');${
52+
typeof styleId == 'string' && styleId.length > 0 ? 'elementStyle.id = "${styleId}";' : ''
53+
}elementStyle.appendChild(document.createTextNode(${cssCode}));document.head.appendChild(elementStyle);}catch(e){console.error('vite-plugin-css-injected-by-js', e);}`;
54+
}
55+
},
56+
};
57+
}
58+
59+
export function removeLinkStyleSheets(html: string, cssFileName: string): string {
60+
const removeCSS = new RegExp(`<link rel=".*"[^>]*?href=".*/?${cssFileName}"[^>]*?>`);
61+
return html.replace(removeCSS, '');
62+
}

tsconfig-base.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"compilerOptions": {
3+
"allowJs": true,
4+
"allowSyntheticDefaultImports": true,
5+
"baseUrl": "src",
6+
"declaration": true,
7+
"declarationMap": false,
8+
"esModuleInterop": true,
9+
"inlineSourceMap": false,
10+
"lib": ["esnext", "dom"],
11+
"listEmittedFiles": false,
12+
"moduleResolution": "node",
13+
"noUnusedLocals": true,
14+
"noUnusedParameters": false,
15+
"outDir": "dist",
16+
"pretty": true,
17+
"resolveJsonModule": true,
18+
"rootDir": "src",
19+
"skipLibCheck": true,
20+
"strict": true,
21+
"sourceMap": false,
22+
"traceResolution": false,
23+
"types": ["node"]
24+
},
25+
"compileOnSave": false,
26+
"include": ["./src"],
27+
"exclude": ["node_modules", "dist"]
28+
}

tsconfig-cjs.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "./tsconfig-base.json",
3+
"compilerOptions": {
4+
"declarationDir": "dist/cjs/declarations",
5+
"module": "commonjs",
6+
"outDir": "dist/cjs",
7+
"target": "es2015"
8+
}
9+
}

tsconfig.json

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
{
2-
"include": ["src"],
2+
"extends": "./tsconfig-base.json",
33
"compilerOptions": {
4-
"declaration": true,
5-
"esModuleInterop": true,
6-
"module": "commonjs",
7-
"moduleResolution": "node",
8-
"noUnusedLocals": true,
9-
"outDir": "dist",
10-
"strict": true,
11-
"sourceMap": false,
12-
"target": "es2018"
4+
"declarationDir": "dist/esm/declarations",
5+
"module": "esnext",
6+
"outDir": "dist/esm",
7+
"target": "esnext"
138
}
149
}

0 commit comments

Comments
 (0)