├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── rollup │ ├── .gitignore │ ├── .vscode │ │ └── extensions.json │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.png │ │ ├── global.css │ │ └── index.html │ ├── rollup.config.js │ ├── src │ │ ├── App.svelte │ │ ├── global.d.ts │ │ └── main.ts │ └── tsconfig.json ├── svelte-kit │ ├── .gitignore │ ├── README.md │ ├── jsconfig.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── app.html │ │ ├── global.d.ts │ │ └── routes │ │ │ └── index.svelte │ ├── static │ │ └── favicon.png │ └── svelte.config.js └── webpack │ ├── App.svelte │ ├── app.module.css │ ├── app2.module.css │ ├── app3.module.css │ ├── components │ ├── Body.svelte │ └── Time.svelte │ ├── dist │ ├── bundle.js │ └── index.html │ ├── main.js │ ├── package-lock.json │ ├── package.json │ └── webpack.config.js ├── package-lock.json ├── package.json ├── src ├── index.ts ├── lib │ ├── camelCase.ts │ ├── generateName.ts │ ├── getHashDijest.ts │ ├── getLocalIdent.ts │ ├── index.ts │ └── requirement.ts ├── parsers │ ├── importDeclaration.ts │ ├── index.ts │ └── template.ts ├── processors │ ├── index.ts │ ├── mixed.ts │ ├── native.ts │ ├── processor.ts │ └── scoped.ts └── types │ └── index.ts ├── tasks └── parser.mjs ├── test ├── assets │ ├── class.module.css │ └── style.module.css ├── compiler.js ├── globalFixtures │ ├── bindVariable.test.js │ ├── class.test.js │ ├── keyframes.test.js │ ├── options.test.js │ └── template.test.js ├── mixedFixtures │ └── stylesAttribute.test.js ├── nativeFixtures │ ├── stylesAttribute.test.js │ └── stylesImports.test.js └── scopedFixtures │ ├── stylesAttribute.test.js │ └── stylesImports.test.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx}] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 100 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/ 3 | example/ 4 | dist/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | es2021: true 5 | }, 6 | extends: [ 7 | 'airbnb-base', 8 | 'plugin:@typescript-eslint/recommended', 9 | 'prettier', 10 | 'prettier/@typescript-eslint' 11 | ], 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | ecmaVersion: 12, 15 | sourceType: 'module' 16 | }, 17 | plugins: ['@typescript-eslint'], 18 | rules: { 19 | 'comma-dangle': ['error', "only-multiline"], 20 | 'import/extensions': [ 21 | 'error', 22 | 'never', 23 | { 24 | ignorePackages: true 25 | } 26 | ], 27 | 'lines-between-class-members': [ 28 | 'error', 29 | 'always', 30 | { 31 | exceptAfterSingleLine: true 32 | } 33 | ], 34 | 'no-const-assign': 'error' 35 | }, 36 | settings: { 37 | 'import/resolver': { 38 | node: { 39 | extensions: ['.ts', '.d.ts'] 40 | } 41 | } 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | example/ 3 | dist/ 4 | *.css -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "parser": "typescript", 4 | "singleQuote": true, 5 | "semi": true, 6 | "tabWidth": 2, 7 | "trailingComma": "es5" 8 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Svelte preprocess CSS Modules, changelog 2 | 3 | ## 3.0.1 (Feb 7 2025) 4 | 5 | ### Fixes 6 | 7 | - Add support to class objects and arrays 8 | 9 | ## 3.0.0 (Jan 17 2025) 10 | 11 | ### Update 12 | 13 | - Support for svelte 5 [#124](https://github.com/micantoine/svelte-preprocess-cssmodules/issues/124) 14 | - Use modern AST 15 | 16 | ### Breaking Changes 17 | - Remove `linearPreprocess` util since it is not being needed anymore 18 | - Set peer dependencies to svelte 5 only 19 | 20 | ## 2.2.5 (Sept 19, 2024) 21 | 22 | ### Updates 23 | 24 | - Replace deprecated method by use of `walk()` from `estree-walker` [#100](https://github.com/micantoine/svelte-preprocess-cssmodules/pull/100) 25 | - Upgrade dev dependencies 26 | - Add svelte 4 to peer dependencies 27 | 28 | ### Fixes 29 | 30 | - Make `cssModules()` parameter optional [#94](https://github.com/micantoine/svelte-preprocess-cssmodules/issues/94) 31 | - Remove typescript from peer dependencies (not needed, keep in dev dependencies) [#93](https://github.com/micantoine/svelte-preprocess-cssmodules/issues/93) 32 | - Properly transform `animation-name` [#98](https://github.com/micantoine/svelte-preprocess-cssmodules/issues/98) 33 | 34 | ## 2.2.4 (Jan 20, 2022) 35 | 36 | ### Fixes 37 | 38 | - Syntax error on keyframes for native mode [issue 84](https://github.com/micantoine/svelte-preprocess-cssmodules/issues/84) 39 | - Prevent svelte to remove the keyframes rule if no html tag exist [issue 76](https://github.com/micantoine/svelte-preprocess-cssmodules/issues/76) 40 | 41 | ## 2.2.3 (June 21, 2022) 42 | 43 | ### Fixes 44 | 45 | - Add support for css binding on svelte blocks `{#if}` `{#each}` `{#await}` `{#key}` [issue 62](https://github.com/micantoine/svelte-preprocess-cssmodules/issues/62) 46 | 47 | ## 2.2.2 (June 21, 2022) 48 | 49 | ### Fixes 50 | 51 | - Set default hash method to `md5` to support node17+ [issue 60](https://github.com/micantoine/svelte-preprocess-cssmodules/issues/60) 52 | 53 | ## 2.2.1 (May 26, 2022) 54 | 55 | ### Fixes 56 | 57 | - Destructuring import with commonjs 58 | - Emphasize on named imports instead of default 59 | 60 | ## 2.2.0 (Apr 6, 2022) 61 | 62 | ### Features 63 | - CSS Binding 64 | - Linear preprocess utility 65 | 66 | ### Updates 67 | - More detailed Readme 68 | 69 | ## 2.1.3 (Mar 14, 2022) 70 | 71 | ### Fixes 72 | - Normalise `includePaths` [issue 42](https://github.com/micantoine/svelte-preprocess-cssmodules/issues/42) 73 | - Readme typos 74 | 75 | ### Updates 76 | - Dependencies 77 | 78 | ## 2.1.2 (Jan 8, 2022) 79 | 80 | - Fix multiline class attribute [issue 39](https://github.com/micantoine/svelte-preprocess-cssmodules/issues/39) 81 | 82 | ## 2.1.1 (Oct 27, 2021) 83 | 84 | - Fix readme 85 | 86 | ## 2.1.0 (Oct 20, 2021) 87 | ### Features 88 | - SvelteKit support 89 | - `useAsDefaultScoping` option 90 | - `parseExternalStylesheet` option 91 | 92 | ### Breaking changes 93 | - Rename option `allowedAttributes` to `includeAttributes` 94 | - External cssModules stylesheets are not being processed automatically. 95 | 96 | ## 2.1.0-rc.2 (Oct 7, 2021) 97 | ### Features 98 | - Add option `useAsDefaultScoping` to enable cssModules globally without the need of the `module` attribute 99 | 100 | ### Breaking changes 101 | - Rename option `allowedAttributes` to `includeAttributes` 102 | - Add option `parseExternalStylesheet` to manually enable the parsing of imported stylesheets *(no more enabled by default)* 103 | 104 | ## 2.1.0-rc.1 (Oct 6, 2021) 105 | - Add ESM distribution 106 | 107 | ## 2.0.2 (May 26, 2021) 108 | - Fix Readme 109 | 110 | ## 2.0.1 (May 6, 2021) 111 | - Fix shorthand directive breaking regular directive 112 | 113 | ## 2.0.0 (May 1, 2021) 114 | New public release 115 | 116 | ## 2.0.0-rc.3 (April 20, 2021) 117 | 118 | ### Features 119 | - Add `:local()` selector 120 | ### Fixes 121 | - Fix native parsing 122 | 123 | ## 2.0.0-rc.2 (April 16, 2021) 124 | 125 | ### Features 126 | - Add option `hashSeeder` to customize the source of the hashing method 127 | - Add option `allowedAttributes` to parse other attributes than `class` 128 | ### Fixes 129 | - Replace `class` attribute on HTML elements and inline components 130 | - Fix external import on `native` & `mixed` mode when ` 48 | 49 |

My red text

50 | ``` 51 | 52 | The component will be compiled to 53 | 54 | ```html 55 | 58 | 59 |

My red text

60 | ``` 61 | 62 | ### Approach 63 | 64 | The default svelte scoping appends every css selectors with a unique class to only affect the elements of the component. 65 | 66 | [CSS Modules](https://github.com/css-modules/css-modules) **scopes each class name** with a unique id/name in order to affect the elements of the component. As the other selectors are not scoped, it is recommended to write each selector with a class. 67 | 68 | ```html 69 | 70 |

lorem ipsum tut moue

71 |

lorem ipsum tut moue

72 | 73 | 77 | ``` 78 | ```html 79 | 80 |

lorem ipsum tut moue

81 |

lorem ipsum tut moue

82 | 83 | 87 | ``` 88 | 89 | _transformed to_ 90 | 91 | ```html 92 | 93 |

lorem ipsum tut moue

94 |

lorem ipsum tut moue

95 | 96 | 100 | ``` 101 | 102 | ```html 103 | 104 |

lorem ipsum tut moue

105 |

lorem ipsum tut moue

106 | 107 | 111 | ``` 112 | 113 | ### Class objects and arrays 114 | 115 | #### Object with thruthy values 116 | 117 | ```html 118 | 121 | 122 |
...
123 | 124 | 129 | ``` 130 | 131 | *generating* 132 | 133 | ```html 134 |
...
135 | 136 |
...
137 | 138 | 143 | ``` 144 | 145 | #### Array with thruthy values 146 | 147 | ```html 148 | 151 | 152 |
...
153 | 154 | 160 | ``` 161 | 162 | *generating* 163 | 164 | ```html 165 |
...
166 | 167 |
...
168 | 169 | 175 | ``` 176 | 177 | ### Class directive 178 | 179 | Toggle a class on an element. 180 | 181 | ```html 182 | 186 | 187 | 190 | 191 | Home 192 | 193 | Home 194 | ``` 195 | 196 | _generating_ 197 | 198 | ```html 199 | 202 | 203 | Home 204 | ``` 205 | 206 | #### Use of shorthand 207 | 208 | ```html 209 | 213 | 214 | 217 | 218 | Home 219 | ``` 220 | 221 | _generating_ 222 | 223 | ```html 224 | 227 | 228 | Home 229 | ``` 230 | 231 | ### Local selector 232 | 233 | Force a selector to be scoped within its component to prevent style inheritance on child components. 234 | 235 | `:local()` is doing the opposite of `:global()` and can only be used with the `native` and `mixed` modes ([see preprocessor modes](#preprocessor-modes)). The svelte scoping is applied to the selector inside `:local()`. 236 | 237 | ```html 238 | 239 | 240 | 244 | 245 |
246 |

My main lorem ipsum tuye

247 | 248 |
249 | ``` 250 | ```html 251 | 252 | 253 | 262 | 263 |

My secondary lorem ipsum tuye

264 | ``` 265 | 266 | *generating* 267 | 268 | ```html 269 | 270 | 271 | 275 | 276 |
277 |

My main lorem ipsum tuye

278 | 279 |
280 | ``` 281 | ```html 282 | 283 | 284 | 287 | 288 |

My secondary lorem ipsum tuye

289 | ``` 290 | 291 | When used with a class, `:local()` cssModules is replaced by the svelte scoping system. This could be useful when targetting global classnames. 292 | 293 | ```html 294 | 303 | 304 |
305 | 306 | 307 |
308 | ``` 309 | 310 | *generating* 311 | 312 | ```html 313 | 321 | 322 |
323 | 324 | 325 |
326 | ``` 327 | 328 | ### CSS binding 329 | 330 | Link the value of a CSS property to a dynamic variable by using `bind()`. 331 | 332 | ```html 333 | 336 | 337 |

My lorem ipsum text

338 | 339 | 346 | ``` 347 | 348 | A scoped css variable, binding the declared statement, will be created on the component **root** elements which the css property will inherit from. 349 | 350 | ```html 351 | 354 | 355 |

356 | My lorem ipsum text 357 |

358 | 359 | 366 | ``` 367 | 368 | An object property can also be targetted and must be wrapped with quotes. 369 | 370 | ```html 371 | 376 | 377 |
378 |

Heading

379 |

My lorem ipsum text

380 |
381 | 382 | 391 | ``` 392 | 393 | _generating_ 394 | 395 | ```html 396 | 401 | 402 |
403 |

Heading

404 |

My lorem ipsum text

405 |
406 | 407 | 416 | ``` 417 | 418 | ### Scoped class on child components 419 | 420 | CSS Modules allows you to pass a scoped classname to a child component giving the possibility to style it from its parent. (Only with the `native` and `mixed` modes – [See preprocessor modes](#preprocessor-modes)). 421 | 422 | ```html 423 | 424 | 427 | 428 | 431 | 432 | 438 | ``` 439 | 440 | ```html 441 | 442 | 443 | 446 | 447 |
448 |

Welcome

449 |

Lorem ipsum tut ewou tu po

450 | 451 |
452 | 453 | 463 | ``` 464 | 465 | _generating_ 466 | 467 | ```html 468 |
469 |

Welcome

470 |

Lorem ipsum tut ewou tu po

471 | 472 |
473 | 474 | 488 | ``` 489 | 490 | 491 | ## Import styles from an external stylesheet 492 | 493 | Alternatively, styles can be created into an external file and imported onto a svelte component. The name referring to the import can then be used on the markup to target any existing classname of the stylesheet. 494 | 495 | - The option `parseExternalStylesheet` need to be enabled. 496 | - The css file must follow the convention `[FILENAME].module.css` in order to be processed. 497 | 498 | **Note:** *That import is only meant for stylesheets relative to the component. You will have to set your own bundler in order to import *node_modules* css files.* 499 | 500 | ```css 501 | /** style.module.css **/ 502 | .red { color: red; } 503 | .blue { color: blue; } 504 | ``` 505 | ```html 506 | 507 | 510 | 511 |

My red text

512 |

My blue text

513 | ``` 514 | 515 | *generating* 516 | 517 | ```html 518 | 522 | 523 |

My red text

524 |

My blue text

525 | ``` 526 | 527 | ### Destructuring import 528 | 529 | ```css 530 | /** style.module.css **/ 531 | section { padding: 10px; } 532 | .red { color: red; } 533 | .blue { color: blue; } 534 | .bold { font-weight: bold; } 535 | ``` 536 | ```html 537 | 538 | 541 | 542 |
543 |

My red text

544 |

My blue text

545 |
546 | ``` 547 | 548 | *generating* 549 | 550 | ```html 551 | 557 | 558 |
559 |

My red text

560 |

My blue text

561 |
562 | ``` 563 | 564 | ### kebab-case situation 565 | 566 | The kebab-case class names are being transformed to a camelCase version to facilitate their use on Markup and Javascript. 567 | 568 | ```css 569 | /** style.module.css **/ 570 | .success { color: green; } 571 | .error-message { 572 | color: red; 573 | text-decoration: line-through; 574 | } 575 | ``` 576 | ```html 577 | 580 | 581 |

My success text

582 |

My error message

583 | 584 | 585 | 586 | 589 | 590 |

My success message

591 |

My error message

592 | ``` 593 | 594 | *generating* 595 | 596 | ```html 597 | 604 | 605 |

My success messge

606 |

My error message

607 | ``` 608 | 609 | ### Unnamed import 610 | 611 | If a css file is being imported without a name, CSS Modules will still apply to the classes of the stylesheet. 612 | 613 | ```css 614 | /** style.module.css **/ 615 | p { font-size: 18px; } 616 | .success { color: green; } 617 | ``` 618 | ```html 619 | 622 | 623 |

My success message

624 |

My another message

625 | ``` 626 | 627 | *generating* 628 | 629 | ```html 630 | 634 | 635 |

My success messge

636 |

My error message

637 | ``` 638 | 639 | ### Directive and Dynamic class 640 | 641 | Use the Svelte's builtin `class:` directive or javascript template to display a class dynamically. 642 | **Note**: the *shorthand directive* is **NOT working** with imported CSS Module identifiers. 643 | 644 | ```html 645 | 651 | 652 | 653 | 654 | 655 |

Success

656 | 657 | 658 |

Notice

661 | 662 |

Notice

663 |

Notice

664 | ``` 665 | 666 | ## Preprocessor Modes 667 | 668 | The mode can be **set globally from the config** or **locally to override the global setting**. 669 | 670 | ### Native 671 | 672 | Scopes classes with CSS Modules, anything else is unscoped. 673 | 674 | Pros: 675 | 676 | - uses default [CSS Modules](https://github.com/css-modules/css-modules) approach 677 | - creates unique ID to avoid classname conflicts and unexpected inheritances 678 | - passes scoped class name to child components 679 | 680 | Cons: 681 | 682 | - does not scope non class selectors. 683 | - forces to write selectors with classes. 684 | - needs to consider third party plugins with `useAsDefaultScoping` on – [Read more](#useasdefaultscoping). 685 | 686 | ### Mixed 687 | 688 | Scopes non-class selectors with svelte scoping in addition to `native` (same as preprocessor `v1`) 689 | 690 | ```html 691 | 695 | 696 |

My red text

697 | ``` 698 | 699 | _generating_ 700 | 701 | ```html 702 | 706 | 707 |

My red text

708 | ``` 709 | 710 | Pros: 711 | 712 | - creates class names with unique ID to avoid conflicts and unexpected inheritances 713 | - uses svelte scoping on non class selectors 714 | - passes scoped class name to child components 715 | 716 | Cons: 717 | 718 | - adds more weight to tag selectors than class selectors (because of the svelte scoping) 719 | 720 | ```html 721 | 725 | 726 | 739 | 740 | 741 | 742 | 746 | 747 | 755 | ``` 756 | 757 | ### Scoped 758 | 759 | Scopes classes with svelte scoping in addition to `mixed`. 760 | 761 | ```html 762 | 766 | 767 |

My red text

768 | ``` 769 | 770 | _generating_ 771 | 772 | ```html 773 | 777 | 778 |

My red text

779 | ``` 780 | 781 | Pros: 782 | 783 | - creates class names with unique ID to avoid conflicts and unexpected inheritances 784 | - scopes every selectors at equal weight 785 | 786 | Cons: 787 | 788 | - does not pass scoped classname to child components 789 | 790 | ### Comparative 791 | 792 | | | Svelte scoping | Preprocessor Native | Preprocessor Mixed | Preprocessor Scoped | 793 | | -------------| ------------- | ------------- | ------------- | ------------- | 794 | | Scopes classes | O | O | O | O | 795 | | Scopes non class selectors | O | X | O | O | 796 | | Creates unique class ID | X | O | O | O | 797 | | Has equal selector weight | O | O | X | O | 798 | | Passes scoped classname to a child component | X | O | O | X | 799 | 800 | ## Why CSS Modules over Svelte scoping? 801 | 802 | - **On a full svelte application**: it is just a question of taste as the default svelte scoping is largely enough. Component styles will never inherit from other styling. 803 | 804 | - **On a hybrid project** (like using svelte to enhance a web page): the default scoping may actually inherits from a class of the same name belonging to the style of the page. In that case using CSS Modules to create a unique ID and to avoid class inheritance might be advantageous. 805 | 806 | ## Configuration 807 | 808 | ### Rollup 809 | 810 | To be used with the plugin [`rollup-plugin-svelte`](https://github.com/sveltejs/rollup-plugin-svelte). 811 | 812 | ```js 813 | import svelte from 'rollup-plugin-svelte'; 814 | import { cssModules } from 'svelte-preprocess-cssmodules'; 815 | 816 | export default { 817 | ... 818 | plugins: [ 819 | svelte({ 820 | preprocess: [ 821 | cssModules(), 822 | ] 823 | }), 824 | ] 825 | ... 826 | } 827 | ``` 828 | 829 | ### Webpack 830 | 831 | To be used with the loader [`svelte-loader`](https://github.com/sveltejs/svelte-loader). 832 | 833 | ```js 834 | const { cssModules } = require('svelte-preprocess-cssmodules'); 835 | 836 | module.exports = { 837 | ... 838 | module: { 839 | rules: [ 840 | { 841 | test: /\.svelte$/, 842 | exclude: /node_modules/, 843 | use: [ 844 | { 845 | loader: 'svelte-loader', 846 | options: { 847 | preprocess: [ 848 | cssModules(), 849 | ] 850 | } 851 | } 852 | ] 853 | } 854 | ] 855 | } 856 | ... 857 | } 858 | ``` 859 | 860 | ### SvelteKit 861 | 862 | As the module distribution is targetting `esnext`, `Node.js 14` or above is required 863 | in order to work. 864 | 865 | ```js 866 | // svelte.config.js 867 | 868 | import { cssModules } from 'svelte-preprocess-cssmodules'; 869 | 870 | const config = { 871 | ... 872 | preprocess: [ 873 | cssModules(), 874 | ] 875 | }; 876 | 877 | export default config; 878 | ``` 879 | 880 | ### Svelte Preprocess 881 | 882 | The CSS Modules preprocessor requires the compoment to be a standard svelte component (using vanilla js and vanilla css). if any other code, such as Typescript or Sass, is encountered, an error will be thrown. Therefore CSS Modules needs to be run at the very end. 883 | 884 | ```js 885 | import { typescript, scss } from 'svelte-preprocess'; 886 | import { cssModules } from 'svelte-preprocess-cssmodules'; 887 | 888 | ... 889 | // svelte config: 890 | preprocess: [ 891 | typescript(), 892 | scss(), 893 | cssModules(), // run last 894 | ], 895 | ... 896 | ``` 897 | 898 | ### Vite 899 | 900 | Set the `svelte.config.js` accordingly. 901 | 902 | ```js 903 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 904 | import { cssModules } from 'svelte-preprocess-cssmodules'; 905 | 906 | export default { 907 | preprocess: [ 908 | vitePreprocess(), 909 | cssModules() 910 | ] 911 | }; 912 | ``` 913 | 914 | 915 | ### Options 916 | Pass an object of the following properties 917 | 918 | | Name | Type | Default | Description | 919 | | ------------- | ------------- | ------------- | ------------- | 920 | | `cssVariableHash` | `{String}` | `[hash:base64:6]` | The hash type ([see locatonIdentName](#localidentname)) | 921 | | [`getLocalIdent`](#getlocalident) | `Function` | `undefined` | Generate the classname by specifying a function instead of using the built-in interpolation | 922 | | [`hashSeeder`](#hashseeder) | `{Array}` | `['style', 'filepath', 'classname']` | An array of keys to base the hash on | 923 | | [`includeAttributes`](#includeattributes) | `{Array}` | `[]` | An array of attributes to parse along with `class` | 924 | | `includePaths` | `{Array}` | `[]` (Any) | An array of paths to be processed | 925 | | [`localIdentName`](#localidentname) | `{String}` | `"[local]-[hash:base64:6]"` | A rule using any available token | 926 | | `mode` | `native\|mixed\|scoped` | `native` | The preprocess mode to use 927 | | `parseExternalStylesheet` | `{Boolean}` | `false` | Enable parsing on imported external stylesheet | 928 | | `parseStyleTag` | `{Boolean}` | `true` | Enable parsing on style tag | 929 | | [`useAsDefaultScoping`](#useasdefaultscoping) | `{Boolean}` | `false` | Replace svelte scoping globally | 930 | 931 | #### `getLocalIdent` 932 | 933 | Customize the creation of the classname instead of relying on the built-in function. 934 | 935 | ```ts 936 | function getLocalIdent( 937 | context: { 938 | context: string, // the context path 939 | resourcePath: string, // path + filename 940 | }, 941 | localIdentName: { 942 | template: string, // the template rule 943 | interpolatedName: string, // the built-in generated classname 944 | }, 945 | className: string, // the classname string 946 | content: { 947 | markup: string, // the markup content 948 | style: string, // the style content 949 | } 950 | ): string { 951 | return `your_generated_classname`; 952 | } 953 | ``` 954 | 955 | 956 | *Example of use* 957 | 958 | ```bash 959 | # Directory 960 | SvelteApp 961 | └─ src 962 | ├─ App.svelte 963 | └─ components 964 | └─ Button.svelte 965 | ``` 966 | ```html 967 | 968 | 969 | 970 | 973 | ``` 974 | 975 | ```js 976 | // Preprocess config 977 | ... 978 | preprocess: [ 979 | cssModules({ 980 | localIdentName: '[path][name]__[local]', 981 | getLocalIdent: (context, { interpolatedName }) => { 982 | return interpolatedName.toLowerCase().replace('src_', ''); 983 | // svelteapp_components_button__red; 984 | } 985 | }) 986 | ], 987 | ... 988 | ``` 989 | 990 | #### `hashSeeder` 991 | 992 | Set the source of the hash (when using `[hash]` / `[contenthash]`). 993 | 994 | The list of available keys are: 995 | 996 | - `style` the content of the style tag (or the imported stylesheet) 997 | - `filepath` the path of the component 998 | - `classname` the local classname 999 | 1000 | *Example of use: creating a common hash per component* 1001 | ```js 1002 | // Preprocess config 1003 | ... 1004 | preprocess: [ 1005 | cssModules({ 1006 | hashSeeder: ['filepath', 'style'], 1007 | }) 1008 | ], 1009 | ... 1010 | ``` 1011 | ```html 1012 | 1013 | 1014 | 1018 | ``` 1019 | 1020 | _generating_ 1021 | 1022 | ```html 1023 | 1024 | 1025 | 1029 | ``` 1030 | 1031 | #### `includeAttributes` 1032 | 1033 | Add other attributes than `class` to be parsed by the preprocesser 1034 | 1035 | ```js 1036 | // Preprocess config 1037 | ... 1038 | preprocess: [ 1039 | cssModules({ 1040 | includeAttributes: ['data-color', 'classname'], 1041 | }) 1042 | ], 1043 | ... 1044 | ``` 1045 | ```html 1046 | 1047 | 1048 | 1052 | ``` 1053 | 1054 | _generating_ 1055 | 1056 | ```html 1057 | 1058 | 1059 | 1063 | ``` 1064 | 1065 | #### `localIdentName` 1066 | 1067 | Inspired by [webpack interpolateName](https://github.com/webpack/loader-utils#interpolatename), here is the list of tokens: 1068 | 1069 | - `[local]` the targeted classname 1070 | - `[ext]` the extension of the resource 1071 | - `[name]` the basename of the resource 1072 | - `[path]` the path of the resource 1073 | - `[folder]` the folder the resource is in 1074 | - `[contenthash]` or `[hash]` *(they are the same)* the hash of the resource content (by default it's the hex digest of the md5 hash) 1075 | - `[:contenthash::]` optionally one can configure 1076 | - other hashTypes, i. e. `sha1`, `md5`, `sha256`, `sha512` 1077 | - other digestTypes, i. e. `hex`, `base26`, `base32`, `base36`, `base49`, `base52`, `base58`, `base62`, `base64` 1078 | - and `length` the length in chars 1079 | 1080 | #### `useAsDefaultScoping` 1081 | 1082 | Globally replace the default svelte scoping by the CSS Modules scoping. As a result, the `module` attribute to ` 1101 | ``` 1102 | 1103 | _generating_ 1104 | 1105 | ```html 1106 |

Welcome

1107 | 1110 | ``` 1111 | 1112 | **Potential issue with third party plugins** 1113 | 1114 | The preprocessor requires you to add the `module` attribute to ` 1171 | 1172 | 1182 | ``` 1183 | 1184 | *Final html code generated by svelte* 1185 | 1186 | ```html 1187 | 1204 | 1205 |
1206 |
My Modal Title
1207 |
1208 |

Lorem ipsum dolor sit, amet consectetur.

1209 |
1210 |
1211 | 1212 | 1213 |
1214 |
1215 | ``` 1216 | ## License 1217 | 1218 | [MIT](https://opensource.org/licenses/MIT) 1219 | -------------------------------------------------------------------------------- /example/rollup/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /public/build/ 3 | 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /example/rollup/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["svelte.svelte-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /example/rollup/README.md: -------------------------------------------------------------------------------- 1 | *Psst — looking for a more complete solution? Check out [SvelteKit](https://kit.svelte.dev), the official framework for building web applications of all sizes, with a beautiful development experience and flexible filesystem-based routing.* 2 | 3 | *Looking for a shareable component template instead? You can [use SvelteKit for that as well](https://kit.svelte.dev/docs#packaging) or the older [sveltejs/component-template](https://github.com/sveltejs/component-template)* 4 | 5 | --- 6 | 7 | # svelte app 8 | 9 | This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template. 10 | 11 | To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit): 12 | 13 | ```bash 14 | npx degit sveltejs/template svelte-app 15 | cd svelte-app 16 | ``` 17 | 18 | *Note that you will need to have [Node.js](https://nodejs.org) installed.* 19 | 20 | 21 | ## Get started 22 | 23 | Install the dependencies... 24 | 25 | ```bash 26 | cd svelte-app 27 | npm install 28 | ``` 29 | 30 | ...then start [Rollup](https://rollupjs.org): 31 | 32 | ```bash 33 | npm run dev 34 | ``` 35 | 36 | Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes. 37 | 38 | By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`. 39 | 40 | If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense. 41 | 42 | ## Building and running in production mode 43 | 44 | To create an optimised version of the app: 45 | 46 | ```bash 47 | npm run build 48 | ``` 49 | 50 | You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com). 51 | 52 | 53 | ## Single-page app mode 54 | 55 | By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere. 56 | 57 | If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json: 58 | 59 | ```js 60 | "start": "sirv public --single" 61 | ``` 62 | 63 | ## Using TypeScript 64 | 65 | This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with: 66 | 67 | ```bash 68 | node scripts/setupTypeScript.js 69 | ``` 70 | 71 | Or remove the script via: 72 | 73 | ```bash 74 | rm scripts/setupTypeScript.js 75 | ``` 76 | 77 | If you want to use `baseUrl` or `path` aliases within your `tsconfig`, you need to set up `@rollup/plugin-alias` to tell Rollup to resolve the aliases. For more info, see [this StackOverflow question](https://stackoverflow.com/questions/63427935/setup-tsconfig-path-in-svelte). 78 | 79 | ## Deploying to the web 80 | 81 | ### With [Vercel](https://vercel.com) 82 | 83 | Install `vercel` if you haven't already: 84 | 85 | ```bash 86 | npm install -g vercel 87 | ``` 88 | 89 | Then, from within your project folder: 90 | 91 | ```bash 92 | cd public 93 | vercel deploy --name my-project 94 | ``` 95 | 96 | ### With [surge](https://surge.sh/) 97 | 98 | Install `surge` if you haven't already: 99 | 100 | ```bash 101 | npm install -g surge 102 | ``` 103 | 104 | Then, from within your project folder: 105 | 106 | ```bash 107 | npm run build 108 | surge public my-project.surge.sh 109 | ``` 110 | -------------------------------------------------------------------------------- /example/rollup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "rollup -c", 7 | "dev": "rollup -c -w", 8 | "start": "sirv public --no-clear", 9 | "check": "svelte-check --tsconfig ./tsconfig.json" 10 | }, 11 | "devDependencies": { 12 | "@rollup/plugin-commonjs": "^17.0.0", 13 | "@rollup/plugin-node-resolve": "^11.0.0", 14 | "@rollup/plugin-typescript": "^11.1.6", 15 | "@tsconfig/svelte": "^5.0.4", 16 | "rollup": "^3.29.5", 17 | "rollup-plugin-css-only": "^3.1.0", 18 | "rollup-plugin-livereload": "^2.0.0", 19 | "rollup-plugin-svelte": "^7.2.2", 20 | "rollup-plugin-terser": "^7.0.0", 21 | "svelte": "^4.2.19", 22 | "svelte-check": "^4.0.2", 23 | "svelte-preprocess": "^6.0.2", 24 | "tslib": "^2.7.0", 25 | "typescript": "^5.6.2" 26 | }, 27 | "dependencies": { 28 | "sass": "^1.79.1", 29 | "sirv-cli": "^1.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/rollup/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/micantoine/svelte-preprocess-cssmodules/29ab98972e32f48949c435fb213e053a1d0d95db/example/rollup/public/favicon.png -------------------------------------------------------------------------------- /example/rollup/public/global.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | body { 8 | color: #333; 9 | margin: 0; 10 | padding: 8px; 11 | box-sizing: border-box; 12 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 13 | } 14 | 15 | a { 16 | color: rgb(0,100,200); 17 | text-decoration: none; 18 | } 19 | 20 | a:hover { 21 | text-decoration: underline; 22 | } 23 | 24 | a:visited { 25 | color: rgb(0,80,160); 26 | } 27 | 28 | label { 29 | display: block; 30 | } 31 | 32 | input, button, select, textarea { 33 | font-family: inherit; 34 | font-size: inherit; 35 | -webkit-padding: 0.4em 0; 36 | padding: 0.4em; 37 | margin: 0 0 0.5em 0; 38 | box-sizing: border-box; 39 | border: 1px solid #ccc; 40 | border-radius: 2px; 41 | } 42 | 43 | input:disabled { 44 | color: #ccc; 45 | } 46 | 47 | button { 48 | color: #333; 49 | background-color: #f4f4f4; 50 | outline: none; 51 | } 52 | 53 | button:disabled { 54 | color: #999; 55 | } 56 | 57 | button:not(:disabled):active { 58 | background-color: #ddd; 59 | } 60 | 61 | button:focus { 62 | border-color: #666; 63 | } 64 | -------------------------------------------------------------------------------- /example/rollup/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Svelte app 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/rollup/rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import livereload from 'rollup-plugin-livereload'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import sveltePreprocess, { typescript as typescriptSvelte, scss } from 'svelte-preprocess'; 7 | import typescript from '@rollup/plugin-typescript'; 8 | import css from 'rollup-plugin-css-only'; 9 | import { cssModules, linearPreprocess } from '../../dist/index'; 10 | 11 | const production = !process.env.ROLLUP_WATCH; 12 | 13 | function serve() { 14 | let server; 15 | 16 | function toExit() { 17 | if (server) server.kill(0); 18 | } 19 | 20 | return { 21 | writeBundle() { 22 | if (server) return; 23 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { 24 | stdio: ['ignore', 'inherit', 'inherit'], 25 | shell: true 26 | }); 27 | 28 | process.on('SIGTERM', toExit); 29 | process.on('exit', toExit); 30 | } 31 | }; 32 | } 33 | 34 | export default { 35 | input: 'src/main.ts', 36 | output: { 37 | sourcemap: true, 38 | format: 'iife', 39 | name: 'app', 40 | file: 'public/build/bundle.js' 41 | }, 42 | plugins: [ 43 | svelte({ 44 | // preprocess: sveltePreprocess({ sourceMap: !production }), 45 | preprocess: linearPreprocess([ 46 | typescriptSvelte(), 47 | scss(), 48 | cssModules(), 49 | ]), 50 | 51 | compilerOptions: { 52 | // enable run-time checks when not in production 53 | dev: !production 54 | } 55 | }), 56 | // we'll extract any component CSS out into 57 | // a separate file - better for performance 58 | css({ output: 'bundle.css' }), 59 | 60 | // If you have external dependencies installed from 61 | // npm, you'll most likely need these plugins. In 62 | // some cases you'll need additional configuration - 63 | // consult the documentation for details: 64 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 65 | resolve({ 66 | browser: true, 67 | dedupe: ['svelte'] 68 | }), 69 | commonjs(), 70 | typescript({ 71 | sourceMap: !production, 72 | inlineSources: !production 73 | }), 74 | 75 | // In dev mode, call `npm run start` once 76 | // the bundle has been generated 77 | !production && serve(), 78 | 79 | // Watch the `public` directory and refresh the 80 | // browser on changes when not in production 81 | !production && livereload('public'), 82 | 83 | // If we're building for production (npm run build 84 | // instead of npm run dev), minify 85 | production && terser() 86 | ], 87 | watch: { 88 | clearScreen: false 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /example/rollup/src/App.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |

Hello {name}!

7 |

Visit the Svelte tutorial to learn how to build Svelte apps.

8 |
9 | 10 | -------------------------------------------------------------------------------- /example/rollup/src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/rollup/src/main.ts: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | const app = new App({ 4 | target: document.body, 5 | props: { 6 | name: 'world' 7 | } 8 | }); 9 | 10 | export default app; -------------------------------------------------------------------------------- /example/rollup/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | 4 | "include": ["src/**/*"], 5 | "exclude": ["node_modules/*", "__sapper__/*", "public/*"] 6 | } -------------------------------------------------------------------------------- /example/svelte-kit/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | -------------------------------------------------------------------------------- /example/svelte-kit/README.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte); 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npm init svelte@next 12 | 13 | # create a new project in my-app 14 | npm init svelte@next my-app 15 | ``` 16 | 17 | > Note: the `@next` is temporary 18 | 19 | ## Developing 20 | 21 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 22 | 23 | ```bash 24 | npm run dev 25 | 26 | # or start the server and open the app in a new browser tab 27 | npm run dev -- --open 28 | ``` 29 | 30 | ## Building 31 | 32 | Before creating a production version of your app, install an [adapter](https://kit.svelte.dev/docs#adapters) for your target environment. Then: 33 | 34 | ```bash 35 | npm run build 36 | ``` 37 | 38 | > You can preview the built app with `npm run preview`, regardless of whether you installed an adapter. This should _not_ be used to serve your app in production. 39 | -------------------------------------------------------------------------------- /example/svelte-kit/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "$lib": ["src/lib"], 6 | "$lib/*": ["src/lib/*"] 7 | } 8 | }, 9 | "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] 10 | } 11 | -------------------------------------------------------------------------------- /example/svelte-kit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-kit", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "dev": "svelte-kit dev", 6 | "build": "svelte-kit build", 7 | "preview": "svelte-kit preview" 8 | }, 9 | "devDependencies": { 10 | "@sveltejs/kit": "next", 11 | "svelte": "^5.18.0" 12 | }, 13 | "type": "module" 14 | } -------------------------------------------------------------------------------- /example/svelte-kit/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %svelte.head% 8 | 9 | 10 |
%svelte.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /example/svelte-kit/src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/svelte-kit/src/routes/index.svelte: -------------------------------------------------------------------------------- 1 |

