Anoop 2 лет назад
Сommit
4ec47c764d
40 измененных файлов: 2707 добавлений и 0 удалений
  1. +6
    -0
      .gitignore
  2. +8
    -0
      .idea/.gitignore
  3. +49
    -0
      .idea/deployment.xml
  4. +8
    -0
      .idea/esbuild-plugin-postcss2.iml
  5. +6
    -0
      .idea/misc.xml
  6. +8
    -0
      .idea/modules.xml
  7. +6
    -0
      .idea/vcs.xml
  8. +1
    -0
      .npmignore
  9. +3
    -0
      .prettierignore
  10. +3
    -0
      .prettierrc
  11. +21
    -0
      LICENSE
  12. +89
    -0
      README.md
  13. +16
    -0
      build.js
  14. +180
    -0
      dist/index.esm.js
  15. +197
    -0
      dist/index.js
  16. +71
    -0
      package.json
  17. +275
    -0
      src/index.ts
  18. +25
    -0
      src/modules.d.ts
  19. +154
    -0
      test/index.js
  20. +3
    -0
      test/styles/basic.css
  21. +11
    -0
      test/styles/example.module.css
  22. +5
    -0
      test/styles/example.module.less
  23. +2
    -0
      test/styles/example.module.sass
  24. +3
    -0
      test/styles/preprocessors.less
  25. +6
    -0
      test/styles/preprocessors.sass
  26. +8
    -0
      test/styles/preprocessors.scss
  27. +4
    -0
      test/styles/preprocessors.styl
  28. +3
    -0
      test/styles/watch.css
  29. +5
    -0
      test/styles/watch2.css
  30. +3
    -0
      test/styles/watch3.css
  31. +1
    -0
      test/tests/basic.ts
  32. +5
    -0
      test/tests/modules.ts
  33. +1
    -0
      test/tests/node_modules.ts
  34. +4
    -0
      test/tests/preprocessors.ts
  35. +6
    -0
      test/tests/styles.css
  36. +6
    -0
      test/tests/styles2.css
  37. +1
    -0
      test/tests/watch.ts
  38. +1
    -0
      test/tests/watch2.ts
  39. +13
    -0
      tsconfig.json
  40. +1490
    -0
      yarn.lock

+ 6
- 0
.gitignore Просмотреть файл

@@ -0,0 +1,6 @@
node_modules
build
yarn-error.log
test/dist
package-lock.json
test/dist

+ 8
- 0
.idea/.gitignore Просмотреть файл

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

+ 49
- 0
.idea/deployment.xml Просмотреть файл

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PublishConfigData">
<serverData>
<paths name="MAGDY SERVER 6">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="MAGDY_SAPOS">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="MEMBTECH-OFFLINE-POS">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="MEMBTECH-STORE">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="MEMBTECH_COM">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="WOLF">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
</serverData>
</component>
</project>

+ 8
- 0
.idea/esbuild-plugin-postcss2.iml Просмотреть файл

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

+ 6
- 0
.idea/misc.xml Просмотреть файл

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
</project>

+ 8
- 0
.idea/modules.xml Просмотреть файл

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/esbuild-plugin-postcss2.iml" filepath="$PROJECT_DIR$/.idea/esbuild-plugin-postcss2.iml" />
</modules>
</component>
</project>

+ 6
- 0
.idea/vcs.xml Просмотреть файл

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

+ 1
- 0
.npmignore Просмотреть файл

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

+ 3
- 0
.prettierignore Просмотреть файл

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

+ 3
- 0
.prettierrc Просмотреть файл

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

+ 21
- 0
LICENSE Просмотреть файл

@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 Marton Lederer

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

+ 89
- 0
README.md Просмотреть файл

@@ -0,0 +1,89 @@
# esbuild-plugin-postcss2

