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', () => {