Skip to content
This repository was archived by the owner on May 25, 2023. It is now read-only.

Commit 2d9c800

Browse files
committed
Initial Commit
0 parents  commit 2d9c800

9 files changed

+3601
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
.idea/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021 Ethan Jones
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
esbuild-scss-modules-plugin
2+
=======
3+
4+
Plugin to use scss and css modules with esbuild.
5+
Based on [indooorsman/esbuild-css-modules-plugin](https://github.com/indooorsman/esbuild-css-modules-plugin/blob/eff4a500c56a45b1550887a8f7c20f57b01a46b7/index.js).
6+
7+
## Example
8+
9+
```js
10+
import esbuild from "esbuild";
11+
import {ScssModulesPlugin} from "esbuild-scss-modules-plugin";
12+
13+
const result = await esbuild.build({
14+
entryPoints: ['src/index.ts'],
15+
bundle: true,
16+
outfile: 'dist/index.js',
17+
18+
plugins: [
19+
ScssModulesPlugin({
20+
inject: false,
21+
minify: true,
22+
cssCallback: (css) => console.log(css),
23+
})
24+
]
25+
})
26+
```
27+
28+
## Options
29+
See `index.ts`
30+
31+
## License
32+
MIT

index.d.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as postcssModules from "postcss-modules";
2+
import * as sass from "sass";
3+
import type * as esbuild from "esbuild";
4+
declare type CssModulesOptions = Parameters<postcssModules>[0];
5+
export declare type PluginOptions = {
6+
inject: boolean;
7+
minify: boolean;
8+
cache: boolean;
9+
localsConvention: CssModulesOptions["localsConvention"];
10+
generateScopedName: CssModulesOptions["generateScopedName"];
11+
scssOptions: sass.Options;
12+
cssCallback?: (css: string, map: {
13+
[className: string]: string;
14+
}) => void;
15+
};
16+
export declare const ScssModulesPlugin: (options?: Partial<PluginOptions>) => esbuild.Plugin;
17+
export default ScssModulesPlugin;
18+
declare module '*.modules.scss' {
19+
interface IClassNames {
20+
[className: string]: string;
21+
}
22+
const classes: IClassNames;
23+
const digest: string;
24+
const css: string;
25+
export default classes;
26+
export { classes, digest, css };
27+
}

index.js

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
"use strict";
2+
// Modified from: https://github.com/indooorsman/esbuild-css-modules-plugin
3+
// eff4a500c56a45b1550887a8f7c20f57b01a46b7
4+
// MIT License
5+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6+
if (k2 === undefined) k2 = k;
7+
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
8+
}) : (function(o, m, k, k2) {
9+
if (k2 === undefined) k2 = k;
10+
o[k2] = m[k];
11+
}));
12+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
13+
Object.defineProperty(o, "default", { enumerable: true, value: v });
14+
}) : function(o, v) {
15+
o["default"] = v;
16+
});
17+
var __importStar = (this && this.__importStar) || function (mod) {
18+
if (mod && mod.__esModule) return mod;
19+
var result = {};
20+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
21+
__setModuleDefault(result, mod);
22+
return result;
23+
};
24+
var __importDefault = (this && this.__importDefault) || function (mod) {
25+
return (mod && mod.__esModule) ? mod : { "default": mod };
26+
};
27+
Object.defineProperty(exports, "__esModule", { value: true });
28+
exports.ScssModulesPlugin = void 0;
29+
const path_1 = __importDefault(require("path"));
30+
const promises_1 = __importDefault(require("fs/promises"));
31+
const crypto_1 = __importDefault(require("crypto"));
32+
const postcss_1 = __importDefault(require("postcss"));
33+
const postcssModules = __importStar(require("postcss-modules"));
34+
const sass = __importStar(require("sass"));
35+
const PLUGIN = 'esbuild-scss-modules-plugin';
36+
const DefaultOptions = {
37+
inject: true,
38+
minify: false,
39+
cache: true,
40+
localsConvention: "camelCaseOnly",
41+
generateScopedName: undefined,
42+
scssOptions: {},
43+
cssCallback: undefined
44+
};
45+
async function buildScss(scssFullPath, sassOptions) {
46+
return new Promise((resolve, reject) => sass.render({
47+
file: scssFullPath,
48+
...sassOptions
49+
}, (err, result) => err ? reject(err) : resolve(result)));
50+
}
51+
async function buildScssModulesJS(scssFullPath, options) {
52+
const css = (await buildScss(scssFullPath, options.scssOptions)).css;
53+
let cssModulesJSON = {};
54+
const result = await postcss_1.default([
55+
postcssModules.default({
56+
localsConvention: options.localsConvention,
57+
generateScopedName: options.generateScopedName,
58+
getJSON(cssSourceFile, json) {
59+
cssModulesJSON = { ...json };
60+
return cssModulesJSON;
61+
}
62+
}),
63+
...(options.minify ? [require("cssnano")({
64+
preset: 'default'
65+
})] : [])
66+
]).process(css, {
67+
from: scssFullPath,
68+
map: false
69+
});
70+
if (options.cssCallback)
71+
await options.cssCallback(result.css, cssModulesJSON);
72+
const classNames = JSON.stringify(cssModulesJSON);
73+
const hash = crypto_1.default.createHash('sha256');
74+
hash.update(result.css);
75+
const digest = hash.digest('hex');
76+
return `
77+
const digest = '${digest}';
78+
const classes = ${classNames};
79+
const css = \`${result.css}\`;
80+
${options.inject && `
81+
(function() {
82+
if (!document.getElementById(digest)) {
83+
var ele = document.createElement('style');
84+
ele.id = digest;
85+
ele.textContent = css;
86+
document.head.appendChild(ele);
87+
}
88+
})();
89+
`}
90+
export default classes;
91+
export { css, digest, classes };
92+
`;
93+
}
94+
const ScssModulesPlugin = (options = {}) => ({
95+
name: PLUGIN,
96+
setup(build) {
97+
const { outdir, bundle } = build.initialOptions;
98+
const results = new Map();
99+
const fullOptions = { ...DefaultOptions, ...options };
100+
build.onResolve({ filter: /\.modules?\.scss$/, namespace: 'file' }, async (args) => {
101+
const sourceFullPath = path_1.default.resolve(args.resolveDir, args.path);
102+
if (results.has(sourceFullPath))
103+
return results.get(sourceFullPath);
104+
const result = await (async () => {
105+
const sourceExt = path_1.default.extname(sourceFullPath);
106+
const sourceBaseName = path_1.default.basename(sourceFullPath, sourceExt);
107+
const jsContent = await buildScssModulesJS(sourceFullPath, fullOptions);
108+
if (bundle) {
109+
return {
110+
path: args.path,
111+
namespace: PLUGIN,
112+
pluginData: {
113+
content: jsContent
114+
}
115+
};
116+
}
117+
if (outdir) {
118+
const isOutdirAbsolute = path_1.default.isAbsolute(outdir);
119+
const absoluteOutdir = isOutdirAbsolute ? outdir : path_1.default.resolve(args.resolveDir, outdir);
120+
const isEntryAbsolute = path_1.default.isAbsolute(args.path);
121+
const entryRelDir = isEntryAbsolute ? path_1.default.dirname(path_1.default.relative(args.resolveDir, args.path)) : path_1.default.dirname(args.path);
122+
const targetSubpath = absoluteOutdir.indexOf(entryRelDir) === -1 ? path_1.default.join(entryRelDir, `${sourceBaseName}.css.js`) : `${sourceBaseName}.css.js`;
123+
const target = path_1.default.resolve(absoluteOutdir, targetSubpath);
124+
await promises_1.default.mkdir(path_1.default.dirname(target), { recursive: true });
125+
await promises_1.default.writeFile(target, jsContent);
126+
}
127+
return { path: sourceFullPath, namespace: 'file' };
128+
})();
129+
if (fullOptions.cache)
130+
results.set(sourceFullPath, result);
131+
return result;
132+
});
133+
build.onLoad({ filter: /\.modules?\.scss$/, namespace: PLUGIN }, (args) => {
134+
return { contents: args.pluginData.content, loader: 'js' };
135+
});
136+
}
137+
});
138+
exports.ScssModulesPlugin = ScssModulesPlugin;
139+
exports.default = exports.ScssModulesPlugin;

