diff --git a/CHANGELOG.md b/CHANGELOG.md index 497e9d31..ecde6c90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +# [3.9.0](https://github.com/kaisermann/svelte-preprocess/compare/v3.7.4...v3.9.0) (2020-06-05) + + +### Bug Fixes + +* 🐛 run globalRule only if postcss is installed ([6294750](https://github.com/kaisermann/svelte-preprocess/commit/62947507064271d1cec796d3e0a7801633b875a8)) + + +### Features + +* add implementation option for scss ([e4ca556](https://github.com/kaisermann/svelte-preprocess/commit/e4ca556821785e2b853f1668489912ebab21ee4b)) +* add the [@global](https://github.com/global) {} rule support ([46722ba](https://github.com/kaisermann/svelte-preprocess/commit/46722bac993308d8e4f1bb3d0b3086b802013d3d)) +* replace the [@global](https://github.com/global) for :global for CSS modules compliance ([3c6a574](https://github.com/kaisermann/svelte-preprocess/commit/3c6a574ac25ea84aea2d1d60e025680d404c30ff)) + + + # [3.8.0](https://github.com/kaisermann/svelte-preprocess/compare/v3.7.4...v3.8.0) (2020-06-05) diff --git a/README.md b/README.md index a465b36a..a4148a8e 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ _Note: only for auto preprocessing_ ### Global style -Add a `global` attribute to your `style` tag and instead of scoping the css, all of its content will be interpreted as global style. +Add a `global` attribute to your `style` tag and instead of scoping the CSS, all of its content will be interpreted as global style. ```html ``` -_Note1: needs postcss to be installed_ +_Note1: needs PostCSS to be installed._ _Note2: if you're using it as a standalone processor, it works best if added to the end of the processors array._ +_Note3: if you need to have some styles be scoped inside a global style tag, use `:local` in the same way you'd use `:global`._ + +### Global rule + +Use a `:global` rule to only expose parts of the stylesheet: + +```html + +``` + +Works best with nesting-enabled CSS preprocessors, but regular CSS selectors like `div :global .global1 .global2` are also supported. + +_Note1: needs PostCSS to be installed._ + +_Note2: if you're using it as a standalone processor, it works best if added to the end of the processors array._ + +_Note3: wrapping `@keyframes` inside `:global {}` blocks is not supported. Use the [`-global-` name prefix for global keyframes](https://svelte.dev/docs#style)._ + ### Preprocessors Current supported out-of-the-box preprocessors are `SCSS`, `Stylus`, `Less`, `Coffeescript`, `TypeScript`, `Pug`, `PostCSS`, `Babel`. @@ -135,7 +163,9 @@ Current supported out-of-the-box preprocessors are `SCSS`, `Stylus`, `Less`, `Co ``` diff --git a/package.json b/package.json index 1f3569ee..67278771 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte-preprocess", - "version": "3.8.0", + "version": "3.9.0", "license": "MIT", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/autoProcess.ts b/src/autoProcess.ts index 62fcbd6b..94d66956 100644 --- a/src/autoProcess.ts +++ b/src/autoProcess.ts @@ -16,6 +16,7 @@ import { Options, Processed, } from './types'; +import { hasPostCssInstalled } from './modules/hasPostcssInstalled'; interface Transformers { typescript?: TransformerOptions; @@ -26,9 +27,10 @@ interface Transformers { postcss?: TransformerOptions; coffeescript?: TransformerOptions; pug?: TransformerOptions; - globalStyle?: TransformerOptions; + globalStyle?: TransformerOptions; + globalRule?: TransformerOptions; replace?: Options.Replace; - [languageName: string]: TransformerOptions; + [languageName: string]: TransformerOptions; } type AutoPreprocessOptions = { @@ -55,6 +57,7 @@ type AutoPreprocessOptions = { coffeescript?: TransformerOptions; pug?: TransformerOptions; globalStyle?: TransformerOptions; + globalRule?: TransformerOptions; // workaround while we don't have this // https://github.com/microsoft/TypeScript/issues/17867 [languageName: string]: @@ -62,7 +65,7 @@ type AutoPreprocessOptions = { | Promise | [string, string][] | string[] - | TransformerOptions; + | TransformerOptions; }; const SVELTE_MAJOR_VERSION = +version[0]; @@ -259,13 +262,23 @@ export function autoPreprocess( dependencies = concat(dependencies, transformed.dependencies); } - if (attributes.global) { - const transformed = await runTransformer('globalStyle', null, { + if (await hasPostCssInstalled()) { + if (attributes.global) { + const transformed = await runTransformer('globalStyle', null, { + content: code, + map, + filename, + }); + + code = transformed.code; + map = transformed.map; + } + + const transformed = await runTransformer('globalRule', null, { content: code, map, filename, }); - code = transformed.code; map = transformed.map; } diff --git a/src/modules/globalifySelector.ts b/src/modules/globalifySelector.ts new file mode 100644 index 00000000..c3a88c37 --- /dev/null +++ b/src/modules/globalifySelector.ts @@ -0,0 +1,16 @@ +export function globalifySelector(selector: string) { + return selector + .trim() + .split(' ') + .filter(Boolean) + .map((selectorPart) => { + if (selectorPart.startsWith(':local')) { + return selectorPart.replace(/:local\((.+?)\)/g, '$1'); + } + if (selectorPart.startsWith(':global')) { + return selectorPart; + } + return `:global(${selectorPart})`; + }) + .join(' '); +} diff --git a/src/modules/hasPostcssInstalled.ts b/src/modules/hasPostcssInstalled.ts new file mode 100644 index 00000000..a8fdc61a --- /dev/null +++ b/src/modules/hasPostcssInstalled.ts @@ -0,0 +1,17 @@ +let cachedResult: boolean; + +export async function hasPostCssInstalled() { + if (cachedResult != null) { + return cachedResult; + } + + let result = false; + try { + await import('postcss'); + result = true; + } catch (e) { + result = false; + } + + return (cachedResult = result); +} diff --git a/src/modules/importAny.ts b/src/modules/importAny.ts new file mode 100644 index 00000000..fee66ebf --- /dev/null +++ b/src/modules/importAny.ts @@ -0,0 +1,11 @@ +export async function importAny(...modules: string[]) { + try { + const mod = await modules.reduce( + (acc, moduleName) => acc.catch(() => import(moduleName)), + Promise.reject(), + ); + return mod; + } catch (e) { + throw new Error(`Cannot find any of modules: ${modules}`); + } +} diff --git a/src/processors/globalRule.ts b/src/processors/globalRule.ts new file mode 100644 index 00000000..1a7a5fe7 --- /dev/null +++ b/src/processors/globalRule.ts @@ -0,0 +1,13 @@ +import { PreprocessorGroup } from '../types'; + +export default (): PreprocessorGroup => { + return { + async style({ content, filename }) { + const { default: transformer } = await import( + '../transformers/globalRule' + ); + + return transformer({ content, filename }); + }, + }; +}; diff --git a/src/transformers/globalRule.ts b/src/transformers/globalRule.ts new file mode 100644 index 00000000..4145e6fd --- /dev/null +++ b/src/transformers/globalRule.ts @@ -0,0 +1,26 @@ +import postcss from 'postcss'; + +import { Transformer } from '../types'; +import { globalifySelector } from '../modules/globalifySelector'; + +const selectorPattern = /:global(?!\()/; + +const globalifyRulePlugin = (root: any) => { + root.walkRules(selectorPattern, (rule: any) => { + const [beginning, ...rest] = rule.selector.split(selectorPattern); + rule.selector = [beginning, ...rest.map(globalifySelector)] + .map((str) => str.trim()) + .join(' ') + .trim(); + }); +}; + +const transformer: Transformer = async ({ content, filename }) => { + const { css, map: newMap } = await postcss() + .use(globalifyRulePlugin) + .process(content, { from: filename, map: true }); + + return { code: css, map: newMap }; +}; + +export default transformer; diff --git a/src/transformers/globalStyle.ts b/src/transformers/globalStyle.ts index ee01ecf6..8468389c 100644 --- a/src/transformers/globalStyle.ts +++ b/src/transformers/globalStyle.ts @@ -1,11 +1,12 @@ import postcss from 'postcss'; import { Transformer } from '../types'; +import { globalifySelector } from '../modules/globalifySelector'; const globalifyPlugin = (root: any) => { root.walkAtRules(/keyframes$/, (atrule: any) => { if (!atrule.params.startsWith('-global-')) { - atrule.params = '-global-' + atrule.params; + atrule.params = `-global-${atrule.params}`; } }); @@ -14,20 +15,7 @@ const globalifyPlugin = (root: any) => { return; } - rule.selectors = rule.selectors.map((selector: string) => { - return selector - .split(' ') - .map((selectorPart) => { - if (selectorPart.startsWith(':local')) { - return selectorPart.replace(/:local\((.+?)\)/g, '$1'); - } - if (selectorPart.startsWith(':global')) { - return selectorPart; - } - return `:global(${selectorPart})`; - }) - .join(' '); - }); + rule.selectors = rule.selectors.map(globalifySelector); }); }; diff --git a/src/transformers/scss.ts b/src/transformers/scss.ts index 78456ce0..d72dff13 100644 --- a/src/transformers/scss.ts +++ b/src/transformers/scss.ts @@ -1,7 +1,8 @@ import { Result } from 'sass'; -import { importAny, getIncludePaths } from '../utils'; +import { getIncludePaths } from '../utils'; import { Transformer, Processed, Options } from '../types'; +import { importAny } from '../modules/importAny'; let sass: Options.Sass['implementation']; diff --git a/src/types/index.ts b/src/types/index.ts index 9fad0b75..8895280c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -33,7 +33,7 @@ export type Transformer = ( args: TransformerArgs, ) => Processed | Promise; -export type TransformerOptions = +export type TransformerOptions = | boolean | Record | Transformer; diff --git a/src/utils.ts b/src/utils.ts index 0b7c6ab5..c54a3ce1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -158,16 +158,4 @@ export const runTransformer = async ( `Error transforming '${name}'.\n\nMessage:\n${e.message}\n\nStack:\n${e.stack}`, ); } -}; - -export const importAny = async (...modules: string[]) => { - try { - const mod = await modules.reduce( - (acc, moduleName) => acc.catch(() => import(moduleName)), - Promise.reject(), - ); - return mod; - } catch (e) { - throw new Error(`Cannot find any of modules: ${modules}`); - } -}; +}; \ No newline at end of file diff --git a/test/transformers/globalRule.test.ts b/test/transformers/globalRule.test.ts new file mode 100644 index 00000000..e01b86ec --- /dev/null +++ b/test/transformers/globalRule.test.ts @@ -0,0 +1,95 @@ +import autoProcess from '../../src'; +import { preprocess } from '../utils'; + +describe('transformer - globalRule', () => { + it('does nothing if postcss is not installed', async () => { + const template = ``; + const opts = autoProcess(); + + expect(async () => await preprocess(template, opts)).not.toThrow(); + }); + + it('wraps selector in :global(...) modifier', async () => { + const template = ``; + const opts = autoProcess(); + const preprocessed = await preprocess(template, opts); + + expect(preprocessed.toString()).toContain( + `:global(div){color:red}:global(.test){}`, + ); + }); + + it('wraps selector in :global(...) only if needed', async () => { + const template = ``; + const opts = autoProcess(); + const preprocessed = await preprocess(template, opts); + + expect(preprocessed.toString()).toContain( + `:global(.test){}:global(.foo){}`, + ); + }); + + it('wraps selector in :global(...) on multiple levels', async () => { + const template = ''; + const opts = autoProcess(); + const preprocessed = await preprocess(template, opts); + + expect(preprocessed.toString()).toMatch( + // either be :global(div .cls){} + // or :global(div) :global(.cls){} + /(:global\(div .cls\)\{\}|:global\(div\) :global\(\.cls\)\{\})/, + ); + }); + + it('wraps selector in :global(...) on multiple levels when in the middle', async () => { + const template = ''; + const opts = autoProcess(); + const preprocessed = await preprocess(template, opts); + + expect(preprocessed.toString()).toMatch( + // either be div div :global(span .cls) {} + // or div div :global(span) :global(.cls) {} + /div div (:global\(span .cls\)\{\}|:global\(span\) :global\(\.cls\)\{\})/, + ); + }); + + it('does not break when at the end', async () => { + const template = ''; + const opts = autoProcess(); + const preprocessed = await preprocess(template, opts); + + expect(preprocessed.toString()).toContain('span{}'); + }); + + it('works with collapsed nesting several times', async () => { + const template = ''; + const opts = autoProcess(); + const preprocessed = await preprocess(template, opts); + + expect(preprocessed.toString()).toMatch( + // either be div :global(span .cls) {} + // or div :global(span) :global(.cls) {} + /div (:global\(span .cls\)\{\}|:global\(span\) :global\(\.cls\)\{\})/, + ); + }); + + it('does not interfere with the :global(...) syntax', async () => { + const template = ''; + const opts = autoProcess(); + const preprocessed = await preprocess(template, opts); + + expect(preprocessed.toString()).toContain('div :global(span){}'); + }); + + it('allows mixing with the :global(...) syntax', async () => { + const template = ''; + const opts = autoProcess(); + const preprocessed = await preprocess(template, opts); + + expect(preprocessed.toString()).toMatch( + // either be div :global(span .cls) {} + // or div :global(span) :global(.cls) {} + /div (:global\(span .cls\)\{\}|:global\(span\) :global\(\.cls\)\{\})/, + ); + }); +}); diff --git a/test/transformers/scss.test.ts b/test/transformers/scss.test.ts index dbbac95d..b04108a2 100644 --- a/test/transformers/scss.test.ts +++ b/test/transformers/scss.test.ts @@ -7,7 +7,7 @@ import { Options } from '../../src/types'; const implementation: Options.Sass['implementation'] = { render(options, callback) { callback(null, { - css: Buffer.from('Foo'), + css: Buffer.from('div#red{color:red}'), stats: { entry: 'data', start: 0, @@ -18,7 +18,7 @@ const implementation: Options.Sass['implementation'] = { }); }, renderSync: () => ({ - css: Buffer.from('Bar'), + css: Buffer.from('div#green{color:green}'), stats: { entry: 'data', start: 0, @@ -30,7 +30,7 @@ const implementation: Options.Sass['implementation'] = { }; describe('transformer - scss', () => { - it('should prepend scss content via `data` option property - via defaul async render', async () => { + it('should prepend scss content via `data` option property - via default async render', async () => { const template = ``; const opts = getAutoPreprocess({ scss: { @@ -58,7 +58,7 @@ describe('transformer - scss', () => { }, }); const preprocessed = await preprocess(template, opts); - expect(preprocessed.toString()).toContain('Foo'); + expect(preprocessed.toString()).toContain('div#red{color:red}'); }); it('should prepend scss content via `data` option property - via renderSync', async () => { @@ -95,6 +95,6 @@ describe('transformer - scss', () => { }, }); const preprocessed = await preprocess(template, opts); - expect(preprocessed.toString()).toContain('Bar'); + expect(preprocessed.toString()).toContain('div#green{color:green}'); }); }); diff --git a/test/utils.test.ts b/test/utils.test.ts index 5ffe23b1..ce1160d6 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -1,6 +1,7 @@ import { resolve } from 'path'; -import { importAny, getIncludePaths } from '../src/utils'; +import { getIncludePaths } from '../src/utils'; +import { importAny } from '../src/modules/importAny'; describe('utils - importAny', () => { it('should throw error when none exist', () => {