This plugin is an optimized, type-friendly version of [esbuild-plugin-postcss](https://github.com/deanc/esbuild-plugin-postcss). It supports CSS preprocessors and CSS modules.

## Install

```sh
yarn add -D esbuild-plugin-postcss2
```

or

```sh
npm i -D esbuild-plugin-postcss2
```

## Usage

Add the plugin to your esbuild plugins:

```js
const esbuild = require("esbuild");
const postCssPlugin = require("esbuild-plugin-postcss2");

esbuild.build({
...
plugins: [
postCssPlugin.default()
]
...
});
```

### PostCSS plugins

Add your desired PostCSS plugin to the plugins array:

```js
const autoprefixer = require("autoprefixer");

esbuild.build({
...
plugins: [
postCssPlugin.default({
plugins: [autoprefixer]
})
]
...
});
```

### CSS modules

PostCSS modules are enabled by default. You can pass in a config or disable it with the `modules` field:

```js
postCssPlugin.default({
// pass in `postcss-modules` custom options
// set to false to disable
modules: {
getJSON(cssFileName, json, outputFileName) {
const path = require("path");
const cssName = path.basename(cssFileName, ".css");
const jsonFileName = path.resolve("./build/" + cssName + ".json");

fs.writeFileSync(jsonFileName, JSON.stringify(json));
}
}
});
```

As per standard any file having `module` before the extension (ie `somefile.module.css`) will be treated as a module.
The option `fileIsModule` allows to override this behavior.

```js
postCssPlugin.default({
// pass a custom `fileIsModule` option to tell whether a file should be treated as a module
// in this example we want everything to be a module except file finishing with `global.css`
fileIsModule: (filepath) => !filepath.endsWith(".global.css")
});
```

### Preprocessors

To use preprocessors (`sass`, `scss`, `stylus`, `less`), just add the desired preprocessor as a `devDependency`:

```sh
yarn add -D sass
```

+ 16
- 0
build.js Просмотреть файл

@@ -0,0 +1,16 @@
const { build } = require("esbuild"),
{ copyFile } = require("fs");

const production = process.env.NODE_ENV === "production",
formats = ["cjs", "esm"];

(async () => {
for (const format of formats) {
await build({
entryPoints: ["./src/index.ts"],
watch: !production,
format,
outfile: `./dist/index${format === "cjs" ? "" : "." + format}.js`
});
}
})();

+ 180
- 0
dist/index.esm.js Просмотреть файл

@@ -0,0 +1,180 @@
import {
ensureDir,
readFile,
readdirSync,
statSync,
writeFile
} from "fs-extra";
import {TextDecoder} from "util";
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";
import resolveFile from "resolve-file";
const defaultOptions = {
plugins: [],
modules: true,
rootDir: process.cwd(),
sassOptions: {},
lessOptions: {},
stylusOptions: {},
fileIsModule: null
};
const postCSSPlugin = ({
plugins = [],
modules = true,
rootDir = process.cwd(),
sassOptions = {},
lessOptions = {},
stylusOptions = {},
fileIsModule
} = defaultOptions) => ({
name: "postcss2",
setup(build) {
const tmpDirPath = tmp.dirSync().name, modulesMap = [];
const modulesPlugin = postcssModules({
generateScopedName: "[name]__[local]___[hash:base64:5]",
...typeof modules !== "boolean" ? modules : {},
getJSON(filepath, json, outpath) {
const mapIndex = modulesMap.findIndex((m) => m.path === filepath);
if (mapIndex !== -1) {
modulesMap[mapIndex].map = json;
} else {
modulesMap.push({
path: filepath,
map: json
});
}
if (typeof modules !== "boolean" && typeof modules.getJSON === "function")
return modules.getJSON(filepath, json, outpath);
}
});
build.onResolve({filter: /.\.(css|sass|scss|less|styl)$/}, async (args) => {
if (args.namespace !== "file" && args.namespace !== "")
return;
let sourceFullPath = resolveFile(args.path);
if (!sourceFullPath)
sourceFullPath = path.resolve(args.resolveDir, args.path);
const sourceExt = path.extname(sourceFullPath);
const sourceBaseName = path.basename(sourceFullPath, sourceExt);
const isModule = fileIsModule ? fileIsModule(sourceFullPath) : sourceBaseName.match(/\.module$/);
const sourceDir = path.dirname(sourceFullPath);
const watchFiles = [sourceFullPath];
let tmpFilePath;
if (args.kind === "entry-point") {
const sourceRelDir = path.relative(path.dirname(rootDir), path.dirname(sourceFullPath));
tmpFilePath = path.resolve(tmpDirPath, sourceRelDir, `${sourceBaseName}.css`);
await ensureDir(path.dirname(tmpFilePath));
} else {
const uniqueTmpDir = path.resolve(tmpDirPath, uniqueId());
tmpFilePath = path.resolve(uniqueTmpDir, `${sourceBaseName}.css`);
}
await ensureDir(path.dirname(tmpFilePath));
const fileContent = await readFile(sourceFullPath);
let css = sourceExt === ".css" ? fileContent : "";
if (sourceExt === ".sass" || sourceExt === ".scss") {
const sassResult = await renderSass({
...sassOptions,
file: sourceFullPath
});
css = sassResult.css.toString();
watchFiles.push(...sassResult.stats.includedFiles);
}
if (sourceExt === ".styl")
css = await renderStylus(new TextDecoder().decode(fileContent), {
...stylusOptions,
filename: sourceFullPath
});
if (sourceExt === ".less")
css = (await less.render(new TextDecoder().decode(fileContent), {
...lessOptions,
filename: sourceFullPath,
rootpath: path.dirname(args.path)
})).css;
const result = await postcss(isModule ? [modulesPlugin, ...plugins] : plugins).process(css, {
from: sourceFullPath,
to: tmpFilePath
});
watchFiles.push(...getPostCssDependencies(result.messages));
await writeFile(tmpFilePath, result.css);
return {
namespace: isModule ? "postcss-module" : "file",
path: tmpFilePath,
watchFiles,
pluginData: {
originalPath: sourceFullPath
}
};
});
build.onLoad({filter: /.*/, namespace: "postcss-module"}, async (args) => {
const mod = modulesMap.find(({path: path2}) => path2 === args?.pluginData?.originalPath), resolveDir = path.dirname(args.path);
return {
resolveDir,
contents: `import ${JSON.stringify(args.path)};
export default ${JSON.stringify(mod && mod.map ? mod.map : {})};`
};
});
}
});
function renderSass(options) {
return new Promise((resolve, reject) => {
getSassImpl().render(options, (e, res) => {
if (e)
reject(e);
else
resolve(res);
});
});
}
function renderStylus(str, options) {
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);
}
function getFilesRecursive(directory) {
return readdirSync(directory).reduce((files, file) => {
const name = path.join(directory, file);
return statSync(name).isDirectory() ? [...files, ...getFilesRecursive(name)] : [...files, name];
}, []);
}
let idCounter = 0;
function uniqueId() {
return Date.now().toString(16) + (idCounter++).toString(16);
}
function getPostCssDependencies(messages) {
let dependencies = [];
for (const message of messages) {
if (message.type == "dir-dependency") {
dependencies.push(...getFilesRecursive(message.dir));
} else if (message.type == "dependency") {
dependencies.push(message.file);
}
}
return dependencies;
}
var src_default = postCSSPlugin;
export {
src_default as default,
defaultOptions
};

