chore: basic plugin

This commit is contained in:
Marton Lederer 2021-02-20 11:13:24 +01:00
commit 8778057fd7
No known key found for this signature in database
GPG Key ID: 9B7FD44832ADFE95
18 changed files with 1597 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
dist
build
yarn-error.log
test/dist

1
.npmignore Normal file
View File

@ -0,0 +1 @@
dist/test

3
.prettierignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
.github
dist

3
.prettierrc Normal file
View File

@ -0,0 +1,3 @@
{
"trailingComma": "none"
}

17
build.js Normal file
View File

@ -0,0 +1,17 @@
const { build } = require("esbuild"),
{ copyFile } = require("fs");
const production = process.env.NODE_ENV === "production";
build({
entryPoints: ["./src/index.ts"],
watch: !production,
format: "cjs",
outfile: `./dist/index.js`
}).catch(() => process.exit(1));
// copy module declarations
copyFile("./src/modules.d.ts", "./dist/modules.d.ts", (err) => {
if (err) throw err;
console.log("[modules.d.ts] copied");
});

60
package.json Normal file
View File

@ -0,0 +1,60 @@
{
"name": "esbuild-plugin-postcss2",
"version": "0.0.1",
"description": "Use postcss with esbuild",
"repository": "https://github.com/martonlederer/esbuild-plugin-postcss2",
"author": "Marton Lederer <marton@lederer.hu>",
"license": "MIT",
"private": false,
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "NODE_ENV=production node build.js && tsc --emitDeclarationOnly",
"dev": "NODE_ENV=development node build.js",
"test": "cd test && mocha 'index.js' --no-timeout --exit",
"fmt": "prettier --write .",
"fmt:check": "prettier --check ."
},
"gitHooks": {
"pre-commit": "prettier --write . && git add -A"
},
"files": [
"dist"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"dependencies": {
"autoprefixer": "^10.2.4",
"fs-extra": "^9.1.0",
"less": "^4.1.1",
"postcss": "^8.2.6",
"postcss-modules": "^4.0.0",
"sass": "^1.32.8",
"stylus": "^0.54.8",
"tmp": "^0.2.1"
},
"devDependencies": {
"@types/chai": "^4.2.15",
"@types/fs-extra": "^9.0.7",
"@types/less": "^3.0.2",
"@types/mocha": "^8.2.1",
"@types/node": "^14.14.28",
"@types/sass": "^1.16.0",
"@types/stylus": "^0.48.33",
"@types/tmp": "^0.2.0",
"chai": "^4.3.0",
"esbuild": "^0.8.49",
"mocha": "^8.3.0",
"prettier": "^2.2.1",
"typescript": "^4.1.5",
"yorkie": "^2.0.0"
},
"peerDependencies": {
"esbuild": "^0.8.46",
"less": "^4.x",
"postcss": "8.x",
"sass": "^1.x",
"stylus": "^0.x"
}
}

148
src/index.ts Normal file
View File

