Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,24 @@ To use preprocessors (`sass`, `scss`, `stylus`, `less`), just add the desired pr
```sh
yarn add -D sass
```

### Enable cache

To reduce the rebuild time in dev mode, you can try to cache the CSS compilation result.

> Note: currently it only supports `.css/.less/.scss`, doesn't support `.styl` yet.

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

esbuild.build({
...
plugins: [
postCssPlugin.default({
enableCache: true
})
]
...
});
```
241 changes: 165 additions & 76 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
readFile,
readdirSync,
statSync,
stat,
writeFile
} from "fs-extra";
import { TextDecoder } from "util";
Expand All @@ -31,6 +32,7 @@ interface PostCSSPluginOptions {
lessOptions?: Less.Options;
stylusOptions?: StylusRenderOptions;
writeToFile?: boolean;
enableCache?: boolean;
}

interface CSSModule {
Expand All @@ -40,6 +42,13 @@ interface CSSModule {
};
}

interface CacheVal {
lastMtimeMs: number;
depFiles: string[];
outputPath: string;
outputCss: string;
}

export const defaultOptions: PostCSSPluginOptions = {
plugins: [],
modules: true,
Expand All @@ -48,22 +57,24 @@ export const defaultOptions: PostCSSPluginOptions = {
lessOptions: {},
stylusOptions: {},
writeToFile: true
};
}

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

const modulesPlugin = postcssModules({
generateScopedName: "[name]__[local]___[hash:base64:5]",
Expand Down Expand Up @@ -102,70 +113,93 @@ const postCSSPlugin = ({
const sourceExt = path.extname(sourceFullPath);
const sourceBaseName = path.basename(sourceFullPath, sourceExt);
const isModule = sourceBaseName.match(/\.module$/);
const sourceDir = path.dirname(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`
);

const cacheVal = await queryCache(sourceFullPath, cache);
if (cacheVal.outputPath === "") {
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));
} 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")
css = (
await renderSass({ ...sassOptions, file: sourceFullPath })
).css.toString();
if (sourceExt === ".styl")
css = await renderStylus(new TextDecoder().decode(fileContent), {
...stylusOptions,
filename: sourceFullPath

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

// parse files with preprocessors
if (sourceExt === ".sass" || sourceExt === ".scss") {
const ret = await renderSass({
...sassOptions,
file: sourceFullPath
});
css = ret.css.toString();
cacheVal.depFiles = ret.stats.includedFiles;
}
if (sourceExt === ".styl") {
css = await renderStylus(new TextDecoder().decode(fileContent), {
...stylusOptions,
filename: sourceFullPath
});
// TODO: how to get .styl dependent files
}
if (sourceExt === ".less") {
const ret = await less.render(
new TextDecoder().decode(fileContent),
{
...lessOptions,
filename: sourceFullPath,
rootpath: path.dirname(args.path)
}
);
css = ret.css;
cacheVal.depFiles = ret.imports;
}

// wait for plugins to complete parsing & get result
const result = await postcss(
isModule ? [modulesPlugin, ...plugins] : plugins
).process(css, {
from: sourceFullPath,
to: tmpFilePath
});
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
});

// Write result CSS
if (writeToFile) {
await writeFile(tmpFilePath, result.css);
cacheVal.depFiles = cacheVal.depFiles.concat(
getPostCssDependencies(result.messages)
);

cacheVal.outputPath = tmpFilePath;
cacheVal.outputCss = result.css;
// Save cache
if (enableCache) {
cache.set(sourceFullPath, cacheVal);
await updateDepFilesCache(cacheVal.depFiles, cache);
}

// Write result CSS
if (writeToFile) {
await writeFile(tmpFilePath, result.css);
}
}

return {
Expand All @@ -174,13 +208,11 @@ const postCSSPlugin = ({
: writeToFile
? "file"
: "postcss-text",
path: tmpFilePath,
watchFiles: [result.opts.from].concat(
getPostCssDependencies(result.messages)
),
path: cacheVal.outputPath,
watchFiles: [sourceFullPath].concat(cacheVal.depFiles),
pluginData: {
originalPath: sourceFullPath,
css: result.css
css: cacheVal.outputCss
}
};
}
Expand Down Expand Up @@ -287,4 +319,61 @@ function getPostCssDependencies(messages: Message[]): string[] {
return dependencies;
}

async function queryCache(
sourceFullPath: string,
cache: Map<string, CacheVal>
): Promise<CacheVal> {
const fileStat = await stat(sourceFullPath);
// fileStat: Stats {
// ...
// mtimeMs: 1634030364414,
// mtime: 2021-10-12T09:19:24.414Z,
// ...
// }
const newCacheVal: CacheVal = {
lastMtimeMs: fileStat.mtimeMs,
depFiles: [],
outputPath: "",
outputCss: ""
};

let cacheVal = cache.get(sourceFullPath);
if (cacheVal === undefined) {
return newCacheVal;
}
if (cacheVal.lastMtimeMs !== fileStat.mtimeMs) {
return newCacheVal;
}

// check dependent files
for (const depFile of cacheVal.depFiles) {
const depCache = cache.get(depFile);
if (depCache === undefined) {
return newCacheVal;
}

const depFileStat = await stat(depFile);
if (depCache.lastMtimeMs !== depFileStat.mtimeMs) {
return newCacheVal;
}
}

return cacheVal;
}

async function updateDepFilesCache(
depFiles: string[],
cache: Map<string, CacheVal>
) {
for (const depFile of depFiles) {
const fileStat = await stat(depFile);
cache.set(depFile, {
lastMtimeMs: fileStat.mtimeMs,
depFiles: [],
outputCss: "",
outputPath: ""
});
}
}

export default postCSSPlugin;