+ 197
- 0
dist/index.js Просмотреть файл

@@ -0,0 +1,197 @@
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __markAsModule = (target) => __defProp(target, "__esModule", {value: true});
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {get: all[name], enumerable: true});
};
var __exportStar = (target, module2, desc) => {
if (module2 && typeof module2 === "object" || typeof module2 === "function") {
for (let key of __getOwnPropNames(module2))
if (!__hasOwnProp.call(target, key) && key !== "default")
__defProp(target, key, {get: () => module2[key], enumerable: !(desc = __getOwnPropDesc(module2, key)) || desc.enumerable});
}
return target;
};
var __toModule = (module2) => {
return __exportStar(__markAsModule(__defProp(module2 != null ? __create(__getProtoOf(module2)) : {}, "default", module2 && module2.__esModule && "default" in module2 ? {get: () => module2.default, enumerable: true} : {value: module2, enumerable: true})), module2);
};
__markAsModule(exports);
__export(exports, {
default: () => src_default,
defaultOptions: () => defaultOptions
});
var import_fs_extra = __toModule(require("fs-extra"));
var import_util = __toModule(require("util"));
var import_path = __toModule(require("path"));
var import_tmp = __toModule(require("tmp"));
var import_postcss2 = __toModule(require("postcss"));
var import_postcss_modules = __toModule(require("postcss-modules"));
var import_less = __toModule(require("less"));
var import_stylus = __toModule(require("stylus"));
var import_resolve_file = __toModule(require("resolve-file"));
const defaultOptions = {
plugins: [],
modules: true,
rootDir: process.cwd(),
sassOptions: {},
lessOptions: {},
stylusOptions: {},
fileIsModule: null
};
const postCSSPlugin = ({
plugins = [],
modules = true,
rootDir = process.cwd(),
sassOptions = {},
lessOptions = {},
stylusOptions = {},
fileIsModule
} = defaultOptions) => ({
name: "postcss2",
setup(build) {
const tmpDirPath = import_tmp.default.dirSync().name, modulesMap = [];
const modulesPlugin = (0, import_postcss_modules.default)({
generateScopedName: "[name]__[local]___[hash:base64:5]",
...typeof modules !== "boolean" ? modules : {},
getJSON(filepath, json, outpath) {
const mapIndex = modulesMap.findIndex((m) => m.path === filepath);
if (mapIndex !== -1) {
modulesMap[mapIndex].map = json;
} else {
modulesMap.push({
path: filepath,
map: json
});
}
if (typeof modules !== "boolean" && typeof modules.getJSON === "function")
return modules.getJSON(filepath, json, outpath);
}
});
build.onResolve({filter: /.\.(css|sass|scss|less|styl)$/}, async (args) => {
if (args.namespace !== "file" && args.namespace !== "")
return;
let sourceFullPath = (0, import_resolve_file.default)(args.path);
if (!sourceFullPath)
sourceFullPath = import_path.default.resolve(args.resolveDir, args.path);
const sourceExt = import_path.default.extname(sourceFullPath);
const sourceBaseName = import_path.default.basename(sourceFullPath, sourceExt);
const isModule = fileIsModule ? fileIsModule(sourceFullPath) : sourceBaseName.match(/\.module$/);
const sourceDir = import_path.default.dirname(sourceFullPath);
const watchFiles = [sourceFullPath];
let tmpFilePath;
if (args.kind === "entry-point") {
const sourceRelDir = import_path.default.relative(import_path.default.dirname(rootDir), import_path.default.dirname(sourceFullPath));
tmpFilePath = import_path.default.resolve(tmpDirPath, sourceRelDir, `${sourceBaseName}.css`);
await (0, import_fs_extra.ensureDir)(import_path.default.dirname(tmpFilePath));
} else {
const uniqueTmpDir = import_path.default.resolve(tmpDirPath, uniqueId());
tmpFilePath = import_path.default.resolve(uniqueTmpDir, `${sourceBaseName}.css`);
}
await (0, import_fs_extra.ensureDir)(import_path.default.dirname(tmpFilePath));
const fileContent = await (0, import_fs_extra.readFile)(sourceFullPath);
let css = sourceExt === ".css" ? fileContent : "";
if (sourceExt === ".sass" || sourceExt === ".scss") {
const sassResult = await renderSass({
...sassOptions,
file: sourceFullPath
});
css = sassResult.css.toString();
watchFiles.push(...sassResult.stats.includedFiles);
}
if (sourceExt === ".styl")
css = await renderStylus(new import_util.TextDecoder().decode(fileContent), {
...stylusOptions,
filename: sourceFullPath
});
if (sourceExt === ".less")
css = (await import_less.default.render(new import_util.TextDecoder().decode(fileContent), {
...lessOptions,
filename: sourceFullPath,
rootpath: import_path.default.dirname(args.path)
})).css;
const result = await (0, import_postcss2.default)(isModule ? [modulesPlugin, ...plugins] : plugins).process(css, {
from: sourceFullPath,
to: tmpFilePath
});
watchFiles.push(...getPostCssDependencies(result.messages));
await (0, import_fs_extra.writeFile)(tmpFilePath, result.css);
return {
namespace: isModule ? "postcss-module" : "file",
path: tmpFilePath,
watchFiles,
pluginData: {
originalPath: sourceFullPath
}
};
});
build.onLoad({filter: /.*/, namespace: "postcss-module"}, async (args) => {
const mod = modulesMap.find(({path: path2}) => path2 === args?.pluginData?.originalPath), resolveDir = import_path.default.dirname(args.path);
return {
resolveDir,
contents: `import ${JSON.stringify(args.path)};
export default ${JSON.stringify(mod && mod.map ? mod.map : {})};`
};
});
}
});
function renderSass(options) {
return new Promise((resolve, reject) => {
getSassImpl().render(options, (e, res) => {
if (e)
reject(e);
else
resolve(res);
});
});
}
function renderStylus(str, options) {
return new Promise((resolve, reject) => {
import_stylus.default.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);
}
function getFilesRecursive(directory) {
return (0, import_fs_extra.readdirSync)(directory).reduce((files, file) => {
const name = import_path.default.join(directory, file);
return (0, import_fs_extra.statSync)(name).isDirectory() ? [...files, ...getFilesRecursive(name)] : [...files, name];
}, []);
}
let idCounter = 0;
function uniqueId() {
return Date.now().toString(16) + (idCounter++).toString(16);
}
function getPostCssDependencies(messages) {
let dependencies = [];
for (const message of messages) {
if (message.type == "dir-dependency") {
dependencies.push(...getFilesRecursive(message.dir));
} else if (message.type == "dependency") {
dependencies.push(message.file);
}
}
return dependencies;
}
var src_default = postCSSPlugin;

+ 71
- 0
package.json Просмотреть файл

@@ -0,0 +1,71 @@
{
"name": "@xhiveframework/esbuild-plugin-postcss2",
"version": "0.1.3",
"description": "Use postcss with esbuild",
"repository": {
"type": "git",
"url": "git+https://github.com/xhiveframework/esbuild-plugin-postcss2.git"
},
"author": "Marton Lederer <marton@lederer.hu>",
"license": "MIT",
"private": false,
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "cross-env NODE_ENV=production node build.js",
"dev": "cross-env NODE_ENV=development node build.js",
"test": "yarn build && 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",
"src/modules.d.ts"
],
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "src/modules.d.ts",
"dependencies": {
"autoprefixer": "^10.2.5",
"fs-extra": "^9.1.0",
"less": "^4.x",
"postcss-modules": "^4.0.0",
"resolve-file": "^0.3.0",
"sass": "^1.x",
"stylus": "^0.x",
"tmp": "^0.2.1"
},
"devDependencies": {
"@types/chai": "^4.2.15",
"@types/fs-extra": "^9.0.9",
"@types/less": "^3.0.2",
"@types/mocha": "^8.2.2",
"@types/node": "^14.14.37",
"@types/sass": "^1.16.0",
"@types/stylus": "^0.48.33",
"@types/tmp": "^0.2.0",
"chai": "^4.3.4",
"cross-env": "^7.0.3",
"esbuild": "^0.11.2",
"mocha": "^8.3.2",
"normalize.css": "^8.0.1",
"postcss-import": "^14.0.2",
"prettier": "^2.2.1",
"typescript": "^4.2.3",
"yorkie": "^2.0.0"
},
"peerDependencies": {
"less": "^4.x",
"postcss": "8.x",
"sass": "^1.x",
"stylus": "^0.x"
},
"bugs": {
"url": "https://github.com/xhiveframework/esbuild-plugin-postcss2/issues"
},
"homepage": "https://github.com/xhiveframework/esbuild-plugin-postcss2#readme"
}