@ -0,0 +1,148 @@
import { Plugin } from "esbuild";
import { Plugin as PostCSSPlugin } from "postcss";
import { ensureDir, readFile, writeFile } from "fs-extra";
import {
SassException,
Result as SassResult,
Options as SassOptions
} from "sass";
import path from "path";
import tmp from "tmp";
import postcss from "postcss";
import postcssModules from "postcss-modules";
import less from "less";
import stylus from "stylus";
interface PostCSSPluginOptions {
plugins: PostCSSPlugin[];
modules: boolean | any;
rootDir?: string;
}
interface CSSModule {
path: string;
map: {
[key: string]: string;
};
}
const postCSSPlugin = ({
plugins = [],
modules = true,
rootDir = process.cwd()
}: PostCSSPluginOptions): Plugin => ({
name: "postcss2",
setup(build) {
// get a temporary path where we can save compiled CSS
const tmpDirPath = tmp.dirSync().name,
modulesMap: CSSModule[] = [];
build.onResolve(
{ filter: /.\.(css|sass|scss|less|styl)$/, namespace: "file" },
async (args) => {
const sourceFullPath = path.resolve(args.resolveDir, args.path),
sourceExt = path.extname(sourceFullPath),
sourceBaseName = path.basename(sourceFullPath, sourceExt),
sourceDir = path.dirname(sourceFullPath),
sourceRelDir = path.relative(path.dirname(rootDir), sourceDir),
isModule = sourceBaseName.match(/\.module$/),
tmpDir = path.resolve(tmpDirPath, sourceRelDir),
tmpFilePath = path.resolve(
tmpDir,
`${sourceBaseName}-tmp-${Date.now()}${
isModule ? ".module" : ""
}.css`
);
await ensureDir(tmpDir);
const fileContent = await readFile(sourceFullPath);
let css = sourceExt === ".css" ? fileContent : "";
// parse css modules with postcss-modules
if (modules !== false && isModule) {
plugins.unshift(
postcssModules({
...(typeof modules !== "boolean" ? modules : {}),
getJSON(filepath, json, outpath) {
modulesMap.push({
path: tmpFilePath,
map: json
});
if (
typeof modules !== "boolean" &&
typeof modules.getJSON === "function"
)
return modules.getJSON(filepath, json, outpath);
}
})
);
}
// parse files with preprocessors
if (sourceExt === ".sass" || sourceExt === ".scss")
css = (await renderSass({ file: sourceFullPath })).css.toString();
if (sourceExt === ".styl")
css = await renderStylus(new TextDecoder().decode(fileContent), {
filename: sourceFullPath
});
if (sourceExt === ".less")
css = (
await less.render(new TextDecoder().decode(fileContent), {
filename: sourceFullPath,
rootpath: path.dirname(args.path)
})
).css;
// wait for plugins to complete parsing & get result
const result = await postcss(plugins).process(css, {
from: sourceFullPath,
to: tmpFilePath
});
// Write result CSS
await writeFile(tmpFilePath, result.css);
return {
path: tmpFilePath
};
}
);
}
});
function renderSass(options: SassOptions): Promise<SassResult> {
return new Promise((resolve, reject) => {
getSassImpl().render(options, (e: SassException, res: SassResult) => {
if (e) reject(e);
else resolve(res);
});
});
}
function renderStylus(str: string, options): Promise<string> {
return new Promise((resolve, reject) => {
stylus.render(str, options, (e, res) => {
if (e) reject(e);
else resolve(res);
});
});
}
function getSassImpl() {
let impl = "sass";
try {
require.resolve("sass");
} catch {
try {
require.resolve("node-sass");
impl = "node-sass";
} catch {
throw new Error('Please install "sass" or "node-sass" package');
}
}
return require(impl);
}
export default postCSSPlugin;

15
src/modules.d.ts vendored Normal file
View File

@ -0,0 +1,15 @@
// css module files
declare module "*.module.css" {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module "*.module.scss" {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module "*.module.sass" {
const classes: { readonly [key: string]: string };
export default classes;
}

36
test/index.js Normal file
View File

@ -0,0 +1,36 @@
const autoprefixer = require("autoprefixer"),
{ build } = require("esbuild"),
postCSS = require("../dist"),
{ assert } = require("chai");
describe("PostCSS esbuild tests", () => {
it("Works with basic CSS imports", (done) => {
test("basic")
.then((res) => {
assert(res);
done();
})
.catch(done);
});
it("Works with preprocessors", (done) => {
test("preprocessors")
.then((res) => {
assert(res);
done();
})
.catch(done);
});
});
function test(test) {
return build({
entryPoints: [`tests/${test}.ts`],
bundle: true,
outdir: "dist",
plugins: [
postCSS.default({
plugins: [autoprefixer]
})
]
}).catch(() => process.exit(1));
}

3
test/styles/basic.css Normal file
View File

@ -0,0 +1,3 @@
.Test {
display: block;
}

View File

@ -0,0 +1,5 @@
@test: 2em;
.LessClass {
height: @test;
}

View File

@ -0,0 +1,6 @@
$test: 20px
.SassClass
text-transform: uppercase
display: flex
font-size: $test

View File

@ -0,0 +1,8 @@
$test: translate(-50%, -50%);
.ScssClass {
position: relative;
top: 50%;
left: 50%;
transform: $test;
}

View File

@ -0,0 +1,4 @@
test = 25px
.StylusClass
margin test

1
test/tests/basic.ts Normal file
View File

@ -0,0 +1 @@
import "../styles/basic.css";

View File

@ -0,0 +1,4 @@
import "../styles/preprocessors.sass";
import "../styles/preprocessors.scss";
import "../styles/preprocessors.less";
import "../styles/preprocessors.styl";

13
tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"esModuleInterop": true,
"moduleResolution": "node",
"module": "commonjs",
"target": "es2017",
"outDir": "dist",
"declaration": true,
"rootDir": "src"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

1265
yarn.lock Normal file

File diff suppressed because it is too large Load Diff