index.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Modified from: https://github.com/indooorsman/esbuild-css-modules-plugin
2+
// eff4a500c56a45b1550887a8f7c20f57b01a46b7
3+
// MIT License
4+
5+
import path from "path";
6+
import fs from "fs/promises";
7+
import crypto from "crypto";
8+
9+
import postcss from "postcss"
10+
import * as postcssModules from "postcss-modules"
11+
import * as sass from "sass";
12+
13+
import type * as esbuild from "esbuild";
14+
15+
const PLUGIN = 'esbuild-scss-modules-plugin'
16+
17+
type CssModulesOptions = Parameters<postcssModules>[0];
18+
export type PluginOptions = {
19+
inject: boolean,
20+
minify: boolean,
21+
cache: boolean,
22+
23+
localsConvention: CssModulesOptions["localsConvention"],
24+
generateScopedName: CssModulesOptions["generateScopedName"],
25+
26+
scssOptions: sass.Options,
27+
cssCallback?: (css: string, map: {[className: string]: string}) => void
28+
};
29+
const DefaultOptions: PluginOptions = {
30+
inject: true,
31+
minify: false,
32+
cache: true,
33+
34+
localsConvention: "camelCaseOnly",
35+
generateScopedName: undefined,
36+
37+
scssOptions: {},
38+
cssCallback: undefined
39+
}
40+
41+
async function buildScss(scssFullPath: string, sassOptions: sass.Options): Promise<sass.Result> {
42+
return new Promise((resolve, reject) => sass.render({
43+
file: scssFullPath,
44+
...sassOptions
45+
}, (err, result) => err ? reject(err) : resolve(result)))
46+
}
47+
48+
async function buildScssModulesJS(scssFullPath: string, options: PluginOptions): Promise<string> {
49+
const css = (await buildScss(scssFullPath, options.scssOptions)).css;
50+
51+
let cssModulesJSON = {};
52+
const result = await postcss([
53+
postcssModules.default({
54+
localsConvention: options.localsConvention,
55+
generateScopedName: options.generateScopedName,
56+
getJSON(cssSourceFile, json) {
57+
cssModulesJSON = { ...json };
58+
return cssModulesJSON;
59+
}
60+
}),
61+
...(options.minify ? [require("cssnano")({
62+
preset: 'default'
63+
})] : [])
64+
]).process(css, {
65+
from: scssFullPath,
66+
map: false
67+
});
68+
69+
if (options.cssCallback) await options.cssCallback(result.css, cssModulesJSON);
70+
71+
const classNames = JSON.stringify(cssModulesJSON);
72+
73+
const hash = crypto.createHash('sha256');
74+
hash.update(result.css);
75+
const digest = hash.digest('hex');
76+
77+
return `
78+
const digest = '${digest}';
79+
const classes = ${classNames};
80+
const css = \`${result.css}\`;
81+
${options.inject && `
82+
(function() {
83+
if (!document.getElementById(digest)) {
84+
var ele = document.createElement('style');
85+
ele.id = digest;
86+
ele.textContent = css;
87+
document.head.appendChild(ele);
88+
}
89+
})();
90+
`}
91+
export default classes;
92+
export { css, digest, classes };
93+
`;
94+
}
95+
96+
export const ScssModulesPlugin = (options: Partial<PluginOptions> = {}) => ({
97+
name: PLUGIN,
98+
setup(build) {
99+
const {outdir, bundle} = build.initialOptions;
100+
const results = new Map();
101+
const fullOptions = {...DefaultOptions, ...options};
102+
103+
build.onResolve(
104+
{ filter: /\.modules?\.scss$/, namespace: 'file' },
105+
async (args) => {
106+
const sourceFullPath = path.resolve(args.resolveDir, args.path);
107+
if (results.has(sourceFullPath)) return results.get(sourceFullPath);
108+
109+
const result = await (async () => {
110+
const sourceExt = path.extname(sourceFullPath);
111+
const sourceBaseName = path.basename(sourceFullPath, sourceExt);
112+
113+
const jsContent = await buildScssModulesJS(sourceFullPath, fullOptions);
114+
115+
if (bundle) {
116+
return {
117+
path: args.path,
118+
namespace: PLUGIN,
119+
pluginData: {
120+
content: jsContent
121+
}
122+
};
123+
}
124+
125+
if (outdir) {
126+
const isOutdirAbsolute = path.isAbsolute(outdir);
127+
const absoluteOutdir = isOutdirAbsolute ? outdir : path.resolve(args.resolveDir, outdir);
128+
const isEntryAbsolute = path.isAbsolute(args.path);
129+
const entryRelDir = isEntryAbsolute ? path.dirname(path.relative(args.resolveDir, args.path)) : path.dirname(args.path);
130+
131+
const targetSubpath = absoluteOutdir.indexOf(entryRelDir) === -1 ? path.join(entryRelDir, `${sourceBaseName}.css.js`) : `${sourceBaseName}.css.js`;
132+
const target = path.resolve(absoluteOutdir, targetSubpath);
133+
134+
await fs.mkdir(path.dirname(target), {recursive: true});
135+
await fs.writeFile(target, jsContent);
136+
}
137+
138+
return { path: sourceFullPath, namespace: 'file' };
139+
})();
140+
141+
if (fullOptions.cache) results.set(sourceFullPath, result);
142+
return result;
143+
}
144+
);
145+
146+
build.onLoad({ filter: /\.modules?\.scss$/, namespace: PLUGIN }, (args) => {
147+
return { contents: args.pluginData.content, loader: 'js' };
148+
});
149+
}
150+
} as esbuild.Plugin);
151+
152+
export default ScssModulesPlugin;
153+
154+
//@ts-expect-error
155+
declare module '*.modules.scss' {
156+
interface IClassNames {
157+
[className: string]: string
158+
}
159+
const classes: IClassNames;
160+
const digest: string;
161+
const css: string;
162+
163+
export default classes;
164+
export {classes, digest, css};
165+
}

0 commit comments

Comments
 (0)