+ 275
- 0
src/index.ts Просмотреть файл

@@ -0,0 +1,275 @@
import { Plugin } from "esbuild";
import { Plugin as PostCSSPlugin, Message } from "postcss";
import {
ensureDir,
readFile,
readdirSync,
statSync,
writeFile
} from "fs-extra";
import { TextDecoder } from "util";
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";
import resolveFile from "resolve-file";

type StylusRenderOptions = Parameters<typeof stylus.render>[1]; // The Stylus.RenderOptions interface doesn't seem to be exported... So next best

interface PostCSSPluginOptions {
plugins: PostCSSPlugin[];
modules: boolean | any;
rootDir?: string;
sassOptions?: SassOptions;
lessOptions?: Less.Options;
stylusOptions?: StylusRenderOptions;
fileIsModule?: (filename: string) => boolean;
}

interface CSSModule {
path: string;
map: {
[key: string]: string;
};
}

export const defaultOptions: PostCSSPluginOptions = {
plugins: [],
modules: true,
rootDir: process.cwd(),
sassOptions: {},
lessOptions: {},
stylusOptions: {},
fileIsModule: null
};

const postCSSPlugin = ({
plugins = [],
modules = true,
rootDir = process.cwd(),
sassOptions = {},
lessOptions = {},
stylusOptions = {},
fileIsModule
}: PostCSSPluginOptions = defaultOptions): Plugin => ({
name: "postcss2",
setup(build) {
// get a temporary path where we can save compiled CSS
const tmpDirPath = tmp.dirSync().name,
modulesMap: CSSModule[] = [];

const modulesPlugin = postcssModules({
generateScopedName: "[name]__[local]___[hash:base64:5]",
...(typeof modules !== "boolean" ? modules : {}),
getJSON(filepath, json, outpath) {
// Make sure to replace json map instead of pushing new map everytime with edit file on watch
const mapIndex = modulesMap.findIndex((m) => m.path === filepath);
if (mapIndex !== -1) {
modulesMap[mapIndex].map = json;
} else {
modulesMap.push({
path: filepath,
map: json
});
}

if (
typeof modules !== "boolean" &&
typeof modules.getJSON === "function"
)
return modules.getJSON(filepath, json, outpath);
}
});

build.onResolve(
{ filter: /.\.(css|sass|scss|less|styl)$/ },
async (args) => {
// Namespace is empty when using CSS as an entrypoint
if (args.namespace !== "file" && args.namespace !== "") return;

// Resolve files from node_modules (ex: npm install normalize.css)
let sourceFullPath = resolveFile(args.path);
if (!sourceFullPath)
sourceFullPath = path.resolve(args.resolveDir, args.path);

const sourceExt = path.extname(sourceFullPath);
const sourceBaseName = path.basename(sourceFullPath, sourceExt);
const isModule = fileIsModule
? fileIsModule(sourceFullPath)
: sourceBaseName.match(/\.module$/);
const sourceDir = path.dirname(sourceFullPath);
const watchFiles = [sourceFullPath];

let tmpFilePath: string;
if (args.kind === "entry-point") {
// For entry points, we use <tempdir>/<path-within-project-root>/<file-name>.css
const sourceRelDir = path.relative(
path.dirname(rootDir),
path.dirname(sourceFullPath)
);
tmpFilePath = path.resolve(
tmpDirPath,
sourceRelDir,
`${sourceBaseName}.css`
);
await ensureDir(path.dirname(tmpFilePath));
} else {
// For others, we use <tempdir>/<unique-directory-name>/<file-name>.css
//
// This is a workaround for the following esbuild issue:
// https://github.com/evanw/esbuild/issues/1101
//
// esbuild is unable to find the file, even though it does exist. This only
// happens for files in a directory with several other entries, so by
// creating a unique directory name per file on every build, we guarantee
// that there will only every be a single file present within the directory,
// circumventing the esbuild issue.
const uniqueTmpDir = path.resolve(tmpDirPath, uniqueId());
tmpFilePath = path.resolve(uniqueTmpDir, `${sourceBaseName}.css`);
}
await ensureDir(path.dirname(tmpFilePath));

const fileContent = await readFile(sourceFullPath);
let css = sourceExt === ".css" ? fileContent : "";

// parse files with preprocessors
if (sourceExt === ".sass" || sourceExt === ".scss") {
const sassResult = await renderSass({
...sassOptions,
file: sourceFullPath
});
css = sassResult.css.toString();
watchFiles.push(...sassResult.stats.includedFiles);
}
if (sourceExt === ".styl")
css = await renderStylus(new TextDecoder().decode(fileContent), {
...stylusOptions,
filename: sourceFullPath
});
if (sourceExt === ".less")
css = (
await less.render(new TextDecoder().decode(fileContent), {
...lessOptions,
filename: sourceFullPath,
rootpath: path.dirname(args.path)
})
).css;

// wait for plugins to complete parsing & get result
const result = await postcss(
isModule ? [modulesPlugin, ...plugins] : plugins
).process(css, {
from: sourceFullPath,
to: tmpFilePath
});
watchFiles.push(...getPostCssDependencies(result.messages));

// Write result CSS
await writeFile(tmpFilePath, result.css);

return {
namespace: isModule ? "postcss-module" : "file",
path: tmpFilePath,
watchFiles,
pluginData: {
originalPath: sourceFullPath
}
};
}
);

// load css modules
build.onLoad(
{ filter: /.*/, namespace: "postcss-module" },
async (args) => {
const mod = modulesMap.find(
({ path }) => path === args?.pluginData?.originalPath
),
resolveDir = path.dirname(args.path);

return {
resolveDir,
contents: `import ${JSON.stringify(
args.path
)};\nexport default ${JSON.stringify(mod && mod.map ? mod.map : {})};`
};
}
);
}
});

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: StylusRenderOptions
): 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);
}