Welcome to SvelteKit

2 |

Visit kit.svelte.dev to read the documentation

3 | 4 | -------------------------------------------------------------------------------- /example/svelte-kit/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/micantoine/svelte-preprocess-cssmodules/29ab98972e32f48949c435fb213e053a1d0d95db/example/svelte-kit/static/favicon.png -------------------------------------------------------------------------------- /example/svelte-kit/svelte.config.js: -------------------------------------------------------------------------------- 1 | import cssModules from '../../dist/index.mjs'; 2 | 3 | /** @type {import('@sveltejs/kit').Config} */ 4 | const config = { 5 | kit: { 6 | // hydrate the
element in src/app.html 7 | target: '#svelte' 8 | }, 9 | preprocess: [ 10 | cssModules({ 11 | includePaths: ['./'] 12 | }), 13 | ] 14 | }; 15 | 16 | export default config; 17 | -------------------------------------------------------------------------------- /example/webpack/App.svelte: -------------------------------------------------------------------------------- 1 | 12 |
13 | 35 | 36 | -------------------------------------------------------------------------------- /example/webpack/app.module.css: -------------------------------------------------------------------------------- 1 | section div { 2 | color: #ffff; 3 | background-color: #000; 4 | } 5 | .error { 6 | color: red; 7 | } 8 | .error-message { 9 | text-decoration: line-through; 10 | } 11 | p > strong { font-weight: 600; } -------------------------------------------------------------------------------- /example/webpack/app2.module.css: -------------------------------------------------------------------------------- 1 | .success { 2 | color: lime; 3 | } 4 | .success-small { 5 | font-size: 14px; 6 | } 7 | .large { font-size: 18px; } -------------------------------------------------------------------------------- /example/webpack/app3.module.css: -------------------------------------------------------------------------------- 1 | header { 2 | background-color: #f8f8f8; 3 | } -------------------------------------------------------------------------------- /example/webpack/components/Body.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | {@render children?.()} 7 |
8 | 9 | -------------------------------------------------------------------------------- /example/webpack/components/Time.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
{time}
27 | 28 | -------------------------------------------------------------------------------- /example/webpack/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Svelte CSS modules loader 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /example/webpack/main.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'svelte'; 2 | import App from './App.svelte' 3 | 4 | const app = mount(App, { target: document.getElementById("app") }); 5 | -------------------------------------------------------------------------------- /example/webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-preprocess-cssmodules-example", 3 | "version": "0.0.0", 4 | "description": "Example using cssmodules preprocessor with svelte-loader", 5 | "main": "main.js", 6 | "private": true, 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "dev": "NODE_ENV=development webpack serve --config webpack.config.js --progress --profile", 10 | "build": "NODE_ENV=production webpack --config webpack.config.js --profile" 11 | }, 12 | "license": "MIT", 13 | "devDependencies": { 14 | "css-loader": "^5.2.7", 15 | "style-loader": "^4.0.0", 16 | "svelte": "^5.15.0", 17 | "svelte-loader": "^3.2.4", 18 | "webpack": "^5.97.1", 19 | "webpack-cli": "^6.0.0", 20 | "webpack-dev-server": "^5.2.0" 21 | }, 22 | "dependencies": { 23 | "swiper": "^6.8.4" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { cssModules } = require('../../'); 3 | 4 | const isProduction = process.env.NODE_ENV === 'production'; 5 | 6 | module.exports = { 7 | mode: process.env.NODE_ENV, 8 | entry: path.resolve(__dirname, 'main.js'), 9 | output: { 10 | filename: 'bundle.js', 11 | path: path.resolve(__dirname, 'dist'), 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(svelte|svelte\.js)$/, 17 | // exclude: /node_modules/, 18 | use: [ 19 | { 20 | loader: 'svelte-loader', 21 | options: { 22 | preprocess: [ 23 | cssModules({ 24 | parseExternalStylesheet: true, 25 | mode: 'native', 26 | includePaths: ['./'], 27 | }), 28 | ], 29 | emitCss: false 30 | } 31 | } 32 | ] 33 | }, 34 | { 35 | test: /node_modules\/svelte\/.*\.mjs$/, 36 | resolve: { 37 | fullySpecified: false 38 | } 39 | }, 40 | { 41 | test: /\.css$/i, 42 | use: ["style-loader", "css-loader"], 43 | }, 44 | ] 45 | }, 46 | resolve: { 47 | extensions: ['.mjs', '.js', '.svelte'], 48 | mainFields: ['svelte', 'browser', 'module', 'main'], 49 | conditionNames: ['svelte', 'browser'], 50 | fallback: { "events": false } 51 | }, 52 | devServer: { 53 | static: path.join(__dirname, 'dist'), 54 | port: 9090 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-preprocess-cssmodules", 3 | "version": "3.0.1", 4 | "description": "Svelte preprocessor to generate CSS Modules classname on Svelte components", 5 | "keywords": [ 6 | "svelte", 7 | "svelte-preprocess", 8 | "css-modules" 9 | ], 10 | "homepage": "https://github.com/micantoine/svelte-preprocess-cssmodules", 11 | "bugs": { 12 | "url": "https://github.com/micantoine/svelte-preprocess-cssmodules/issues" 13 | }, 14 | "author": { 15 | "name": "micantoine" 16 | }, 17 | "scripts": { 18 | "prebuild": "rm -rf dist/", 19 | "build": "npm run build:cjs && npm run build:esm", 20 | "build:cjs": "tsc --module commonjs --target es6 --outDir dist --declaration true", 21 | "build:esm": "tsc --module esnext --target esnext --outDir dist/esm && node tasks/parser.mjs && rm -rf dist/esm/", 22 | "dev": "npm run build:cjs -- -w", 23 | "lint": "eslint --ext .ts --fix ./src", 24 | "format": "prettier --write --loglevel warn ./{src,test}", 25 | "test": "jest", 26 | "prepublishOnly": "npm run test && npm run build" 27 | }, 28 | "license": "MIT", 29 | "main": "./dist/index.js", 30 | "module": "./dist/index.mjs", 31 | "types": "./dist/index.d.ts", 32 | "exports": { 33 | ".": { 34 | "import": "./dist/index.mjs", 35 | "require": "./dist/index.js" 36 | } 37 | }, 38 | "directories": { 39 | "example": "example" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "https://github.com/micantoine/svelte-preprocess-cssmodules.git" 44 | }, 45 | "husky": { 46 | "hooks": { 47 | "pre-commit": "lint-staged" 48 | } 49 | }, 50 | "lint-staged": { 51 | "*.{ts, js}": [ 52 | "eslint --fix", 53 | "prettier --write" 54 | ] 55 | }, 56 | "dependencies": { 57 | "acorn": "^8.5.0", 58 | "big.js": "^6.1.1", 59 | "estree-walker": "^2.0.2", 60 | "magic-string": "^0.25.7" 61 | }, 62 | "devDependencies": { 63 | "@types/big.js": "^6.1.2", 64 | "@types/estree": "0.0.47", 65 | "@typescript-eslint/eslint-plugin": "^5.62.0", 66 | "@typescript-eslint/parser": "^5.62.0", 67 | "eslint": "^7.10.0", 68 | "eslint-config-airbnb-base": "^14.2.0", 69 | "eslint-config-prettier": "^6.15.0", 70 | "eslint-plugin-import": "^2.22.1", 71 | "husky": "^4.3.0", 72 | "jest": "^26.0.1", 73 | "lint-staged": "^10.5.1", 74 | "prettier": "^3.3.3", 75 | "svelte": "^5.15.0", 76 | "typescript": "^4.9.5" 77 | }, 78 | "peerDependencies": { 79 | "svelte": "^5.15.0" 80 | }, 81 | "files": [ 82 | "dist/" 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-multi-assign */ 2 | import { parse } from 'svelte/compiler'; 3 | import type { AST, PreprocessorGroup, MarkupPreprocessor } from 'svelte/compiler'; 4 | import { mixedProcessor, nativeProcessor, scopedProcessor } from './processors'; 5 | import type { PluginOptions } from './types'; 6 | import { 7 | getLocalIdent, 8 | isFileIncluded, 9 | hasModuleImports, 10 | hasModuleAttribute, 11 | normalizeIncludePaths, 12 | } from './lib'; 13 | 14 | const defaultOptions = (): PluginOptions => { 15 | return { 16 | cssVariableHash: '[hash:base64:6]', 17 | getLocalIdent, 18 | hashSeeder: ['style', 'filepath', 'classname'], 19 | includeAttributes: [], 20 | includePaths: [], 21 | localIdentName: '[local]-[hash:base64:6]', 22 | mode: 'native', 23 | parseExternalStylesheet: false, 24 | parseStyleTag: true, 25 | useAsDefaultScoping: false, 26 | }; 27 | }; 28 | 29 | let pluginOptions: PluginOptions; 30 | 31 | /** 32 | * cssModules markup phase 33 | * @param param0 34 | * @returns the preprocessor markup 35 | */ 36 | const markup: MarkupPreprocessor = async ({ content, filename }) => { 37 | if ( 38 | !filename || 39 | !isFileIncluded(pluginOptions.includePaths, filename) || 40 | (!pluginOptions.parseStyleTag && !pluginOptions.parseExternalStylesheet) 41 | ) { 42 | return { code: content }; 43 | } 44 | 45 | let ast: AST.Root; 46 | try { 47 | ast = parse(content, { modern: true, filename }); 48 | } catch (err) { 49 | throw new Error(`${err}\n\nThe svelte component failed to be parsed.`); 50 | } 51 | 52 | if ( 53 | !pluginOptions.useAsDefaultScoping && 54 | !hasModuleAttribute(ast) && 55 | !hasModuleImports(content) 56 | ) { 57 | return { code: content }; 58 | } 59 | 60 | // eslint-disable-next-line prefer-const 61 | let { mode, hashSeeder } = pluginOptions; 62 | 63 | if (pluginOptions.parseStyleTag && hasModuleAttribute(ast)) { 64 | const moduleAttribute = ast.css?.attributes.find((item) => item.name === 'module'); 65 | mode = moduleAttribute.value !== true ? moduleAttribute.value[0].data : mode; 66 | } 67 | 68 | if (!['native', 'mixed', 'scoped'].includes(mode)) { 69 | throw new Error(`Module only accepts 'native', 'mixed' or 'scoped': '${mode}' was passed.`); 70 | } 71 | 72 | hashSeeder.forEach((value) => { 73 | if (!['style', 'filepath', 'classname'].includes(value)) { 74 | throw new Error( 75 | `The hash seeder only accepts the keys 'style', 'filepath' and 'classname': '${value}' was passed.` 76 | ); 77 | } 78 | }); 79 | 80 | let processor = nativeProcessor; 81 | 82 | if (mode === 'mixed') { 83 | processor = mixedProcessor; 84 | } else if (mode === 'scoped') { 85 | processor = scopedProcessor; 86 | } 87 | 88 | const parsedContent = await processor(ast, content, filename, pluginOptions); 89 | return { code: parsedContent }; 90 | }; 91 | 92 | /** 93 | * css Modules 94 | * @param options 95 | * @returns the css modules preprocessors 96 | */ 97 | const cssModulesPreprocessor = (options: Partial = {}): PreprocessorGroup => { 98 | pluginOptions = { 99 | ...defaultOptions(), 100 | ...options, 101 | }; 102 | 103 | if (pluginOptions.includePaths) { 104 | pluginOptions.includePaths = normalizeIncludePaths(pluginOptions.includePaths); 105 | } 106 | 107 | return { 108 | markup, 109 | }; 110 | }; 111 | 112 | // export default cssModulesPreprocessor; 113 | export default exports = module.exports = cssModulesPreprocessor; 114 | export const cssModules = cssModulesPreprocessor; 115 | 116 | // const cssModulesPreprocessor: any = module.exports = cssModules; 117 | // cssModulesPreprocessor.cssModules = cssModules; 118 | // export default module.exports = cssModules; 119 | -------------------------------------------------------------------------------- /src/lib/camelCase.ts: -------------------------------------------------------------------------------- 1 | const camelCase = (str: string): string => { 2 | const strings = str.split('-'); 3 | return strings.reduce((acc: string, val: string) => { 4 | return `${acc}${val[0].toUpperCase()}${val.slice(1)}`; 5 | }); 6 | }; 7 | 8 | export default camelCase; 9 | -------------------------------------------------------------------------------- /src/lib/generateName.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import getHashDigest from './getHashDijest'; 3 | import type { PluginOptions } from '../types'; 4 | 5 | const PATTERN_PATH_UNALLOWED = /[<>:"/\\|?*]/g; 6 | 7 | /** 8 | * interpolateName, adjusted version of loader-utils/interpolateName 9 | * @param resourcePath The file resourcePath 10 | * @param localName The local name/rules to replace 11 | * @param content The content to base the hash on 12 | */ 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | function interpolateName(resourcePath: string, localName: any, content: any) { 15 | const filename = localName || '[hash].[ext]'; 16 | 17 | let ext = 'svelte'; 18 | let basename = 'file'; 19 | let directory = ''; 20 | let folder = ''; 21 | 22 | const parsed = path.parse(resourcePath); 23 | let composedResourcePath = resourcePath; 24 | 25 | if (parsed.ext) { 26 | ext = parsed.ext.substr(1); 27 | } 28 | 29 | if (parsed.dir) { 30 | basename = parsed.name; 31 | composedResourcePath = parsed.dir + path.sep; 32 | } 33 | directory = composedResourcePath.replace(/\\/g, '/').replace(/\.\.(\/)?/g, '_$1'); 34 | 35 | if (directory.length === 1) { 36 | directory = ''; 37 | } else if (directory.length > 1) { 38 | folder = path.basename(directory); 39 | } 40 | 41 | let url = filename; 42 | 43 | if (content) { 44 | url = url.replace( 45 | /\[(?:([^:\]]+):)?(?:hash|contenthash)(?::([a-z]+\d*))?(?::(\d+))?\]/gi, 46 | (all: never, hashType: string, digestType: string, maxLength: never) => 47 | getHashDigest(content, hashType, digestType, parseInt(maxLength, 10)) 48 | ); 49 | } 50 | 51 | return url 52 | .replace(/\[ext\]/gi, () => ext) 53 | .replace(/\[name\]/gi, () => basename) 54 | .replace(/\[path\]/gi, () => directory) 55 | .replace(/\[folder\]/gi, () => folder); 56 | } 57 | 58 | /** 59 | * generateName 60 | * @param resourcePath The file resourcePath 61 | * @param style The style content 62 | * @param className The cssModules className 63 | * @param localIdentName The localIdentName rule 64 | */ 65 | export function generateName( 66 | resourcePath: string, 67 | style: string, 68 | className: string, 69 | pluginOptions: Pick 70 | ): string { 71 | const filePath = resourcePath; 72 | const localName = pluginOptions.localIdentName.length 73 | ? pluginOptions.localIdentName.replace(/\[local\]/gi, () => className) 74 | : className; 75 | 76 | const hashSeeder = pluginOptions.hashSeeder 77 | .join('-') 78 | .replace(/style/gi, () => style) 79 | .replace(/filepath/gi, () => filePath) 80 | .replace(/classname/gi, () => className); 81 | 82 | let interpolatedName = interpolateName(resourcePath, localName, hashSeeder).replace(/\./g, '-'); 83 | 84 | // replace unwanted characters from [path] 85 | if (PATTERN_PATH_UNALLOWED.test(interpolatedName)) { 86 | interpolatedName = interpolatedName.replace(PATTERN_PATH_UNALLOWED, '_'); 87 | } 88 | 89 | // prevent class error when the generated classname starts from a non word charater 90 | if (/^(?![a-zA-Z_])/.test(interpolatedName)) { 91 | interpolatedName = `_${interpolatedName}`; 92 | } 93 | 94 | // prevent svelte "Unused CSS selector" warning when the generated classname ends by `-` 95 | if (interpolatedName.slice(-1) === '-') { 96 | interpolatedName = interpolatedName.slice(0, -1); 97 | } 98 | 99 | return interpolatedName; 100 | } 101 | 102 | /** 103 | * Create the interpolated name 104 | * @param filename tthe resource filename 105 | * @param markup Markup content 106 | * @param style Stylesheet content 107 | * @param className the className 108 | * @param pluginOptions preprocess-cssmodules options 109 | * @return the interpolated name 110 | */ 111 | export function createClassName( 112 | filename: string, 113 | markup: string, 114 | style: string, 115 | className: string, 116 | pluginOptions: PluginOptions 117 | ): string { 118 | const interpolatedName = generateName(filename, style, className, pluginOptions); 119 | return pluginOptions.getLocalIdent( 120 | { 121 | context: path.dirname(filename), 122 | resourcePath: filename, 123 | }, 124 | { 125 | interpolatedName, 126 | template: pluginOptions.localIdentName, 127 | }, 128 | className, 129 | { 130 | markup, 131 | style, 132 | } 133 | ); 134 | } 135 | -------------------------------------------------------------------------------- /src/lib/getHashDijest.ts: -------------------------------------------------------------------------------- 1 | import Big from 'big.js'; 2 | import { createHash, type BinaryToTextEncoding } from 'crypto'; 3 | 4 | const baseEncodeTables = { 5 | 26: 'abcdefghijklmnopqrstuvwxyz', 6 | 32: '123456789abcdefghjkmnpqrstuvwxyz', // no 0lio 7 | 36: '0123456789abcdefghijklmnopqrstuvwxyz', 8 | 49: 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ', // no lIO 9 | 52: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 10 | 58: '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ', // no 0lIO 11 | 62: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 12 | 64: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_', 13 | }; 14 | 15 | /** 16 | * encodeBufferToBase, esm version of loader-utils/getHashDigest 17 | * @param buffer The memory buffer 18 | * @param base the enconding base 19 | */ 20 | const encodeBufferToBase = (buffer: Buffer, base: number): string => { 21 | const baseEncondingNumber = base as keyof typeof baseEncodeTables; 22 | const encodeTable = baseEncodeTables[baseEncondingNumber]; 23 | if (!encodeTable) { 24 | throw new Error(`Unknown encoding base${base}`); 25 | } 26 | 27 | const readLength = buffer.length; 28 | Big.DP = 0; 29 | Big.RM = Big.DP; 30 | let big = new Big(0); 31 | 32 | for (let i = readLength - 1; i >= 0; i -= 1) { 33 | big = big.times(256).plus(buffer[i]); 34 | } 35 | 36 | let output = ''; 37 | while (big.gt(0)) { 38 | const modulo = big.mod(base) as unknown as number; 39 | output = encodeTable[modulo] + output; 40 | big = big.div(base); 41 | } 42 | 43 | Big.DP = 20; 44 | Big.RM = 1; 45 | 46 | return output; 47 | }; 48 | 49 | /** 50 | * getHashDigest, esm version of loader-utils/getHashDigest 51 | * @param buffer The memory buffer 52 | * @param hashType The hashtype to use 53 | * @param digestType The encoding type to use 54 | */ 55 | const getHashDigest = ( 56 | buffer: Buffer, 57 | hashType: string, 58 | digestType: string, 59 | maxLength = 9999 60 | ): string => { 61 | const hash = createHash(hashType || 'md5'); 62 | 63 | hash.update(buffer); 64 | 65 | if ( 66 | digestType === 'base26' || 67 | digestType === 'base32' || 68 | digestType === 'base36' || 69 | digestType === 'base49' || 70 | digestType === 'base52' || 71 | digestType === 'base58' || 72 | digestType === 'base62' || 73 | digestType === 'base64' 74 | ) { 75 | return encodeBufferToBase(hash.digest(), parseInt(digestType.substring(4), 10)).substring( 76 | 0, 77 | maxLength 78 | ); 79 | } 80 | const encoding = (digestType as BinaryToTextEncoding) || 'hex'; 81 | return hash.digest(encoding).substring(0, maxLength); 82 | }; 83 | 84 | export default getHashDigest; 85 | -------------------------------------------------------------------------------- /src/lib/getLocalIdent.ts: -------------------------------------------------------------------------------- 1 | interface Context { 2 | context: string; 3 | resourcePath: string; 4 | } 5 | 6 | interface LocalIdentName { 7 | template: string; 8 | interpolatedName: string; 9 | } 10 | 11 | interface Options { 12 | markup: string; 13 | style: string; 14 | } 15 | 16 | export type GetLocalIdent = { 17 | (context: Context, localIdentName: LocalIdentName, localName: string, options: Options): string; 18 | }; 19 | 20 | // eslint-disable-next-line max-len 21 | export const getLocalIdent: GetLocalIdent = (_context, localIdentName) => 22 | localIdentName.interpolatedName; 23 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { default as camelCase } from './camelCase'; 2 | export * from './generateName'; 3 | export * from './getLocalIdent'; 4 | export * from './requirement'; 5 | -------------------------------------------------------------------------------- /src/lib/requirement.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import type { AST } from 'svelte/compiler'; 3 | 4 | /** 5 | * Normalize path by replacing potential backslashes to slashes 6 | * @param filepath The file path to normalize 7 | * @returns a path using forward slashes 8 | */ 9 | const normalizePath = (filepath: string): string => 10 | path.sep === '\\' ? filepath.replace(/\\/g, '/') : filepath; 11 | 12 | /** 13 | * Normalize all included paths 14 | * @param paths all paths to be normalized 15 | * @returns list of path using forward slashes 16 | */ 17 | export const normalizeIncludePaths = (paths: string[]): string[] => 18 | paths.map((includePath) => normalizePath(path.resolve(includePath))); 19 | 20 | /** 21 | * Check if a file requires processing 22 | * @param includePaths List of allowd paths 23 | * @param filename the current filename to compare with the paths 24 | * @returns The permission status 25 | */ 26 | export const isFileIncluded = (includePaths: string[], filename: string): boolean => { 27 | if (includePaths.length < 1) { 28 | return true; 29 | } 30 | 31 | return includePaths.some((includePath) => filename.startsWith(includePath)); 32 | }; 33 | 34 | /** 35 | * Check if a component is importing external module stylesheets 36 | * @param content The component content 37 | * @returns The status 38 | */ 39 | export const hasModuleImports = (content: string): boolean => { 40 | const pattern = /(? { 50 | const moduleAttribute = ast?.css?.attributes.find((item) => item.name === 'module'); 51 | return moduleAttribute !== undefined; 52 | }; 53 | -------------------------------------------------------------------------------- /src/parsers/importDeclaration.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | import path from 'path'; 3 | import fs, { constants } from 'fs'; 4 | import MagicString from 'magic-string'; 5 | import { parse, type AST } from 'svelte/compiler'; 6 | import { walk } from 'estree-walker'; 7 | import type { ImportDeclaration } from 'estree'; 8 | import type Processor from '../processors/processor'; 9 | 10 | /** 11 | * Parse CssModules Imports 12 | */ 13 | export default (processor: Processor): void => { 14 | if (!processor.ast.instance) { 15 | return; 16 | } 17 | 18 | const backup = { ...processor }; 19 | 20 | let importedContent = ''; 21 | 22 | walk(processor.ast.instance, { 23 | enter(baseNode) { 24 | (baseNode as AST.Script).content?.body.forEach((node) => { 25 | if ( 26 | node.type === 'ImportDeclaration' && 27 | String(node.source.value)?.search(/\.module\.s?css$/) !== -1 28 | ) { 29 | const nodeBody = node as ImportDeclaration & AST.BaseNode; 30 | const sourceValue = String(nodeBody.source.value); 31 | const absolutePath = path.resolve(path.dirname(processor.filename), sourceValue); 32 | const nodeModulesPath = path.resolve(`${path.resolve()}/node_modules`, sourceValue); 33 | 34 | try { 35 | processor.importedCssModuleList = {}; 36 | const fileContent = fs.readFileSync(absolutePath, 'utf8'); 37 | const fileStyle = `${processor.style.openTag}${fileContent}${processor.style.closeTag}`; 38 | 39 | let fileMagicContent = new MagicString(fileStyle); 40 | 41 | processor.ast = parse(fileStyle, { 42 | filename: absolutePath, 43 | modern: true, 44 | }); 45 | processor.magicContent = fileMagicContent; 46 | processor.cssKeyframeList = {}; 47 | processor.cssAnimationProperties = []; 48 | 49 | processor.styleParser(processor); 50 | 51 | fileMagicContent = processor.magicContent; 52 | processor.ast = backup.ast; 53 | processor.magicContent = backup.magicContent; 54 | processor.cssKeyframeList = backup.cssKeyframeList; 55 | processor.cssAnimationProperties = backup.cssAnimationProperties; 56 | 57 | if (nodeBody.specifiers.length === 0) { 58 | processor.magicContent.remove(nodeBody.start, nodeBody.end); 59 | } else if (nodeBody.specifiers[0].type === 'ImportDefaultSpecifier') { 60 | const specifiers = `const ${nodeBody.specifiers[0].local.name} = ${JSON.stringify( 61 | processor.importedCssModuleList 62 | )};`; 63 | processor.magicContent.overwrite(nodeBody.start, nodeBody.end, specifiers); 64 | } else { 65 | const specifierNames = nodeBody.specifiers.map((item) => { 66 | return item.local.name; 67 | }); 68 | const specifiers = `const { ${specifierNames.join(', ')} } = ${JSON.stringify( 69 | Object.fromEntries( 70 | Object.entries(processor.importedCssModuleList).filter(([key]) => 71 | specifierNames.includes(key) 72 | ) 73 | ) 74 | )};`; 75 | processor.magicContent.overwrite(nodeBody.start, nodeBody.end, specifiers); 76 | } 77 | 78 | const content = `\n${fileMagicContent 79 | .toString() 80 | .replace(processor.style.openTag, '') 81 | .replace(processor.style.closeTag, '')}`; 82 | 83 | if (processor.style.ast) { 84 | processor.magicContent.prependLeft(processor.style.ast.content.start, content); 85 | } else { 86 | importedContent += content; 87 | } 88 | } catch (err: any) { 89 | fs.access(nodeModulesPath, constants.F_OK, (error) => { 90 | if (error) { 91 | throw new Error(err); // not found in node_modules packages either, throw orignal error 92 | } 93 | }); 94 | } 95 | } 96 | }); 97 | }, 98 | }); 99 | 100 | if (importedContent) { 101 | processor.magicContent.append( 102 | `${processor.style.openTag}${importedContent}${processor.style.closeTag}` 103 | ); 104 | } 105 | }; 106 | -------------------------------------------------------------------------------- /src/parsers/index.ts: -------------------------------------------------------------------------------- 1 | export { default as parseImportDeclaration } from './importDeclaration'; 2 | export { default as parseTemplate } from './template'; 3 | -------------------------------------------------------------------------------- /src/parsers/template.ts: -------------------------------------------------------------------------------- 1 | import { walk } from 'estree-walker'; 2 | import type { AST } from 'svelte/compiler'; 3 | import type Processor from '../processors/processor'; 4 | 5 | interface CssVariables { 6 | styleAttribute: string; 7 | values: string; 8 | } 9 | 10 | /** 11 | * Update a string of multiple Classes 12 | * @param processor The CSS Module Processor 13 | * @param classNames The attribute value containing one or multiple classes 14 | * @returns the CSS Modules classnames 15 | */ 16 | const updateMultipleClasses = (processor: Processor, classNames: string): string => { 17 | const classes: string[] = classNames.split(' '); 18 | const generatedClassNames: string = classes.reduce((accumulator, currentValue, currentIndex) => { 19 | let value: string = currentValue; 20 | const rawValue: string = value.trim(); 21 | if (rawValue in processor.cssModuleList) { 22 | value = value.replace(rawValue, processor.cssModuleList[rawValue]); 23 | } 24 | if (currentIndex < classes.length - 1) { 25 | value += ' '; 26 | } 27 | return `${accumulator}${value}`; 28 | }, ''); 29 | 30 | return generatedClassNames; 31 | }; 32 | 33 | /** 34 | * Parse and update literal expression element 35 | * @param processor: The CSS Module Processor 36 | * @param expression The expression node 37 | */ 38 | const parseLiteralExpression = ( 39 | processor: Processor, 40 | expression: AST.ExpressionTag['expression'] | null 41 | ): void => { 42 | const exp = expression as typeof expression & AST.BaseNode; 43 | if (exp.type === 'Literal' && typeof exp.value === 'string') { 44 | const generatedClassNames = updateMultipleClasses(processor, exp.value); 45 | processor.magicContent.overwrite(exp.start, exp.end, `'${generatedClassNames}'`); 46 | } 47 | }; 48 | 49 | /** 50 | * Parse and update conditional expression 51 | * @param processor: The CSS Module Processor 52 | * @param expression The expression node 53 | */ 54 | const parseConditionalExpression = ( 55 | processor: Processor, 56 | expression: AST.ExpressionTag['expression'] 57 | ): void => { 58 | if (expression.type === 'ConditionalExpression') { 59 | const { consequent, alternate } = expression; 60 | parseLiteralExpression(processor, consequent); 61 | parseLiteralExpression(processor, alternate); 62 | } 63 | }; 64 | 65 | /** 66 | * Parse and update object expression 67 | * @param processor: The CSS Module Processor 68 | * @param expression The expression node 69 | */ 70 | const parseObjectExpression = ( 71 | processor: Processor, 72 | expression: AST.ExpressionTag['expression'] 73 | ): void => { 74 | if (expression.type === 'ObjectExpression') { 75 | expression?.properties.forEach((property) => { 76 | if (property.type === 'Property') { 77 | const key = property.key as (typeof property)['key'] & AST.BaseNode; 78 | 79 | if (property.shorthand) { 80 | if (key.type === 'Identifier') { 81 | processor.magicContent.overwrite( 82 | key.start, 83 | key.end, 84 | `'${processor.cssModuleList[key.name]}': ${key.name}` 85 | ); 86 | } 87 | } else if (key.type === 'Identifier') { 88 | processor.magicContent.overwrite( 89 | key.start, 90 | key.end, 91 | `'${processor.cssModuleList[key.name]}'` 92 | ); 93 | } else if (key.type !== 'PrivateIdentifier') { 94 | parseLiteralExpression(processor, key); 95 | } 96 | } 97 | }); 98 | } 99 | }; 100 | /** 101 | * Parse and update array expression 102 | * @param processor: The CSS Module Processor 103 | * @param expression The expression node 104 | */ 105 | const parseArrayExpression = ( 106 | processor: Processor, 107 | expression: AST.ExpressionTag['expression'] 108 | ): void => { 109 | if (expression.type === 'ArrayExpression') { 110 | expression.elements.forEach((el) => { 111 | if (el?.type === 'LogicalExpression') { 112 | parseLiteralExpression(processor, el.right); 113 | } else if (el?.type !== 'SpreadElement') { 114 | parseLiteralExpression(processor, el); 115 | } 116 | }); 117 | } 118 | }; 119 | 120 | /** 121 | * Add the dynamic variables to elements 122 | * @param processor The CSS Module Processor 123 | * @param node the node element 124 | * @param cssVar the cssVariables data 125 | */ 126 | const addDynamicVariablesToElements = ( 127 | processor: Processor, 128 | fragment: AST.Fragment, 129 | cssVar: CssVariables 130 | ): void => { 131 | fragment.nodes?.forEach((childNode) => { 132 | if (childNode.type === 'Component' || childNode.type === 'KeyBlock') { 133 | addDynamicVariablesToElements(processor, childNode.fragment, cssVar); 134 | } else if (childNode.type === 'EachBlock') { 135 | addDynamicVariablesToElements(processor, childNode.body, cssVar); 136 | if (childNode.fallback) { 137 | addDynamicVariablesToElements(processor, childNode.fallback, cssVar); 138 | } 139 | } else if (childNode.type === 'SnippetBlock') { 140 | addDynamicVariablesToElements(processor, childNode.body, cssVar); 141 | } else if (childNode.type === 'RegularElement') { 142 | const attributesLength = childNode.attributes.length; 143 | if (attributesLength) { 144 | const styleAttr = childNode.attributes.find( 145 | (attr) => attr.type !== 'SpreadAttribute' && attr.name === 'style' 146 | ) as AST.Attribute; 147 | if (styleAttr && Array.isArray(styleAttr.value)) { 148 | processor.magicContent.appendLeft(styleAttr.value[0].start, cssVar.values); 149 | } else { 150 | const lastAttr = childNode.attributes[attributesLength - 1]; 151 | processor.magicContent.appendRight(lastAttr.end, ` ${cssVar.styleAttribute}`); 152 | } 153 | } else { 154 | processor.magicContent.appendRight( 155 | childNode.start + childNode.name.length + 1, 156 | ` ${cssVar.styleAttribute}` 157 | ); 158 | } 159 | } else if (childNode.type === 'IfBlock') { 160 | addDynamicVariablesToElements(processor, childNode.consequent, cssVar); 161 | if (childNode.alternate) { 162 | addDynamicVariablesToElements(processor, childNode.alternate, cssVar); 163 | } 164 | } else if (childNode.type === 'AwaitBlock') { 165 | if (childNode.pending) { 166 | addDynamicVariablesToElements(processor, childNode.pending, cssVar); 167 | } 168 | if (childNode.then) { 169 | addDynamicVariablesToElements(processor, childNode.then, cssVar); 170 | } 171 | if (childNode.catch) { 172 | addDynamicVariablesToElements(processor, childNode.catch, cssVar); 173 | } 174 | } 175 | }); 176 | }; 177 | 178 | /** 179 | * Get the formatted css variables values 180 | * @param processor: The CSS Module Processor 181 | * @returns the values and the style attribute; 182 | */ 183 | const cssVariables = (processor: Processor): CssVariables => { 184 | const cssVarListKeys = Object.keys(processor.cssVarList); 185 | let styleAttribute = ''; 186 | let values = ''; 187 | 188 | if (cssVarListKeys.length) { 189 | for (let i = 0; i < cssVarListKeys.length; i += 1) { 190 | const key = cssVarListKeys[i]; 191 | values += `--${processor.cssVarList[key]}:{${key}};`; 192 | } 193 | styleAttribute = `style="${values}"`; 194 | } 195 | 196 | return { styleAttribute, values }; 197 | }; 198 | 199 | /** 200 | * Parse the template markup to update the class attributes with CSS modules 201 | * @param processor The CSS Module Processor 202 | */ 203 | export default (processor: Processor): void => { 204 | const directiveLength: number = 'class:'.length; 205 | const allowedAttributes = ['class', ...processor.options.includeAttributes]; 206 | 207 | const cssVar = cssVariables(processor); 208 | let dynamicVariablesAdded = false; 209 | 210 | walk(processor.ast.fragment, { 211 | enter(baseNode) { 212 | const node = baseNode as AST.Fragment | AST.Fragment['nodes'][0]; 213 | 214 | // css variables on parent elements 215 | if (node.type === 'Fragment' && cssVar.values.length && !dynamicVariablesAdded) { 216 | dynamicVariablesAdded = true; 217 | addDynamicVariablesToElements(processor, node, cssVar); 218 | } 219 | 220 | if ( 221 | ['RegularElement', 'Component'].includes(node.type) && 222 | (node as AST.Component | AST.RegularElement).attributes.length > 0 223 | ) { 224 | (node as AST.Component | AST.RegularElement).attributes.forEach((item) => { 225 | if (item.type === 'Attribute' && allowedAttributes.includes(item.name)) { 226 | if (Array.isArray(item.value)) { 227 | item.value.forEach((classItem) => { 228 | if (classItem.type === 'Text' && classItem.data.length > 0) { 229 | const generatedClassNames = updateMultipleClasses(processor, classItem.data); 230 | processor.magicContent.overwrite( 231 | classItem.start, 232 | classItem.start + classItem.data.length, 233 | generatedClassNames 234 | ); 235 | } else if (classItem.type === 'ExpressionTag') { 236 | parseConditionalExpression(processor, classItem.expression); 237 | } 238 | }); 239 | } else if (typeof item.value === 'object' && item.value.type === 'ExpressionTag') { 240 | parseObjectExpression(processor, item.value.expression); 241 | parseArrayExpression(processor, item.value.expression); 242 | parseConditionalExpression(processor, item.value.expression); 243 | } 244 | } 245 | if (item.type === 'ClassDirective') { 246 | const classNames = item.name.split('.'); 247 | const name = classNames.length > 1 ? classNames[1] : classNames[0]; 248 | if (name in processor.cssModuleList) { 249 | const start = item.start + directiveLength; 250 | const end = start + item.name.length; 251 | if (item.expression.type === 'Identifier' && item.name === item.expression.name) { 252 | processor.magicContent.overwrite( 253 | start, 254 | end, 255 | `${processor.cssModuleList[name]}={${item.name}}` 256 | ); 257 | } else { 258 | processor.magicContent.overwrite(start, end, processor.cssModuleList[name]); 259 | } 260 | } 261 | } 262 | }); 263 | } 264 | }, 265 | }); 266 | }; 267 | -------------------------------------------------------------------------------- /src/processors/index.ts: -------------------------------------------------------------------------------- 1 | export { default as nativeProcessor } from './native'; 2 | export { default as mixedProcessor } from './mixed'; 3 | export { default as scopedProcessor } from './scoped'; 4 | -------------------------------------------------------------------------------- /src/processors/mixed.ts: -------------------------------------------------------------------------------- 1 | import { walk } from 'estree-walker'; 2 | import type { AST } from 'svelte/compiler'; 3 | import type { PluginOptions } from '../types'; 4 | import Processor from './processor'; 5 | 6 | type Boundaries = { start: number; end: number }; 7 | 8 | /** 9 | * Update the selector boundaries 10 | * @param boundaries The current boundaries 11 | * @param start the new boundary start value 12 | * @param end the new boundary end value 13 | * @returns the updated boundaries 14 | */ 15 | const updateSelectorBoundaries = ( 16 | boundaries: Boundaries[], 17 | start: number, 18 | end: number 19 | ): Boundaries[] => { 20 | const selectorBoundaries = boundaries; 21 | if (selectorBoundaries[selectorBoundaries.length - 1]?.end === start) { 22 | selectorBoundaries[selectorBoundaries.length - 1].end = end; 23 | } else { 24 | selectorBoundaries.push({ start, end }); 25 | } 26 | return selectorBoundaries; 27 | }; 28 | 29 | /** 30 | * The mixed style parser 31 | * @param processor The CSS Module Processor 32 | */ 33 | const parser = (processor: Processor): void => { 34 | if (!processor.ast.css) { 35 | return; 36 | } 37 | walk(processor.ast.css, { 38 | enter(baseNode) { 39 | (baseNode as AST.CSS.StyleSheet).children?.forEach((node) => { 40 | if (node.type === 'Atrule' && node.name === 'keyframes') { 41 | processor.parseKeyframes(node); 42 | this.skip(); 43 | } 44 | if (node.type === 'Rule') { 45 | node.prelude.children.forEach((child) => { 46 | child.children.forEach((grandChild) => { 47 | if (grandChild.type === 'RelativeSelector') { 48 | const classSelectors = grandChild.selectors.filter( 49 | (item) => item.type === 'ClassSelector' 50 | ); 51 | if (classSelectors.length > 0) { 52 | let selectorBoundaries: Array = []; 53 | let start = 0; 54 | let end = 0; 55 | 56 | grandChild.selectors.forEach((item, index) => { 57 | if (!item.start && start > 0) { 58 | selectorBoundaries = updateSelectorBoundaries(selectorBoundaries, start, end); 59 | start = 0; 60 | end = 0; 61 | } else { 62 | let hasPushed = false; 63 | if (end !== item.start) { 64 | start = item.start; 65 | end = item.end; 66 | } else { 67 | selectorBoundaries = updateSelectorBoundaries( 68 | selectorBoundaries, 69 | start, 70 | item.end 71 | ); 72 | hasPushed = true; 73 | start = 0; 74 | end = 0; 75 | } 76 | if ( 77 | hasPushed === false && 78 | grandChild.selectors && 79 | index === grandChild.selectors.length - 1 80 | ) { 81 | selectorBoundaries = updateSelectorBoundaries( 82 | selectorBoundaries, 83 | start, 84 | end 85 | ); 86 | } 87 | } 88 | }); 89 | 90 | selectorBoundaries.forEach((boundary) => { 91 | const hasClassSelector = classSelectors.filter( 92 | (item) => boundary.start <= item.start && boundary.end >= item.end 93 | ); 94 | if (hasClassSelector.length > 0) { 95 | processor.magicContent.appendLeft(boundary.start, ':global('); 96 | processor.magicContent.appendRight(boundary.end, ')'); 97 | } 98 | }); 99 | } 100 | 101 | grandChild.selectors.forEach((item) => { 102 | processor.parsePseudoLocalSelectors(item); 103 | processor.parseClassSelectors(item); 104 | }); 105 | } 106 | }); 107 | }); 108 | 109 | processor.parseBoundVariables(node.block); 110 | processor.storeAnimationProperties(node.block); 111 | } 112 | }); 113 | }, 114 | }); 115 | 116 | processor.overwriteAnimationProperties(); 117 | }; 118 | 119 | const mixedProcessor = async ( 120 | ast: AST.Root, 121 | content: string, 122 | filename: string, 123 | options: PluginOptions 124 | ): Promise => { 125 | const processor = new Processor(ast, content, filename, options, parser); 126 | const processedContent = processor.parse(); 127 | return processedContent; 128 | }; 129 | 130 | export default mixedProcessor; 131 | -------------------------------------------------------------------------------- /src/processors/native.ts: -------------------------------------------------------------------------------- 1 | import { walk } from 'estree-walker'; 2 | import type { AST } from 'svelte/compiler'; 3 | import type { PluginOptions } from '../types'; 4 | import Processor from './processor'; 5 | 6 | type Boundaries = { start: number; end: number }; 7 | 8 | /** 9 | * Update the selector boundaries 10 | * @param boundaries The current boundaries 11 | * @param start the new boundary start value 12 | * @param end the new boundary end value 13 | * @returns the updated boundaries 14 | */ 15 | const updateSelectorBoundaries = ( 16 | boundaries: Boundaries[], 17 | start: number, 18 | end: number 19 | ): Boundaries[] => { 20 | const selectorBoundaries = boundaries; 21 | const lastIndex = selectorBoundaries.length - 1; 22 | if (selectorBoundaries[lastIndex]?.end === start) { 23 | selectorBoundaries[lastIndex].end = end; 24 | } else if (selectorBoundaries.length < 1 || selectorBoundaries[lastIndex].end < end) { 25 | selectorBoundaries.push({ start, end }); 26 | } 27 | return selectorBoundaries; 28 | }; 29 | 30 | /** 31 | * The native style parser 32 | * @param processor The CSS Module Processor 33 | */ 34 | const parser = (processor: Processor): void => { 35 | if (!processor.ast.css) { 36 | return; 37 | } 38 | 39 | let selectorBoundaries: Boundaries[] = []; 40 | 41 | walk(processor.ast.css, { 42 | enter(baseNode) { 43 | (baseNode as AST.CSS.StyleSheet).children?.forEach((node) => { 44 | if (node.type === 'Atrule' && node.name === 'keyframes') { 45 | processor.parseKeyframes(node); 46 | this.skip(); 47 | } 48 | if (node.type === 'Rule') { 49 | node.prelude.children.forEach((child) => { 50 | if (child.type === 'ComplexSelector') { 51 | let start = 0; 52 | let end = 0; 53 | 54 | child.children.forEach((grandChild, index) => { 55 | let hasPushed = false; 56 | if (grandChild.type === 'RelativeSelector') { 57 | grandChild.selectors.forEach((item) => { 58 | if ( 59 | item.type === 'PseudoClassSelector' && 60 | (item.name === 'global' || item.name === 'local') 61 | ) { 62 | processor.parsePseudoLocalSelectors(item); 63 | if (start > 0 && end > 0) { 64 | selectorBoundaries = updateSelectorBoundaries( 65 | selectorBoundaries, 66 | start, 67 | end 68 | ); 69 | hasPushed = true; 70 | } 71 | start = item.end + 1; 72 | end = 0; 73 | } else if (item.start && item.end) { 74 | if (start === 0) { 75 | start = item.start; 76 | } 77 | end = item.end; 78 | processor.parseClassSelectors(item); 79 | } 80 | }); 81 | 82 | if ( 83 | hasPushed === false && 84 | child.children && 85 | index === child.children.length - 1 && 86 | end > 0 87 | ) { 88 | selectorBoundaries = updateSelectorBoundaries(selectorBoundaries, start, end); 89 | } 90 | } 91 | }); 92 | } 93 | }); 94 | 95 | processor.parseBoundVariables(node.block); 96 | processor.storeAnimationProperties(node.block); 97 | } 98 | }); 99 | }, 100 | }); 101 | 102 | processor.overwriteAnimationProperties(); 103 | 104 | selectorBoundaries.forEach((boundary) => { 105 | processor.magicContent.appendLeft(boundary.start, ':global('); 106 | processor.magicContent.appendRight(boundary.end, ')'); 107 | }); 108 | }; 109 | 110 | const nativeProcessor = async ( 111 | ast: AST.Root, 112 | content: string, 113 | filename: string, 114 | options: PluginOptions 115 | ): Promise => { 116 | const processor = new Processor(ast, content, filename, options, parser); 117 | const processedContent = processor.parse(); 118 | return processedContent; 119 | }; 120 | 121 | export default nativeProcessor; 122 | -------------------------------------------------------------------------------- /src/processors/processor.ts: -------------------------------------------------------------------------------- 1 | import MagicString from 'magic-string'; 2 | import type { AST } from 'svelte/compiler'; 3 | import { CSSModuleList, PluginOptions } from '../types'; 4 | import { 5 | camelCase, 6 | createClassName, 7 | generateName, 8 | hasModuleAttribute, 9 | hasModuleImports, 10 | } from '../lib'; 11 | import { parseImportDeclaration, parseTemplate } from '../parsers'; 12 | 13 | export default class Processor { 14 | public filename: string; 15 | public options: PluginOptions; 16 | public rawContent: string; 17 | public cssModuleList: CSSModuleList = {}; 18 | public cssVarList: CSSModuleList = {}; 19 | public cssKeyframeList: CSSModuleList = {}; 20 | public cssAnimationProperties: AST.CSS.Declaration[] = []; 21 | public importedCssModuleList: CSSModuleList = {}; 22 | 23 | public ast: AST.Root; 24 | public style: { 25 | ast?: AST.Root['css']; 26 | openTag: string; 27 | closeTag: string; 28 | }; 29 | 30 | public magicContent: MagicString; 31 | 32 | public styleParser: (param: Processor) => void; 33 | public isParsingImports = false; 34 | 35 | constructor( 36 | ast: AST.Root, 37 | content: string, 38 | filename: string, 39 | options: PluginOptions, 40 | parser: (param: Processor) => void 41 | ) { 42 | this.filename = filename; 43 | this.options = options; 44 | this.rawContent = content; 45 | this.ast = ast; 46 | this.magicContent = new MagicString(content); 47 | this.styleParser = parser.bind(this); 48 | 49 | this.style = { 50 | ast: ast.css, 51 | openTag: ast.css ? content.substring(ast.css.start, ast.css.content.start) : '', 53 | }; 54 | } 55 | 56 | /** 57 | * Create CssModule classname 58 | * @param name The raw classname 59 | * @returns The generated module classname 60 | */ 61 | public createModuleClassname = (name: string): string => { 62 | const generatedClassName = createClassName( 63 | this.filename, 64 | this.rawContent, 65 | this.ast.css?.content.styles ?? '', 66 | name, 67 | this.options 68 | ); 69 | 70 | return generatedClassName; 71 | }; 72 | 73 | /** 74 | * Add CssModule data to list 75 | * @param name The raw classname 76 | * @param value The generated module classname 77 | */ 78 | public addModule = (name: string, value: string): void => { 79 | if (this.isParsingImports) { 80 | this.importedCssModuleList[camelCase(name)] = value; 81 | } 82 | this.cssModuleList[name] = value; 83 | }; 84 | 85 | /** 86 | * Parse component 87 | * @returns The CssModule updated component 88 | */ 89 | public parse = (): string => { 90 | if ( 91 | this.options.parseStyleTag && 92 | (hasModuleAttribute(this.ast) || (this.options.useAsDefaultScoping && this.ast.css)) 93 | ) { 94 | this.isParsingImports = false; 95 | this.styleParser(this); 96 | } 97 | 98 | if (this.options.parseExternalStylesheet && hasModuleImports(this.rawContent)) { 99 | this.isParsingImports = true; 100 | parseImportDeclaration(this); 101 | } 102 | 103 | if (Object.keys(this.cssModuleList).length > 0 || Object.keys(this.cssVarList).length > 0) { 104 | parseTemplate(this); 105 | } 106 | 107 | return this.magicContent.toString(); 108 | }; 109 | 110 | /** 111 | * Parse css dynamic variables bound to js bind() 112 | * @param node The ast "Selector" node to parse 113 | */ 114 | public parseBoundVariables = (node: AST.CSS.Block): void => { 115 | const bindedVariableNodes = (node.children.filter( 116 | (item) => item.type === 'Declaration' && item.value.includes('bind(') 117 | ) ?? []) as AST.CSS.Declaration[]; 118 | 119 | if (bindedVariableNodes.length > 0) { 120 | bindedVariableNodes.forEach((item) => { 121 | const name = item.value.replace(/'|"|bind\(|\)/g, ''); 122 | const varName = name.replace(/\./, '-'); 123 | 124 | const generatedVarName = generateName( 125 | this.filename, 126 | this.ast.css?.content.styles ?? '', 127 | varName, 128 | { 129 | hashSeeder: ['style', 'filepath'], 130 | localIdentName: `[local]-${this.options.cssVariableHash}`, 131 | } 132 | ); 133 | const bindStart = item.end - item.value.length; 134 | this.magicContent.overwrite(bindStart, item.end, `var(--${generatedVarName})`); 135 | this.cssVarList[name] = generatedVarName; 136 | }); 137 | } 138 | }; 139 | 140 | /** 141 | * Parse keyframes 142 | * @param node The ast "Selector" node to parse 143 | */ 144 | public parseKeyframes = (node: AST.CSS.Atrule): void => { 145 | if (node.prelude.indexOf('-global-') === -1) { 146 | const animationName = this.createModuleClassname(node.prelude); 147 | if (node.block?.end) { 148 | this.magicContent.overwrite( 149 | node.start, 150 | node.block.start - 1, 151 | `@keyframes -global-${animationName}` 152 | ); 153 | this.cssKeyframeList[node.prelude] = animationName; 154 | } 155 | } 156 | }; 157 | 158 | /** 159 | * Parse pseudo selector :local() 160 | * @param node The ast "Selector" node to parse 161 | */ 162 | public parseClassSelectors = (node: AST.CSS.SimpleSelector): void => { 163 | if (node.type === 'ClassSelector') { 164 | const generatedClassName = this.createModuleClassname(node.name); 165 | this.addModule(node.name, generatedClassName); 166 | this.magicContent.overwrite(node.start, node.end, `.${generatedClassName}`); 167 | } 168 | }; 169 | 170 | /** 171 | * Parse pseudo selector :local() 172 | * @param node The ast "Selector" node to parse 173 | */ 174 | public parsePseudoLocalSelectors = (node: AST.CSS.SimpleSelector): void => { 175 | if (node.type === 'PseudoClassSelector' && node.name === 'local') { 176 | this.magicContent.remove(node.start, node.start + `:local(`.length); 177 | this.magicContent.remove(node.end - 1, node.end); 178 | } 179 | }; 180 | 181 | /** 182 | * Store animation properties 183 | * @param node The ast "Selector" node to parse 184 | */ 185 | public storeAnimationProperties = (node: AST.CSS.Block): void => { 186 | const animationNodes = (node.children.filter( 187 | (item) => 188 | item.type === 'Declaration' && ['animation', 'animation-name'].includes(item.property) 189 | ) ?? []) as AST.CSS.Declaration[]; 190 | 191 | if (animationNodes.length > 0) { 192 | this.cssAnimationProperties.push(...animationNodes); 193 | } 194 | }; 195 | 196 | /** 197 | * Overwrite animation properties 198 | * apply module when required 199 | */ 200 | public overwriteAnimationProperties = (): void => { 201 | this.cssAnimationProperties.forEach((item) => { 202 | Object.keys(this.cssKeyframeList).forEach((key) => { 203 | const index = item.value.indexOf(key); 204 | if (index > -1) { 205 | const keyStart = item.end - item.value.length + index; 206 | const keyEnd = keyStart + key.length; 207 | this.magicContent.overwrite(keyStart, keyEnd, this.cssKeyframeList[key]); 208 | } 209 | }); 210 | }); 211 | }; 212 | } 213 | -------------------------------------------------------------------------------- /src/processors/scoped.ts: -------------------------------------------------------------------------------- 1 | import { walk } from 'estree-walker'; 2 | import type { AST } from 'svelte/compiler'; 3 | import type { PluginOptions } from '../types'; 4 | import Processor from './processor'; 5 | 6 | /** 7 | * The scoped style parser 8 | * @param processor The CSS Module Processor 9 | */ 10 | const parser = (processor: Processor): void => { 11 | if (!processor.ast.css) { 12 | return; 13 | } 14 | walk(processor.ast.css, { 15 | enter(baseNode) { 16 | (baseNode as AST.CSS.StyleSheet).children?.forEach((node) => { 17 | if (node.type === 'Rule') { 18 | node.prelude.children.forEach((child) => { 19 | child.children.forEach((grandChild) => { 20 | if (grandChild.type === 'RelativeSelector') { 21 | grandChild.selectors.forEach((item) => { 22 | processor.parsePseudoLocalSelectors(item); 23 | processor.parseClassSelectors(item); 24 | }); 25 | } 26 | }); 27 | }); 28 | 29 | processor.parseBoundVariables(node.block); 30 | } 31 | }); 32 | }, 33 | }); 34 | }; 35 | 36 | const scopedProcessor = async ( 37 | ast: AST.Root, 38 | content: string, 39 | filename: string, 40 | options: PluginOptions 41 | ): Promise => { 42 | const processor = new Processor(ast, content, filename, options, parser); 43 | const processedContent = processor.parse(); 44 | return processedContent; 45 | }; 46 | 47 | export default scopedProcessor; 48 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { GetLocalIdent } from '../lib'; 2 | 3 | export type PluginOptions = { 4 | cssVariableHash: string; 5 | getLocalIdent: GetLocalIdent; 6 | hashSeeder: Array<'style' | 'filepath' | 'classname'>; 7 | includeAttributes: string[]; 8 | includePaths: string[]; 9 | localIdentName: string; 10 | mode: 'native' | 'mixed' | 'scoped'; 11 | parseExternalStylesheet: boolean; 12 | parseStyleTag: boolean; 13 | useAsDefaultScoping: boolean; 14 | }; 15 | 16 | export type CSSModuleList = Record; 17 | export type CSSModuleDirectory = Record; 18 | -------------------------------------------------------------------------------- /tasks/parser.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow */ 2 | import { readdir, lstat, readFile, existsSync, writeFile } from 'fs'; 3 | import { resolve, dirname } from 'path'; 4 | import { Parser } from 'acorn'; 5 | import { walk } from 'estree-walker'; 6 | import MagicString from 'magic-string'; 7 | 8 | const parseDir = (dir) => { 9 | readdir(dir, (err, children) => { 10 | if (err) return 11 | children.forEach((child) => { 12 | const pathname = `${dir}/${child}`; 13 | lstat(pathname, (err, stats) => { 14 | if (err) return 15 | if (stats.isDirectory()) { 16 | parseDir(pathname); 17 | } 18 | if (stats.isFile()) { 19 | readFile(pathname, 'utf-8', (err, content) => { 20 | if (err) return 21 | const ast = Parser.parse(content, { 22 | ecmaVersion: 'latest', 23 | sourceType: 'module' 24 | }); 25 | const magicContent = new MagicString(content); 26 | walk(ast, { 27 | enter(node) { 28 | if (['ImportDeclaration', 'ExportNamedDeclaration', 'ExportAllDeclaration'].includes(node.type) && node.source) { 29 | const filename = resolve(dirname(pathname), `${node.source.value}.js`); 30 | const dirIndex = resolve(dirname(pathname), `${node.source.value}/index.js`); 31 | if (existsSync(filename)) { 32 | magicContent.prependLeft(node.source.end - 1, '.mjs'); 33 | } else if (existsSync(dirIndex)) { 34 | magicContent.prependLeft(node.source.end - 1, '/index.mjs'); 35 | } 36 | } else if ( 37 | node.type === 'ExportDefaultDeclaration' 38 | && node.declaration.type === 'AssignmentExpression' 39 | && node.declaration.right.type === 'AssignmentExpression' 40 | && node.declaration.right.left.object.name === 'module' 41 | && node.declaration.right.left.property.name === 'exports' 42 | ) { 43 | magicContent.remove(node.declaration.left.start, node.declaration.right.right.start); 44 | } 45 | // } else if ( 46 | // node.type === 'ExportDefaultDeclaration' 47 | // && node.declaration?.left?.type === 'MemberExpression' 48 | // && node.declaration.left.object.name === 'module' 49 | // && node.declaration.left.property.name === 'exports' 50 | // ) { 51 | // magicContent.remove(node.declaration.left.start, node.declaration.right.start); 52 | // } 53 | } 54 | }); 55 | const mjsPathname = pathname.replace('/esm', '').replace('.js', '.mjs'); 56 | writeFile(mjsPathname, magicContent.toString(), (err) => { 57 | if (err) throw err; 58 | }); 59 | }); 60 | } 61 | }); 62 | }); 63 | }); 64 | } 65 | 66 | parseDir('./dist/esm'); 67 | -------------------------------------------------------------------------------- /test/assets/class.module.css: -------------------------------------------------------------------------------- 1 | .error { color:red } 2 | .success { color:green } -------------------------------------------------------------------------------- /test/assets/style.module.css: -------------------------------------------------------------------------------- 1 | section { padding:10px } 2 | .error { color:red } 3 | .success-message { color:green } -------------------------------------------------------------------------------- /test/compiler.js: -------------------------------------------------------------------------------- 1 | const svelte = require('svelte/compiler'); 2 | const cssModules = require('../'); 3 | 4 | module.exports = async ({ source }, options) => { 5 | const { code } = await svelte.preprocess(source, [cssModules(options)], { 6 | filename: 'test/App.svelte', 7 | }); 8 | 9 | return code; 10 | }; 11 | -------------------------------------------------------------------------------- /test/globalFixtures/bindVariable.test.js: -------------------------------------------------------------------------------- 1 | const compiler = require('../compiler.js'); 2 | 3 | const script = ""; 4 | 5 | describe('Bind variable to CSS', () => { 6 | test('root elements', async () => { 7 | const output = await compiler( 8 | { 9 | source: `${script}
blue
red
`, 10 | }, 11 | { 12 | cssVariableHash: '123', 13 | } 14 | ); 15 | 16 | expect(output).toBe( 17 | `${script}
blue
red
` 18 | ); 19 | }); 20 | 21 | test('root element with attributes', async () => { 22 | const output = await compiler( 23 | { 24 | source: `${script}
blue
`, 25 | }, 26 | { 27 | cssVariableHash: '123', 28 | localIdentName: '[local]-123', 29 | } 30 | ); 31 | 32 | expect(output).toBe( 33 | `${script}
blue
` 34 | ); 35 | }); 36 | 37 | test('root element with style attribute', async () => { 38 | const output = await compiler( 39 | { 40 | source: `${script}
blue
`, 41 | }, 42 | { 43 | cssVariableHash: '123', 44 | } 45 | ); 46 | 47 | expect(output).toBe( 48 | `${script}
blue
` 49 | ); 50 | }); 51 | 52 | test('element wrapped by a root component', async () => { 53 | const output = await compiler( 54 | { 55 | source: `${script}
blue
`, 56 | }, 57 | { 58 | cssVariableHash: '123', 59 | } 60 | ); 61 | 62 | expect(output).toBe( 63 | `${script}
blue
` 64 | ); 65 | }); 66 | 67 | test('deep nested element in components', async () => { 68 | const output = await compiler( 69 | { 70 | source: `${script} 71 |
blue
72 | 73 |
blue
74 | 75 | red 76 | green 77 | none 78 | 79 |
80 |
yellow blue
81 | `, 82 | }, 83 | { 84 | cssVariableHash: '123', 85 | mode: 'scoped', 86 | } 87 | ); 88 | 89 | expect(output).toBe( 90 | `${script} 91 |
blue
92 | 93 |
blue
94 | 95 | red 96 | green 97 | none 98 | 99 |
100 |
yellow blue
101 | ` 102 | ); 103 | }); 104 | 105 | test('root elements bound with js expression', async () => { 106 | const output = await compiler( 107 | { 108 | source: ` 109 |
black
110 | `, 116 | }, 117 | { 118 | cssVariableHash: '123', 119 | mode: 'scoped', 120 | } 121 | ); 122 | 123 | expect(output).toBe( 124 | ` 125 |
black
126 | ` 132 | ); 133 | }); 134 | 135 | test('root elements has if statement', async () => { 136 | const output = await compiler( 137 | { 138 | source: 139 | `${script}` + 140 | `{#if color === 'blue'}
blue
` + 141 | `{:else if color === 'red'}
red
` + 142 | `{:else}
none
` + 143 | `{/if}`, 144 | }, 145 | { 146 | cssVariableHash: '123', 147 | } 148 | ); 149 | 150 | expect(output).toBe( 151 | `${script}` + 152 | `{#if color === 'blue'}
blue
` + 153 | `{:else if color === 'red'}
red
` + 154 | `{:else}
none
` + 155 | `{/if}` 156 | ); 157 | }); 158 | 159 | test('root elements has `each` statement', async () => { 160 | const output = await compiler( 161 | { 162 | source: 163 | `${script}` + 164 | `{#each [0,1,2,3] as number}` + 165 | `
{number}
` + 166 | `{/each}`, 167 | }, 168 | { 169 | cssVariableHash: '123', 170 | } 171 | ); 172 | 173 | expect(output).toBe( 174 | `${script}` + 175 | `{#each [0,1,2,3] as number}` + 176 | `
{number}
` + 177 | `{/each}` 178 | ); 179 | }); 180 | 181 | test('root element has `each` statement', async () => { 182 | const output = await compiler( 183 | { 184 | source: 185 | `${script}` + 186 | `{#await promise}` + 187 | `

...waiting

` + 188 | `{:then number}` + 189 | `

The number is {number}

` + 190 | `{:catch error}` + 191 | `

{error.message}

` + 192 | `{/await}` + 193 | `{#await promise then value}` + 194 | `

the value is {value}

` + 195 | `{/await}`, 196 | }, 197 | { 198 | cssVariableHash: '123', 199 | } 200 | ); 201 | 202 | expect(output).toBe( 203 | `${script}` + 204 | `{#await promise}` + 205 | `

...waiting

` + 206 | `{:then number}` + 207 | `

The number is {number}

` + 208 | `{:catch error}` + 209 | `

{error.message}

` + 210 | `{/await}` + 211 | `{#await promise then value}` + 212 | `

the value is {value}

` + 213 | `{/await}` 214 | ); 215 | }); 216 | 217 | test('root element has `key` statement', async () => { 218 | const output = await compiler( 219 | { 220 | source: 221 | `${script}` + 222 | `{#key value}` + 223 | `
{value}
` + 224 | `{/key}`, 225 | }, 226 | { 227 | cssVariableHash: '123', 228 | } 229 | ); 230 | 231 | expect(output).toBe( 232 | `${script}` + 233 | `{#key value}` + 234 | `
{value}
` + 235 | `{/key}` 236 | ); 237 | }); 238 | }); 239 | -------------------------------------------------------------------------------- /test/globalFixtures/class.test.js: -------------------------------------------------------------------------------- 1 | const compiler = require('../compiler.js'); 2 | 3 | describe('Class Attribute Object', () => { 4 | test('Shorthand', async () => { 5 | const output = await compiler( 6 | { 7 | source: `` 8 | + `btn`, 9 | }, 10 | { 11 | localIdentName: '[local]-123', 12 | } 13 | ); 14 | 15 | expect(output).toBe( 16 | `` 17 | + `btn`, 18 | ); 19 | }); 20 | test('Identifier key', async () => { 21 | const output = await compiler( 22 | { 23 | source: `` 24 | + `btn`, 25 | }, 26 | { 27 | localIdentName: '[local]-123', 28 | } 29 | ); 30 | 31 | expect(output).toBe( 32 | `` 33 | + `btn`, 34 | ); 35 | }); 36 | test('Literal key', async () => { 37 | const output = await compiler( 38 | { 39 | source: `` 40 | + `btn`, 41 | }, 42 | { 43 | localIdentName: '[local]-123', 44 | } 45 | ); 46 | 47 | expect(output).toBe( 48 | `` 49 | + `btn`, 50 | ); 51 | }); 52 | test('Multiple literal keys', async () => { 53 | const output = await compiler( 54 | { 55 | source: `` 56 | + `btn`, 57 | }, 58 | { 59 | localIdentName: '[local]-123', 60 | } 61 | ); 62 | 63 | expect(output).toBe( 64 | `` 65 | + `btn`, 66 | ); 67 | }); 68 | test('Multiple conditions', async () => { 69 | const output = await compiler( 70 | { 71 | source: `` 72 | + `btn`, 73 | }, 74 | { 75 | localIdentName: '[local]-123', 76 | } 77 | ); 78 | 79 | expect(output).toBe( 80 | `` 81 | + `btn`, 82 | ); 83 | }); 84 | }); 85 | 86 | describe('Class Attribute Array', () => { 87 | test('Thruthy value', async () => { 88 | const output = await compiler( 89 | { 90 | source: `` 91 | + `btn`, 92 | }, 93 | { 94 | localIdentName: '[local]-123', 95 | } 96 | ); 97 | 98 | expect(output).toBe( 99 | `` 100 | + `btn`, 101 | ); 102 | }); 103 | 104 | test('Combined thruty values', async () => { 105 | const output = await compiler( 106 | { 107 | source: `` 108 | + `btn`, 109 | }, 110 | { 111 | localIdentName: '[local]-123', 112 | } 113 | ); 114 | 115 | expect(output).toBe( 116 | `` 117 | + `btn`, 118 | ); 119 | }); 120 | 121 | test('Mixed condition', async () => { 122 | const output = await compiler( 123 | { 124 | source: `` 125 | + `btn`, 126 | }, 127 | { 128 | localIdentName: '[local]-123', 129 | } 130 | ); 131 | 132 | expect(output).toBe( 133 | `` 134 | + `btn`, 135 | ); 136 | }); 137 | 138 | test('has variables', async () => { 139 | const output = await compiler( 140 | { 141 | source: `` 142 | + `btn`, 143 | }, 144 | { 145 | localIdentName: '[local]-123', 146 | } 147 | ); 148 | 149 | expect(output).toBe( 150 | `` 151 | + `btn`, 152 | ); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /test/globalFixtures/keyframes.test.js: -------------------------------------------------------------------------------- 1 | const compiler = require('../compiler.js'); 2 | 3 | describe('Scoped Keyframes', () => { 4 | test('Mixed mode on tag selector', async () => { 5 | const source = 6 | '' + 10 | '

Title

'; 11 | 12 | const expectedOutput = 13 | '' + 17 | '

Title

'; 18 | 19 | const output = await compiler( 20 | { 21 | source, 22 | }, 23 | { 24 | mode: 'mixed', 25 | localIdentName: '[local]-123', 26 | } 27 | ); 28 | 29 | expect(output).toBe(expectedOutput); 30 | }); 31 | 32 | test('Mixed mode on tag selector with animation-name property', async () => { 33 | const source = 34 | '' + 38 | '

Title

'; 39 | 40 | const expectedOutput = 41 | '' + 45 | '

Title

'; 46 | 47 | const output = await compiler( 48 | { 49 | source, 50 | }, 51 | { 52 | mode: 'mixed', 53 | localIdentName: '[local]-123', 54 | } 55 | ); 56 | 57 | expect(output).toBe(expectedOutput); 58 | }); 59 | 60 | test('Native mode with multiple animation properties', async () => { 61 | const source = 62 | '' + 67 | 'Red'; 68 | 69 | const expectedOutput = 70 | '' + 75 | 'Red'; 76 | 77 | const output = await compiler( 78 | { 79 | source, 80 | }, 81 | { 82 | mode: 'native', 83 | localIdentName: '[local]-123', 84 | } 85 | ); 86 | 87 | expect(output).toBe(expectedOutput); 88 | }); 89 | 90 | test('Native move on non global keyframes only', async () => { 91 | const source = 92 | '' + 97 | 'Red'; 98 | 99 | const expectedOutput = 100 | '' + 105 | 'Red'; 106 | 107 | const output = await compiler( 108 | { 109 | source, 110 | }, 111 | { 112 | mode: 'native', 113 | localIdentName: '[local]-123', 114 | } 115 | ); 116 | 117 | expect(output).toBe(expectedOutput); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /test/globalFixtures/options.test.js: -------------------------------------------------------------------------------- 1 | const compiler = require('../compiler.js'); 2 | 3 | test('Customize generated classname from getLocalIdent', async () => { 4 | const output = await compiler( 5 | { 6 | source: 'Red', 7 | }, 8 | { 9 | localIdentName: '[local]-123456MC', 10 | getLocalIdent: (context, { interpolatedName }) => { 11 | return interpolatedName.toLowerCase(); 12 | }, 13 | } 14 | ); 15 | 16 | expect(output).toBe( 17 | 'Red' 18 | ); 19 | }); 20 | 21 | test('Do not process style without the module attribute', async () => { 22 | const output = await compiler( 23 | { 24 | source: 'Red', 25 | }, 26 | { 27 | localIdentName: '[local]-123', 28 | } 29 | ); 30 | 31 | expect(output).toBe('Red'); 32 | }); 33 | 34 | describe('When the mode option has an invalid value', () => { 35 | const source = ''; 36 | 37 | it('throws an exception', async () => { 38 | await expect(compiler({ source }, { mode: 'svelte' })).rejects.toThrow( 39 | `Module only accepts 'native', 'mixed' or 'scoped': 'svelte' was passed.` 40 | ); 41 | }); 42 | }); 43 | 44 | describe('When the module attribute has an invalid value', () => { 45 | const source = ''; 46 | 47 | it('throws an exception', async () => { 48 | await expect(compiler({ source })).rejects.toThrow( 49 | `Module only accepts 'native', 'mixed' or 'scoped': 'svelte' was passed.` 50 | ); 51 | }); 52 | }); 53 | 54 | test('Use the filepath only as hash seeder', async () => { 55 | const output = await compiler( 56 | { 57 | source: 58 | 'Red', 59 | }, 60 | { 61 | localIdentName: '[local]-[hash:6]', 62 | hashSeeder: ['filepath'], 63 | } 64 | ); 65 | 66 | expect(output).toBe( 67 | 'Red' 68 | ); 69 | }); 70 | 71 | describe('When the hashSeeder has a wrong key', () => { 72 | const source = ''; 73 | 74 | it('throws an exception', async () => { 75 | await expect( 76 | compiler( 77 | { 78 | source, 79 | }, 80 | { 81 | hashSeeder: ['filepath', 'content'], 82 | } 83 | ) 84 | ).rejects.toThrow( 85 | `The hash seeder only accepts the keys 'style', 'filepath' and 'classname': 'content' was passed.` 86 | ); 87 | }); 88 | }); 89 | 90 | describe('When the preprocessor is set as default scoping', () => { 91 | it('parses the style tag with no module attributes', async () => { 92 | const source = '

red

'; 93 | const output = await compiler( 94 | { 95 | source, 96 | }, 97 | { 98 | localIdentName: '[local]-123', 99 | useAsDefaultScoping: true, 100 | } 101 | ); 102 | 103 | expect(output).toBe( 104 | '

red

' 105 | ); 106 | }); 107 | 108 | it('parses the style tag with module attributes', async () => { 109 | const source = '

red

'; 110 | const output = await compiler( 111 | { 112 | source, 113 | }, 114 | { 115 | localIdentName: '[local]-123', 116 | useAsDefaultScoping: true, 117 | } 118 | ); 119 | 120 | expect(output).toBe( 121 | '

red

' 122 | ); 123 | }); 124 | 125 | it('does not parse when `parseStyleTag` is off', async () => { 126 | const source = '

red

'; 127 | const output = await compiler( 128 | { 129 | source, 130 | }, 131 | { 132 | localIdentName: '[local]-123', 133 | parseStyleTag: false, 134 | useAsDefaultScoping: true, 135 | } 136 | ); 137 | 138 | expect(output).toBe( 139 | '

red

' 140 | ); 141 | }); 142 | 143 | it('does not parse when the style tag does not exist', async () => { 144 | const source = '

red

'; 145 | const output = await compiler( 146 | { 147 | source, 148 | }, 149 | { 150 | useAsDefaultScoping: true, 151 | } 152 | ); 153 | 154 | expect(output).toBe('

red

'); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /test/globalFixtures/template.test.js: -------------------------------------------------------------------------------- 1 | const compiler = require('../compiler.js'); 2 | 3 | test('Replace multiline class attribute', async () => { 4 | const output = await compiler( 5 | { 6 | source: `btn`, 11 | }, 12 | { 13 | localIdentName: '[local]-123', 14 | } 15 | ); 16 | 17 | expect(output).toBe( 18 | `btn` 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /test/mixedFixtures/stylesAttribute.test.js: -------------------------------------------------------------------------------- 1 | const compiler = require('../compiler.js'); 2 | 3 | describe('Mixed Mode', () => { 4 | test('Chain Selector', async () => { 5 | const source = 6 | '\n' + 7 | 'Red'; 8 | 9 | const expectedOutput = 10 | '\n' + 11 | 'Red'; 12 | 13 | const output = await compiler( 14 | { 15 | source, 16 | }, 17 | { 18 | localIdentName: '[local]-123456', 19 | mode: 'mixed', 20 | } 21 | ); 22 | 23 | expect(output).toBe(expectedOutput); 24 | }); 25 | 26 | test('CSS Modules class targetting children', async () => { 27 | const source = 28 | '\n' + 32 | '
Red*
'; 33 | 34 | const expectedOutput = 35 | '\n' + 39 | '
Red*
'; 40 | 41 | const output = await compiler( 42 | { 43 | source, 44 | }, 45 | { 46 | localIdentName: '[local]-123', 47 | mode: 'mixed', 48 | } 49 | ); 50 | 51 | expect(output).toBe(expectedOutput); 52 | }); 53 | 54 | test('CSS Modules class has a parent', async () => { 55 | const source = 56 | '\n' + 61 | '
Red
'; 62 | 63 | const expectedOutput = 64 | '\n' + 69 | '
Red
'; 70 | 71 | const output = await compiler( 72 | { 73 | source, 74 | }, 75 | { 76 | localIdentName: '[local]-123', 77 | mode: 'mixed', 78 | } 79 | ); 80 | 81 | expect(output).toBe(expectedOutput); 82 | }); 83 | 84 | test('CSS Modules chaining pseudo selector', async () => { 85 | const source = 86 | '\n' + 91 | '
Red
'; 92 | 93 | const expectedOutput = 94 | '\n' + 99 | '
Red
'; 100 | 101 | const output = await compiler( 102 | { 103 | source, 104 | }, 105 | { 106 | localIdentName: '[local]-123', 107 | mode: 'mixed', 108 | } 109 | ); 110 | 111 | expect(output).toBe(expectedOutput); 112 | }); 113 | 114 | test('CSS Modules class is used within a media query', async () => { 115 | const source = 116 | '\n' + 122 | '
Red
'; 123 | 124 | const expectedOutput = 125 | '\n' + 131 | '
Red
'; 132 | 133 | const output = await compiler( 134 | { 135 | source, 136 | }, 137 | { 138 | localIdentName: '[local]-123', 139 | mode: 'mixed', 140 | } 141 | ); 142 | 143 | expect(output).toBe(expectedOutput); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /test/nativeFixtures/stylesAttribute.test.js: -------------------------------------------------------------------------------- 1 | const compiler = require('../compiler.js'); 2 | 3 | describe('Native Mode', () => { 4 | test('Generate CSS Modules and globalize all selectors', async () => { 5 | const source = 6 | '' + 12 | 'Red'; 13 | 14 | const expectedOutput = 15 | '' + 21 | 'Red'; 22 | 23 | const output = await compiler( 24 | { 25 | source, 26 | }, 27 | { 28 | localIdentName: '[local]-123', 29 | } 30 | ); 31 | 32 | expect(output).toBe(expectedOutput); 33 | }); 34 | 35 | test('Globalize non global selector only', async () => { 36 | const source = 37 | '' + 43 | 'Red'; 44 | 45 | const expectedOutput = 46 | '' + 52 | 'Red'; 53 | 54 | const output = await compiler( 55 | { 56 | source, 57 | }, 58 | { 59 | localIdentName: '[local]-123', 60 | } 61 | ); 62 | 63 | expect(output).toBe(expectedOutput); 64 | }); 65 | 66 | test('Scoped local selector', async () => { 67 | const source = 68 | '' + 75 | 'Red'; 76 | 77 | const expectedOutput = 78 | '' + 85 | 'Red'; 86 | 87 | const output = await compiler( 88 | { 89 | source, 90 | }, 91 | { 92 | mode: 'native', 93 | localIdentName: '[local]-123', 94 | } 95 | ); 96 | 97 | expect(output).toBe(expectedOutput); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /test/nativeFixtures/stylesImports.test.js: -------------------------------------------------------------------------------- 1 | const compiler = require('../compiler.js'); 2 | 3 | describe('Native Mode Imports', () => { 4 | test('Imports into existing `; 14 | 15 | const expectedOutput = 16 | ` 19 |
Error
20 |
Success
21 | `; 26 | 27 | const output = await compiler( 28 | { 29 | source, 30 | }, 31 | { 32 | mode: 'native', 33 | localIdentName: '[local]-123', 34 | parseExternalStylesheet: true, 35 | } 36 | ); 37 | 38 | expect(output).toBe(expectedOutput); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/scopedFixtures/stylesAttribute.test.js: -------------------------------------------------------------------------------- 1 | const compiler = require('../compiler.js'); 2 | 3 | const source = 'Red'; 4 | 5 | describe('Scoped Mode', () => { 6 | test('Generate CSS Modules from HTML attributes, Replace CSS className', async () => { 7 | const output = await compiler( 8 | { 9 | source, 10 | }, 11 | { 12 | localIdentName: '[local]-123', 13 | } 14 | ); 15 | 16 | expect(output).toBe( 17 | 'Red' 18 | ); 19 | }); 20 | 21 | test('Avoid generated class to start with a non character', async () => { 22 | const output = await compiler( 23 | { 24 | source, 25 | }, 26 | { 27 | localIdentName: '1[local]', 28 | } 29 | ); 30 | expect(output).toBe( 31 | 'Red' 32 | ); 33 | }); 34 | 35 | test('Avoid generated class to end with a hyphen', async () => { 36 | const output = await compiler( 37 | { 38 | source, 39 | }, 40 | { 41 | localIdentName: '[local]-', 42 | } 43 | ); 44 | expect(output).toBe( 45 | 'Red' 46 | ); 47 | }); 48 | 49 | test('Generate class with path token', async () => { 50 | const output = await compiler( 51 | { 52 | source, 53 | }, 54 | { 55 | localIdentName: '[path][name]__[local]', 56 | } 57 | ); 58 | expect(output).toBe( 59 | 'Red' 60 | ); 61 | }); 62 | 63 | test('Replace directive', async () => { 64 | const output = await compiler( 65 | { 66 | source: 67 | 'Red', 68 | }, 69 | { 70 | localIdentName: '[local]-123', 71 | } 72 | ); 73 | expect(output).toBe( 74 | 'Red' 75 | ); 76 | }); 77 | 78 | test('Replace short hand directive', async () => { 79 | const output = await compiler( 80 | { 81 | source: 82 | 'Red', 83 | }, 84 | { 85 | localIdentName: '[local]-123', 86 | } 87 | ); 88 | expect(output).toBe( 89 | 'Red' 90 | ); 91 | }); 92 | 93 | test('Replace multiple classnames on attribute', async () => { 94 | const output = await compiler( 95 | { 96 | source: 97 | 'Red', 98 | }, 99 | { 100 | localIdentName: '[local]-123', 101 | } 102 | ); 103 | expect(output).toBe( 104 | 'Red' 105 | ); 106 | }); 107 | 108 | test('Replace classnames on conditional expression', async () => { 109 | const output = await compiler( 110 | { 111 | source: `Red`, 112 | }, 113 | { 114 | localIdentName: '[local]-123', 115 | } 116 | ); 117 | expect(output).toBe( 118 | `Red` 119 | ); 120 | }); 121 | 122 | test('Replace classname on component', async () => { 123 | const output = await compiler( 124 | { 125 | source: `