diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 2b28dc722..85f4f6532 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -34,6 +34,10 @@ module.exports = { If you need to specify custom babel configuration, you can pass them here. These babel options will be used by Linaria when parsing and evaluating modules. +- `scope: boolean or string` (default: `false`): + + When set to `true` this option will create unique CSS class names based on your package.json name and version. This may be useful for libraries that want unique class names in different versions. You can alternatively provide a unique string to use (instead of the package.json name and version). + ## `linaria/babel` preset The preset pre-processes and evaluates the CSS. The bundler plugins use this preset under the hood. You also might want to use this preset if you import the components outside of the files handled by your bundler, such as on your server or in unit tests. diff --git a/package.json b/package.json index 483553f8c..a3934417f 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "normalize-path": "^3.0.0", "postcss": "^7.0.14", "react-is": "^16.8.3", + "read-pkg": "^5.0.0", "rollup-pluginutils": "^2.4.1", "source-map": "^0.7.3", "strip-ansi": "^5.0.0", diff --git a/src/__tests__/transform.test.js b/src/__tests__/transform.test.js index 32d8cf533..b41f4996d 100644 --- a/src/__tests__/transform.test.js +++ b/src/__tests__/transform.test.js @@ -147,3 +147,45 @@ it("doesn't throw due to duplicate preset", async () => { ) ).not.toThrowError('Duplicate plugin/preset detected'); }); + +it('respects scope option', async () => { + const testFile = dedent` + import { css } from 'linaria'; + + export const test = css\` + color: red; + \`; + `; + + let falseResult = transform(testFile, { + filename: './test.js', + outputFilename: '../.linaria-cache/test.css', + pluginOptions: { + scopeString: false, + }, + }); + + expect(falseResult).toMatchSnapshot(); + + let trueResult = transform(testFile, { + filename: './test.js', + outputFilename: '../.linaria-cache/test.css', + pluginOptions: { + scopeString: true, + }, + }); + + expect(trueResult).toMatchSnapshot(); + expect(trueResult.cssText).not.toBe(falseResult.cssText); + + let customScopeResult = transform(testFile, { + filename: './test.js', + outputFilename: '../.linaria-cache/test.css', + pluginOptions: { + scopeString: 'custom-scope', + }, + }); + + expect(customScopeResult).toMatchSnapshot(); + expect(customScopeResult.cssText).not.toBe(trueResult.cssText); +}); diff --git a/src/babel/types.js b/src/babel/types.js index 1c312e9ab..d79b6bff5 100644 --- a/src/babel/types.js +++ b/src/babel/types.js @@ -30,6 +30,7 @@ export type StrictOptions = {| evaluate: boolean, ignore: RegExp, babelOptions: Object, + scope: string | false, |}; export type Location = { diff --git a/src/babel/utils/loadOptions.js b/src/babel/utils/loadOptions.js index abff58c22..92b56b425 100644 --- a/src/babel/utils/loadOptions.js +++ b/src/babel/utils/loadOptions.js @@ -1,6 +1,7 @@ /* @flow */ import cosmiconfig from 'cosmiconfig'; +import readPkg from 'read-pkg'; import type { StrictOptions } from '../types'; export type PluginOptions = $Shape<{ @@ -14,16 +15,34 @@ export default function loadOptions( overrides?: PluginOptions = {} ): StrictOptions { const { configFile, ...rest } = overrides; + let scopeOption; + let scopeString = ''; const result = configFile !== undefined ? explorer.loadSync(configFile) : explorer.searchSync(); + if (result && result.config) { + scopeOption = result.config.scope; + } + + if (scopeOption === true) { + try { + const pkgJson = readPkg.sync(); + scopeString = `${pkgJson.name}@${pkgJson.version}`; + } catch (err) { + console.log(`Couldn't retrieve package.json name/version.`); + } + } else if (typeof scopeOption === 'string') { + scopeString = scopeOption; + } + const options = { displayName: false, evaluate: true, ignore: /node_modules/, + scopeString, ...(result ? result.config : null), ...rest, }; diff --git a/src/babel/visitors/TaggedTemplateExpression.js b/src/babel/visitors/TaggedTemplateExpression.js index 4d9fbec7c..ac86c8db6 100644 --- a/src/babel/visitors/TaggedTemplateExpression.js +++ b/src/babel/visitors/TaggedTemplateExpression.js @@ -131,9 +131,10 @@ export default function TaggedTemplateExpression( // Also use append the index of the class to the filename for uniqueness in the file const slug = toValidCSSIdentifier( `${displayName.charAt(0).toLowerCase()}${slugify( - `${relative(state.file.opts.root, state.file.opts.filename)}:${ - state.index - }` + `${options.scopeString ? `${options.scopeString}:` : ''}${relative( + state.file.opts.root, + state.file.opts.filename + )}:${state.index}` )}` ); diff --git a/yarn.lock b/yarn.lock index 7387de9de..071efd535 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6261,6 +6261,14 @@ read-pkg@^4.0.1: parse-json "^4.0.0" pify "^3.0.0" +read-pkg@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.0.0.tgz#75449907ece8dfb89cbc76adcba2665316e32b94" + integrity sha512-OWufaRc67oJjcgrxckW/qO9q22iYzyiONh8h+GMcnOvSHAmhV1Dr3x+gyRjP+Qxc5jKupkSfoCQLS/98rDPh9A== + dependencies: + normalize-package-data "^2.3.2" + parse-json "^4.0.0" + readable-stream@^2.0.1, readable-stream@^2.1.5: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"