function getFilesRecursive(directory: string): string[] {
return readdirSync(directory).reduce((files, file) => {
const name = path.join(directory, file);

return statSync(name).isDirectory()
? [...files, ...getFilesRecursive(name)]
: [...files, name];
}, []);
}

let idCounter = 0;

/**
* Generates an id that is guaranteed to be unique for the Node.JS instance.
*/
function uniqueId(): string {
return Date.now().toString(16) + (idCounter++).toString(16);
}

function getPostCssDependencies(messages: Message[]): string[] {
let dependencies = [];
for (const message of messages) {
if (message.type == "dir-dependency") {
dependencies.push(...getFilesRecursive(message.dir));
} else if (message.type == "dependency") {
dependencies.push(message.file);
}
}
return dependencies;
}

export default postCSSPlugin;

+ 25
- 0
src/modules.d.ts Просмотреть файл

@@ -0,0 +1,25 @@
// 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;
}

declare module "*.module.less" {
const classes: { readonly [key: string]: string };
export default classes;
}

declare module "*.module.styl" {
const classes: { readonly [key: string]: string };
export default classes;
}

+ 154
- 0
test/index.js Просмотреть файл

@@ -0,0 +1,154 @@
const autoprefixer = require("autoprefixer"),
postCssImport = require("postcss-import"),
{ build } = require("esbuild"),
postCSS = require("../dist"),
{ assert } = require("chai"),
fs = require("fs");

