From 2abb463e9a93d3d95a2c8552b000e6d74d67d932 Mon Sep 17 00:00:00 2001 From: baurine <2008.hbl@gmail.com> Date: Mon, 6 Dec 2021 13:27:31 +0800 Subject: [PATCH 1/5] support cache --- src/index.ts | 213 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 145 insertions(+), 68 deletions(-) diff --git a/src/index.ts b/src/index.ts index 4b0b401..490b2fc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import { readFile, readdirSync, statSync, + stat, writeFile } from "fs-extra"; import { TextDecoder } from "util"; @@ -31,6 +32,7 @@ interface PostCSSPluginOptions { lessOptions?: Less.Options; stylusOptions?: StylusRenderOptions; writeToFile?: boolean; + enableCache?: boolean; } interface CSSModule { @@ -40,6 +42,13 @@ interface CSSModule { }; } +interface CacheVal { + lastMtimeMs: number; + depFiles: string[]; + outputPath: string; + outputCss: string; +} + const postCSSPlugin = ({ plugins = [], modules = true, @@ -47,13 +56,15 @@ const postCSSPlugin = ({ sassOptions = {}, lessOptions = {}, stylusOptions = {}, - writeToFile = true + writeToFile = true, + enableCache = false }: PostCSSPluginOptions): 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 = new Map(); const modulesPlugin = postcssModules({ generateScopedName: "[name]__[local]___[hash:base64:5]", @@ -92,70 +103,81 @@ 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 //.css - const sourceRelDir = path.relative( - path.dirname(rootDir), - path.dirname(sourceFullPath) - ); - tmpFilePath = path.resolve( - tmpDirPath, - sourceRelDir, - `${sourceBaseName}.css` - ); + + const cacheVal = await queryCache(sourceFullPath, cache); + let tmpFilePath: string = cacheVal.outputPath; + + if (cacheVal.outputCss === "") { + if (args.kind === "entry-point") { + // For entry points, we use //.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 //.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 //.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") + css = ( + await renderSass({ ...sassOptions, file: sourceFullPath }) + ).css.toString(); + 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 }); - 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); + + // Write result CSS + if (writeToFile) { + await writeFile(tmpFilePath, result.css); + } + + cacheVal.outputPath = tmpFilePath; + cacheVal.outputCss = result.css; + cacheVal.depFiles = getPostCssDependencies(result.messages); + if (enableCache) { + cache.set(sourceFullPath, cacheVal); + await updateDepFilesCache(cacheVal.depFiles, cache); + } } return { @@ -165,12 +187,10 @@ const postCSSPlugin = ({ ? "file" : "postcss-text", path: tmpFilePath, - watchFiles: [result.opts.from].concat( - getPostCssDependencies(result.messages) - ), + watchFiles: [sourceFullPath].concat(cacheVal.depFiles), pluginData: { originalPath: sourceFullPath, - css: result.css + css: cacheVal.outputCss } }; } @@ -277,4 +297,61 @@ function getPostCssDependencies(messages: Message[]): string[] { return dependencies; } +async function queryCache( + sourceFullPath: string, + cache: Map +): Promise { + 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 +) { + for (const depFile of depFiles) { + const fileStat = await stat(depFile); + cache.set(depFile, { + lastMtimeMs: fileStat.mtimeMs, + depFiles: [], + outputCss: "", + outputPath: "" + }); + } +} + export default postCSSPlugin; From 60ee71489b1a0457ac428baba540dea8d9b2551b Mon Sep 17 00:00:00 2001 From: baurine <2008.hbl@gmail.com> Date: Mon, 6 Dec 2021 17:10:57 +0800 Subject: [PATCH 2/5] handle dep files --- src/index.ts | 69 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/src/index.ts b/src/index.ts index 490b2fc..376742e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,8 @@ import { readdirSync, statSync, stat, - writeFile + writeFile, + existsSync } from "fs-extra"; import { TextDecoder } from "util"; import { @@ -100,14 +101,27 @@ const postCSSPlugin = ({ if (!sourceFullPath) sourceFullPath = path.resolve(args.resolveDir, args.path); + // hack + let exist = existsSync(sourceFullPath + ".js"); + if (exist) { + return; + } + exist = existsSync(sourceFullPath); + if (!exist) { + sourceFullPath = path.resolve( + process.cwd(), + "node_modules", + args.path + ); + } + const sourceExt = path.extname(sourceFullPath); const sourceBaseName = path.basename(sourceFullPath, sourceExt); const isModule = sourceBaseName.match(/\.module$/); const cacheVal = await queryCache(sourceFullPath, cache); - let tmpFilePath: string = cacheVal.outputPath; - - if (cacheVal.outputCss === "") { + if (cacheVal.outputPath === "") { + let tmpFilePath: string = ""; if (args.kind === "entry-point") { // For entry points, we use //.css const sourceRelDir = path.relative( @@ -140,23 +154,33 @@ const postCSSPlugin = ({ 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") + 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 }); - if (sourceExt === ".less") - css = ( - await less.render(new TextDecoder().decode(fileContent), { + // 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; + } + ); + css = ret.css; + cacheVal.depFiles = ret.imports; + } // wait for plugins to complete parsing & get result const result = await postcss( @@ -165,19 +189,22 @@ const postCSSPlugin = ({ 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; - cacheVal.depFiles = getPostCssDependencies(result.messages); + // Save cache if (enableCache) { cache.set(sourceFullPath, cacheVal); await updateDepFilesCache(cacheVal.depFiles, cache); } + + // Write result CSS + if (writeToFile) { + await writeFile(tmpFilePath, result.css); + } } return { @@ -186,7 +213,7 @@ const postCSSPlugin = ({ : writeToFile ? "file" : "postcss-text", - path: tmpFilePath, + path: cacheVal.outputPath, watchFiles: [sourceFullPath].concat(cacheVal.depFiles), pluginData: { originalPath: sourceFullPath, From 46db412695a68600a71055375524dd3026c72012 Mon Sep 17 00:00:00 2001 From: baurine <2008.hbl@gmail.com> Date: Mon, 6 Dec 2021 17:11:26 +0800 Subject: [PATCH 3/5] remove hack code --- src/index.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/index.ts b/src/index.ts index 376742e..c5aafc8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -101,20 +101,6 @@ const postCSSPlugin = ({ if (!sourceFullPath) sourceFullPath = path.resolve(args.resolveDir, args.path); - // hack - let exist = existsSync(sourceFullPath + ".js"); - if (exist) { - return; - } - exist = existsSync(sourceFullPath); - if (!exist) { - sourceFullPath = path.resolve( - process.cwd(), - "node_modules", - args.path - ); - } - const sourceExt = path.extname(sourceFullPath); const sourceBaseName = path.basename(sourceFullPath, sourceExt); const isModule = sourceBaseName.match(/\.module$/); From 09466773f687919207ab9fc3b72f31f79ac8b8f5 Mon Sep 17 00:00:00 2001 From: baurine <2008.hbl@gmail.com> Date: Mon, 6 Dec 2021 17:36:47 +0800 Subject: [PATCH 4/5] refine --- src/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index c5aafc8..38d836b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,8 +6,7 @@ import { readdirSync, statSync, stat, - writeFile, - existsSync + writeFile } from "fs-extra"; import { TextDecoder } from "util"; import { From 237918c1e09cdebabd165e463c79e38f1cb7bcba Mon Sep 17 00:00:00 2001 From: baurine <2008.hbl@gmail.com> Date: Mon, 6 Dec 2021 17:54:42 +0800 Subject: [PATCH 5/5] update README --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 6f0ebcd..3fe58f4 100644 --- a/README.md +++ b/README.md @@ -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 + }) + ] + ... +}); +```