diff --git a/docs/BUNDLERS_INTEGRATION.md b/docs/BUNDLERS_INTEGRATION.md index b1a1f1551..af103b758 100644 --- a/docs/BUNDLERS_INTEGRATION.md +++ b/docs/BUNDLERS_INTEGRATION.md @@ -178,16 +178,10 @@ The loader accepts the following options: Setting this option to `true` will include source maps for the generated CSS so that you can see where source of the class name in devtools. We recommend to enable this only in development mode because the sourcemap is inlined into the CSS files. -- `cacheProvider: undefined | string | ICache` (default: `undefined`): - By default Linaria use a memory cache to store temporary CSS files. But if you are using this loader with [thread-loader](https://www.npmjs.com/package/thread-loader) you should use some consistent cache to prevent [some unexpected issues](https://github.com/callstack/linaria/issues/881). This options support a `ICache` instance or a path to NodeJS module which export a `ICache` instance as `module.exports` - - > ``` - > interface ICache { - > get: (key: string) => Promise; - > set: (key: string, value: string) => Promise - > } - > ``` +- `cacheDirectory: string` (default: `'.linaria-cache'`): + Path to the directory where the loader will output the intermediate CSS files. You can pass a relative or absolute directory path. Make sure the directory is inside the working directory for things to work properly. **You should add this directory to `.gitignore` so you don't accidentally commit them.** + - `extension: string` (default: `'.linaria.css'`): An extension of the intermediate CSS files. @@ -225,6 +219,7 @@ You can pass options to the loader like so: loader: '@linaria/webpack-loader', options: { sourceMap: false, + cacheDirectory: '.linaria-cache', }, } ``` diff --git a/packages/webpack4-loader/package.json b/packages/webpack4-loader/package.json index 03928c43e..6e04d5c3a 100644 --- a/packages/webpack4-loader/package.json +++ b/packages/webpack4-loader/package.json @@ -38,17 +38,22 @@ "watch": "yarn build --watch" }, "devDependencies": { + "@types/cosmiconfig": "^5.0.3", "@types/enhanced-resolve": "^3.0.6", "@types/loader-utils": "^1.1.3", "@types/mkdirp": "^0.5.2", + "@types/normalize-path": "^3.0.0", "source-map": "^0.7.3" }, "dependencies": { "@linaria/babel-preset": "^3.0.0-beta.17", "@linaria/logger": "^3.0.0-beta.15", + "cosmiconfig": "^5.1.0", "enhanced-resolve": "^4.1.0", + "find-yarn-workspace-root": "^1.2.1", "loader-utils": "^1.2.3", - "mkdirp": "^0.5.1" + "mkdirp": "^0.5.1", + "normalize-path": "^3.0.0" }, "peerDependencies": { "@babel/core": ">=7" diff --git a/packages/webpack4-loader/src/index.ts b/packages/webpack4-loader/src/index.ts index c003f6995..9cc08c8b5 100644 --- a/packages/webpack4-loader/src/index.ts +++ b/packages/webpack4-loader/src/index.ts @@ -4,13 +4,24 @@ * returns transformed code without template literals and attaches generated source maps */ +import fs from 'fs'; import path from 'path'; +import mkdirp from 'mkdirp'; +import normalize from 'normalize-path'; import loaderUtils from 'loader-utils'; import enhancedResolve from 'enhanced-resolve'; +import findYarnWorkspaceRoot from 'find-yarn-workspace-root'; import type { RawSourceMap } from 'source-map'; +import cosmiconfig from 'cosmiconfig'; import { EvalCache, Module, Result, transform } from '@linaria/babel-preset'; import { debug, notify } from '@linaria/logger'; -import { getCacheInstance } from './cache'; + +const workspaceRoot = findYarnWorkspaceRoot(); +const lernaConfig = cosmiconfig('lerna', { + searchPlaces: ['lerna.json'], +}).searchSync(); +const lernaRoot = + lernaConfig !== null ? path.dirname(lernaConfig.filepath) : null; type LoaderContext = Parameters[0]; @@ -24,8 +35,6 @@ const castSourceMap = ( } : undefined; -const outputCssLoader = require.resolve('./outputCssLoader'); - export default function webpack4Loader( this: LoaderContext, content: string, @@ -45,6 +54,7 @@ export default function webpack4Loader( const { sourceMap = undefined, + cacheDirectory = '.linaria-cache', preprocessor = undefined, extension = '.linaria.css', cacheProvider, @@ -52,7 +62,20 @@ export default function webpack4Loader( ...rest } = loaderUtils.getOptions(this) || {}; - const outputFileName = this.resourcePath.replace(/\.[^.]+$/, extension); + const root = workspaceRoot || lernaRoot || process.cwd(); + + const baseOutputFileName = this.resourcePath.replace(/\.[^.]+$/, extension); + + const outputFilename = normalize( + path.join( + path.isAbsolute(cacheDirectory) + ? cacheDirectory + : path.join(process.cwd(), cacheDirectory), + this.resourcePath.includes(root) + ? path.relative(root, baseOutputFileName) + : baseOutputFileName + ) + ); // this._compilation is a deprecated API // However there seems to be no other way to access webpack's resolver @@ -111,6 +134,7 @@ export default function webpack4Loader( result = transform(content, { filename: path.relative(process.cwd(), this.resourcePath), inputSourceMap: inputSourceMap ?? undefined, + outputFilename, pluginOptions: rest, preprocessor, }); @@ -141,21 +165,30 @@ export default function webpack4Loader( }); } - getCacheInstance(cacheProvider) - .then((cacheInstance) => cacheInstance.set(this.resourcePath, cssText)) - .then(() => { - const request = `${outputFileName}!=!${outputCssLoader}?cacheProvider=${encodeURIComponent( - cacheProvider ?? '' - )}!${this.resourcePath}`; - const stringifiedRequest = loaderUtils.stringifyRequest(this, request); - - return this.callback( - null, - `${result.code}\n\nrequire(${stringifiedRequest});`, - castSourceMap(result.sourceMap) - ); - }) - .catch((err: Error) => this.callback(err)); + // Read the file first to compare the content + // Write the new content only if it's changed + // This will prevent unnecessary WDS reloads + let currentCssText; + + try { + currentCssText = fs.readFileSync(outputFilename, 'utf-8'); + } catch (e) { + // Ignore error + } + + if (currentCssText !== cssText) { + mkdirp.sync(path.dirname(outputFilename)); + fs.writeFileSync(outputFilename, cssText); + } + + this.callback( + null, + `${result.code}\n\nrequire(${loaderUtils.stringifyRequest( + this, + outputFilename + )});`, + castSourceMap(result.sourceMap) + ); return; } diff --git a/packages/webpack4-loader/src/outputCssLoader.ts b/packages/webpack4-loader/src/outputCssLoader.ts deleted file mode 100644 index ab7c180df..000000000 --- a/packages/webpack4-loader/src/outputCssLoader.ts +++ /dev/null @@ -1,13 +0,0 @@ -import loaderUtils from 'loader-utils'; -import { getCacheInstance } from './cache'; - -type LoaderContext = Parameters[0]; - -export default function outputCssLoader(this: LoaderContext) { - this.async(); - const { cacheProvider } = loaderUtils.getOptions(this) || {}; - getCacheInstance(cacheProvider) - .then((cacheInstance) => cacheInstance.get(this.resourcePath)) - .then((result) => this.callback(null, result)) - .catch((err: Error) => this.callback(err)); -} diff --git a/packages/webpack5-loader/package.json b/packages/webpack5-loader/package.json index 1d339049c..18f075954 100644 --- a/packages/webpack5-loader/package.json +++ b/packages/webpack5-loader/package.json @@ -38,18 +38,23 @@ "watch": "yarn build --watch" }, "devDependencies": { + "@types/cosmiconfig": "^5.0.3", "@types/enhanced-resolve": "^3.0.6", "@types/loader-utils": "^1.1.3", "@types/mkdirp": "^0.5.2", + "@types/normalize-path": "^3.0.0", "source-map": "^0.7.3", "webpack": "^5.6.0" }, "dependencies": { "@linaria/babel-preset": "^3.0.0-beta.17", "@linaria/logger": "^3.0.0-beta.15", + "cosmiconfig": "^5.1.0", "enhanced-resolve": "^5.3.1", + "find-yarn-workspace-root": "^1.2.1", "loader-utils": "^2.0.0", - "mkdirp": "^0.5.1" + "mkdirp": "^0.5.1", + "normalize-path": "^3.0.0" }, "peerDependencies": { "@babel/core": ">=7", diff --git a/packages/webpack5-loader/src/index.ts b/packages/webpack5-loader/src/index.ts index b50ef7902..324f8635c 100644 --- a/packages/webpack5-loader/src/index.ts +++ b/packages/webpack5-loader/src/index.ts @@ -4,15 +4,24 @@ * returns transformed code without template literals and attaches generated source maps */ +import fs from 'fs'; import path from 'path'; import loaderUtils from 'loader-utils'; +import mkdirp from 'mkdirp'; +import normalize from 'normalize-path'; import enhancedResolve from 'enhanced-resolve'; +import findYarnWorkspaceRoot from 'find-yarn-workspace-root'; import type { RawSourceMap } from 'source-map'; +import cosmiconfig from 'cosmiconfig'; import { EvalCache, Module, Result, transform } from '@linaria/babel-preset'; import { debug, notify } from '@linaria/logger'; -import { getCacheInstance } from './cache'; -const outputCssLoader = require.resolve('./outputCssLoader'); +const workspaceRoot = findYarnWorkspaceRoot(); +const lernaConfig = cosmiconfig('lerna', { + searchPlaces: ['lerna.json'], +}).searchSync(); +const lernaRoot = + lernaConfig !== null ? path.dirname(lernaConfig.filepath) : null; export default function webpack5Loader( this: any, @@ -33,6 +42,7 @@ export default function webpack5Loader( const { sourceMap = undefined, + cacheDirectory = '.linaria-cache', preprocessor = undefined, extension = '.linaria.css', cacheProvider, @@ -40,7 +50,20 @@ export default function webpack5Loader( ...rest } = this.getOptions() || {}; - const outputFileName = this.resourcePath.replace(/\.[^.]+$/, extension); + const root = workspaceRoot || lernaRoot || process.cwd(); + + const baseOutputFileName = this.resourcePath.replace(/\.[^.]+$/, extension); + + const outputFilename = normalize( + path.join( + path.isAbsolute(cacheDirectory) + ? cacheDirectory + : path.join(process.cwd(), cacheDirectory), + this.resourcePath.includes(root) + ? path.relative(root, baseOutputFileName) + : baseOutputFileName + ) + ); // this._compilation is a deprecated API // However there seems to be no other way to access webpack's resolver @@ -104,6 +127,7 @@ export default function webpack5Loader( result = transform(content, { filename: path.relative(process.cwd(), this.resourcePath), inputSourceMap: inputSourceMap ?? undefined, + outputFilename, pluginOptions: rest, preprocessor, }); @@ -134,21 +158,30 @@ export default function webpack5Loader( }); } - getCacheInstance(cacheProvider) - .then((cacheInstance) => cacheInstance.set(this.resourcePath, cssText)) - .then(() => { - const request = `${outputFileName}!=!${outputCssLoader}?cacheProvider=${encodeURIComponent( - cacheProvider ?? '' - )}!${this.resourcePath}`; - const stringifiedRequest = loaderUtils.stringifyRequest(this, request); - - return this.callback( - null, - `${result.code}\n\nrequire(${stringifiedRequest});`, - result.sourceMap ?? undefined - ); - }) - .catch((err: Error) => this.callback(err)); + // Read the file first to compare the content + // Write the new content only if it's changed + // This will prevent unnecessary WDS reloads + let currentCssText; + + try { + currentCssText = fs.readFileSync(outputFilename, 'utf-8'); + } catch (e) { + // Ignore error + } + + if (currentCssText !== cssText) { + mkdirp.sync(path.dirname(outputFilename)); + fs.writeFileSync(outputFilename, cssText); + } + + this.callback( + null, + `${result.code}\n\nrequire(${loaderUtils.stringifyRequest( + this, + outputFilename + )});`, + result.sourceMap ?? undefined + ); return; } diff --git a/packages/webpack5-loader/src/outputCssLoader.ts b/packages/webpack5-loader/src/outputCssLoader.ts deleted file mode 100644 index fe37aae1e..000000000 --- a/packages/webpack5-loader/src/outputCssLoader.ts +++ /dev/null @@ -1,13 +0,0 @@ -import webpack from 'webpack'; -import { getCacheInstance, ICache } from './cache'; - -export default function outputCssLoader( - this: webpack.LoaderContext<{ cacheProvider: string | ICache | undefined }> -) { - this.async(); - const { cacheProvider } = this.getOptions(); - getCacheInstance(cacheProvider) - .then((cacheInstance) => cacheInstance.get(this.resourcePath)) - .then((result) => this.callback(null, result)) - .catch((err: Error) => this.callback(err)); -} diff --git a/yarn.lock b/yarn.lock index 70444b256..75552e761 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6760,6 +6760,14 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-yarn-workspace-root@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-1.2.1.tgz#40eb8e6e7c2502ddfaa2577c176f221422f860db" + integrity sha512-dVtfb0WuQG+8Ag2uWkbG79hOUzEsRrhBzgfn86g2sJPkzmcpGdghbNTfUKGTxymFrY/tLIodDzLoW9nOJ4FY8Q== + dependencies: + fs-extra "^4.0.3" + micromatch "^3.1.4" + findup-sync@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" @@ -6870,7 +6878,7 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@^4.0.2: +fs-extra@^4.0.2, fs-extra@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==