describe("PostCSS esbuild tests", () => {
it("Works with basic CSS imports", (done) => {
test(["tests/basic.ts"])
.then((res) => {
assert(res);
done();
})
.catch(done);
});
it("Works with preprocessors", (done) => {
test(["tests/preprocessors.ts"])
.then((res) => {
assert(res);
done();
})
.catch(done);
});
it("Works with CSS modules", (done) => {
test(["tests/modules.ts"])
.then((res) => {
assert(res);
done();
})
.catch(done);
});
it("Works with CSS as entrypoint", (done) => {
test(["tests/styles.css", "tests/styles2.css"])
.then((res) => {
assert(res);
done();
})
.catch(done);
});
it("Works with node_modules import", (done) => {
test(["tests/node_modules.ts"])
.then((res) => {
assert(res);
done();
})
.catch(done);
});
it("Works while waching css files directly", (done) => {
let notTriggerTimeout = null;
build({
entryPoints: ["tests/watch.ts"],
bundle: true,
outdir: "dist",
watch: {
onRebuild: (error, result) => {
notTriggerTimeout = null;
if (error) return done(error);
assert(result);
done();
}
},
plugins: [
postCSS.default({
plugins: [autoprefixer]
})
]
})
.then(() => {
// test if modifying the css actually triggers the onRebuild event
const data = `.Test { display: block; }`;
fs.writeFile("./styles/watch.css", data, (err) => {
if (err) return done(err);
notTriggerTimeout = setTimeout(() => {
done("Watch file not triggered!");
}, 1000);
});
})
.catch(() => process.exit(1));
});

it("Works while waching css files through dependencies", (done) => {
let notTriggerTimeout = null;
build({
entryPoints: ["tests/watch2.ts"],
bundle: true,
outdir: "dist",
watch: {
onRebuild: (error, result) => {
notTriggerTimeout = null;
if (error) return done(error);
assert(result);
done();
}
},
plugins: [
postCSS.default({
plugins: [autoprefixer, postCssImport]
})
]
})
.then(() => {
// test if modifying the css actually triggers the onRebuild event
const data = `.Test { display: block; }`;
fs.writeFile("./styles/watch3.css", data, (err) => {
if (err) return done(err);
notTriggerTimeout = setTimeout(() => {
done("Watch file not triggered!");
}, 1000);
});
})
.catch(() => process.exit(1));
});

it("Works with custom module function", (done) => {
let testFilename = null;
build({
entryPoints: ["tests/basic.ts"],
bundle: true,
outdir: "dist",
plugins: [
postCSS.default({
plugins: [autoprefixer, postCssImport],
modules: true,
fileIsModule: (filename) => {
testFilename = filename;
return false;
}
})
]
})
.then(() => {
// ensure the proper filename was passed
assert.match(testFilename, /styles\/basic\.css/);
})
.catch((e) => {
console.error(e);
process.exit(1);
});
});
});

function test(entryPoint) {
return build({
entryPoints: entryPoint,
bundle: true,
outdir: "dist",
plugins: [
postCSS.default({
plugins: [autoprefixer]
})
]
}).catch(() => process.exit(1));
}

+ 3
- 0
test/styles/basic.css Просмотреть файл

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

+ 11
- 0
test/styles/example.module.css Просмотреть файл

@@ -0,0 +1,11 @@
.TestModule {
align-items: center;
}

.TestModuleAnother {
justify-content: space-between;
}

.TextModuleLast {
width: 100vw;
}

+ 5
- 0
test/styles/example.module.less Просмотреть файл

@@ -0,0 +1,5 @@
@text: left;

.TestLessModule {
text-align: @text;
}

+ 2
- 0
test/styles/example.module.sass Просмотреть файл

@@ -0,0 +1,2 @@
.TestModuleSass
display: flex

+ 3
- 0
test/styles/preprocessors.less Просмотреть файл

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

+ 6
- 0
test/styles/preprocessors.sass Просмотреть файл

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

+ 8
- 0
test/styles/preprocessors.scss Просмотреть файл

@@ -0,0 +1,8 @@
$test: translate(-50%, -50%);

.ScssClass {
position: relative;
top: 50%;
left: 50%;
transform: $test;
}

+ 4
- 0
test/styles/preprocessors.styl Просмотреть файл

@@ -0,0 +1,4 @@
test = 25px

.StylusClass
margin test

+ 3
- 0
test/styles/watch.css Просмотреть файл

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

+ 5
- 0
test/styles/watch2.css Просмотреть файл

@@ -0,0 +1,5 @@
@import "./watch3.css";

.Test {
display: block;
}

+ 3
- 0
test/styles/watch3.css Просмотреть файл

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

+ 1
- 0
test/tests/basic.ts Просмотреть файл

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

+ 5
- 0
test/tests/modules.ts Просмотреть файл

@@ -0,0 +1,5 @@
import styles from "../styles/example.module.sass";
import styles2 from "../styles/example.module.css";
import styles3 from "../styles/example.module.less";

console.log(styles, styles2, styles3);

+ 1
- 0
test/tests/node_modules.ts Просмотреть файл

@@ -0,0 +1 @@
import "normalize.css";

+ 4
- 0
test/tests/preprocessors.ts Просмотреть файл

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

+ 6
- 0
test/tests/styles.css Просмотреть файл

@@ -0,0 +1,6 @@
.example {
display: grid;
transition: all 0.5s;
user-select: none;
background: linear-gradient(to bottom, white, black);
}

+ 6
- 0
test/tests/styles2.css Просмотреть файл

@@ -0,0 +1,6 @@
.example {
display: grid;
transition: all 0.5s;
user-select: none;
background: linear-gradient(to bottom, white, black);
}

+ 1
- 0
test/tests/watch.ts Просмотреть файл

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

+ 1
- 0
test/tests/watch2.ts Просмотреть файл

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

+ 13
- 0
tsconfig.json Просмотреть файл

@@ -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"]
}

+ 1490
- 0
yarn.lock
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


Загрузка…
Отмена
Сохранить