├── .gitignore
├── .eslintignore
├── .prettierignore
├── example
├── rollup
│ ├── src
│ │ ├── global.d.ts
│ │ ├── main.ts
│ │ └── App.svelte
│ ├── .gitignore
│ ├── .vscode
│ │ └── extensions.json
│ ├── public
│ │ ├── favicon.png
│ │ ├── index.html
│ │ └── global.css
│ ├── tsconfig.json
│ ├── package.json
│ ├── rollup.config.js
│ └── README.md
├── svelte-kit
│ ├── src
│ │ ├── global.d.ts
│ │ ├── routes
│ │ │ └── index.svelte
│ │ └── app.html
│ ├── .gitignore
│ ├── static
│ │ └── favicon.png
│ ├── jsconfig.json
│ ├── package.json
│ ├── svelte.config.js
│ └── README.md
└── webpack
│ ├── app3.module.css
│ ├── app2.module.css
│ ├── main.js
│ ├── app.module.css
│ ├── dist
│ └── index.html
│ ├── components
│ ├── Body.svelte
│ └── Time.svelte
│ ├── package.json
│ ├── webpack.config.js
│ └── App.svelte
├── test
├── assets
│ ├── class.module.css
│ └── style.module.css
├── compiler.js
├── globalFixtures
│ ├── template.test.js
│ ├── keyframes.test.js
│ ├── options.test.js
│ ├── class.test.js
│ └── bindVariable.test.js
├── nativeFixtures
│ ├── stylesImports.test.js
│ └── stylesAttribute.test.js
├── mixedFixtures
│ └── stylesAttribute.test.js
└── scopedFixtures
│ ├── stylesImports.test.js
│ └── stylesAttribute.test.js
├── src
├── parsers
│ ├── index.ts
│ ├── importDeclaration.ts
│ └── template.ts
├── lib
│ ├── index.ts
│ ├── camelCase.ts
│ ├── getLocalIdent.ts
│ ├── requirement.ts
│ ├── getHashDijest.ts
│ └── generateName.ts
├── processors
│ ├── index.ts
│ ├── scoped.ts
│ ├── native.ts
│ ├── mixed.ts
│ └── processor.ts
├── types
│ └── index.ts
└── index.ts
├── .prettierrc.json
├── .editorconfig
├── tsconfig.json
├── .eslintrc.js
├── LICENSE
├── package.json
├── tasks
└── parser.mjs
├── CHANGELOG.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .DS_Store
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | test/
3 | example/
4 | dist/
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | example/
3 | dist/
4 | *.css
--------------------------------------------------------------------------------
/example/rollup/src/global.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/example/svelte-kit/src/global.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/example/webpack/app3.module.css:
--------------------------------------------------------------------------------
1 | header {
2 | background-color: #f8f8f8;
3 | }
--------------------------------------------------------------------------------
/test/assets/class.module.css:
--------------------------------------------------------------------------------
1 | .error { color:red }
2 | .success { color:green }
--------------------------------------------------------------------------------
/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/svelte-kit/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 |
--------------------------------------------------------------------------------
/test/assets/style.module.css:
--------------------------------------------------------------------------------
1 | section { padding:10px }
2 | .error { color:red }
3 | .success-message { color:green }
--------------------------------------------------------------------------------
/example/rollup/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/micantoine/svelte-preprocess-cssmodules/HEAD/example/rollup/public/favicon.png
--------------------------------------------------------------------------------
/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/svelte-kit/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/micantoine/svelte-preprocess-cssmodules/HEAD/example/svelte-kit/static/favicon.png
--------------------------------------------------------------------------------
/src/parsers/index.ts:
--------------------------------------------------------------------------------
1 | export { default as parseImportDeclaration } from './importDeclaration';
2 | export { default as parseTemplate } from './template';
3 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "bracketSpacing": true,
3 | "parser": "typescript",
4 | "singleQuote": true,
5 | "semi": true,
6 | "tabWidth": 2,
7 | "trailingComma": "es5"
8 | }
--------------------------------------------------------------------------------
/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | export { default as camelCase } from './camelCase';
2 | export * from './generateName';
3 | export * from './getLocalIdent';
4 | export * from './requirement';
5 |
--------------------------------------------------------------------------------
/example/rollup/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/svelte/tsconfig.json",
3 |
4 | "include": ["src/**/*"],
5 | "exclude": ["node_modules/*", "__sapper__/*", "public/*"]
6 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Svelte CSS modules loader
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/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/src/routes/index.svelte:
--------------------------------------------------------------------------------
1 | Welcome to SvelteKit
2 | Visit kit.svelte.dev to read the documentation
3 |
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/webpack/components/Body.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | {@render children?.()}
7 |
8 |
9 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "allowSyntheticDefaultImports": true,
5 | "esModuleInterop": true,
6 | "noImplicitAny": true,
7 | "moduleResolution": "node",
8 | "removeComments": true,
9 | "sourceMap": false,
10 | "strict": true,
11 | "baseUrl": ".",
12 | "skipLibCheck": true,
13 | "resolveJsonModule": true,
14 | "lib": [
15 | "esnext"
16 | ],
17 | "typeRoots": [
18 | "./src/types",
19 | "./node_modules/@types",
20 | ],
21 | "types": [
22 | "node"
23 | ],
24 | },
25 | "include": [
26 | "src/**/*.ts"
27 | ],
28 | "exclude": [
29 | "node_modules",
30 | "dist",
31 | "tasks"
32 | ]
33 | }
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 micantoine
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/example/webpack/components/Time.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 | {time}
27 |
28 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/example/webpack/App.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 | theme.color = 'red'}
20 | on:keyup={() => theme.color = 'green'}
21 | >
22 | My Modal title
23 |
24 |
25 |
26 | Lorem ipsum dolor sit , amet consectetur adipisicing elit. Placeat, deserunt.
27 | Lorem ipsum dolor sit, amet consectetur adipisicing elit. Placeat, deserunt. Lorem ipsum dolor sit amet.
28 |
29 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/scopedFixtures/stylesImports.test.js:
--------------------------------------------------------------------------------
1 | const compiler = require('../compiler.js');
2 |
3 | describe('Scoped Mode Imports', () => {
4 | test('do no apply styling', async () => {
5 | const source = `
8 | Error
9 | Success
`;
10 |
11 | const expectedOutput = `
14 | Error
15 | Success
`;
16 |
17 | const output = await compiler(
18 | {
19 | source,
20 | },
21 | {
22 | mode: 'scoped',
23 | localIdentName: '[local]-123',
24 | }
25 | );
26 |
27 | expect(output).toBe(expectedOutput);
28 | });
29 |
30 | test('Import all classes from stylesheet', async () => {
31 | const source = `
34 | Error
35 | Success
`;
36 |
37 | const expectedOutput =
38 | `
41 | Error
42 | Success
` +
43 | ``;
46 |
47 | const output = await compiler(
48 | {
49 | source,
50 | },
51 | {
52 | mode: 'scoped',
53 | localIdentName: '[local]-123',
54 | parseExternalStylesheet: true,
55 | }
56 | );
57 |
58 | expect(output).toBe(expectedOutput);
59 | });
60 |
61 | test('Destructuring imports', async () => {
62 | const source = `
65 | Error
66 | Success
`;
67 |
68 | const expectedOutput =
69 | `
72 | Error
73 | Success
` +
74 | ``;
77 |
78 | const output = await compiler(
79 | {
80 | source,
81 | },
82 | {
83 | mode: 'scoped',
84 | localIdentName: '[local]-123',
85 | parseExternalStylesheet: true,
86 | }
87 | );
88 |
89 | expect(output).toBe(expectedOutput);
90 | });
91 |
92 | test('multiple selectors imported', async () => {
93 | const source = `
96 | Success
97 | Error
`;
98 |
99 | const expectedOutput =
100 | `
103 | Success
104 | Error
` +
105 | ``;
109 |
110 | const output = await compiler(
111 | {
112 | source,
113 | },
114 | {
115 | mode: 'scoped',
116 | localIdentName: '[local]-123',
117 | parseExternalStylesheet: true,
118 | }
119 | );
120 | });
121 |
122 | test('Class directives with default specifier', async () => {
123 | const source = `
126 | Error
127 | Success
`;
128 |
129 | const expectedOutput =
130 | `
133 | Error
134 | Success
` +
135 | ``;
138 |
139 | const output = await compiler(
140 | {
141 | source,
142 | },
143 | {
144 | mode: 'scoped',
145 | localIdentName: '[local]-123',
146 | parseExternalStylesheet: true,
147 | }
148 | );
149 |
150 | expect(output).toBe(expectedOutput);
151 | });
152 |
153 | test('Imports into existing `;
163 |
164 | const expectedOutput =
165 | `
168 | Error
169 | Success
170 | `;
175 |
176 | const output = await compiler(
177 | {
178 | source,
179 | },
180 | {
181 | mode: 'scoped',
182 | localIdentName: '[local]-123',
183 | parseExternalStylesheet: true,
184 | }
185 | );
186 |
187 | expect(output).toBe(expectedOutput);
188 | });
189 | });
190 |
--------------------------------------------------------------------------------
/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 `',
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 |
--------------------------------------------------------------------------------
/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: ` `,
126 | },
127 | {
128 | localIdentName: '[local]-123',
129 | }
130 | );
131 | expect(output).toBe(
132 | ` `
133 | );
134 | });
135 |
136 | test('Replace classname listed in Red `,
140 | },
141 | {
142 | localIdentName: '[local]-123',
143 | }
144 | );
145 | expect(output).toBe(
146 | `Red `
147 | );
148 | });
149 |
150 | test('Replace class attribute only', async () => {
151 | const output = await compiler(
152 | {
153 | source: `Red `,
154 | },
155 | {
156 | localIdentName: '[local]-123',
157 | }
158 | );
159 | expect(output).toBe(
160 | `Red `
161 | );
162 | });
163 |
164 | test('Skip empty class attribute', async () => {
165 | const output = await compiler(
166 | {
167 | source: `Red `,
168 | },
169 | {
170 | localIdentName: '[local]-123',
171 | }
172 | );
173 | expect(output).toBe(
174 | `Red `
175 | );
176 | });
177 |
178 | test('Parse extra attributes as well', async () => {
179 | const output = await compiler(
180 | {
181 | source: `Red `,
182 | },
183 | {
184 | localIdentName: '[local]-123',
185 | includeAttributes: ['data-color'],
186 | }
187 | );
188 | expect(output).toBe(
189 | `Red `
190 | );
191 | });
192 |
193 | test('Do not replace the classname', async () => {
194 | const output = await compiler(
195 | {
196 | source: `Red `,
197 | },
198 | {
199 | localIdentName: '[local]-123',
200 | }
201 | );
202 | expect(output).toBe(`Red `);
203 | });
204 |
205 | test('Do not replace the classname when `parseStyleTag` is off', async () => {
206 | const output = await compiler(
207 | {
208 | source: `Red `,
209 | },
210 | {
211 | localIdentName: '[local]-123',
212 | parseStyleTag: false,
213 | }
214 | );
215 | expect(output).toBe(
216 | `Red `
217 | );
218 | });
219 | });
220 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Svelte preprocess CSS Modules
2 |
3 | Generate CSS Modules classnames on Svelte components
4 |
5 | ```bash
6 | npm install --save-dev svelte-preprocess-cssmodules
7 | ```
8 |
9 | for `svelte 4` and below, use version 2 of the preprocessor.
10 |
11 | ## Table of Content
12 |
13 | - [Usage](#usage)
14 | - [Approach](#approach)
15 | - [Class objects and arrays](#class-objects-and-arrays)
16 | - [Class directive](#class-directive)
17 | - [Local selector](#local-selector)
18 | - [CSS binding](#css-binding)
19 | - [Scoped class on child components](#scoped-class-on-child-components)
20 | - [Import styles from an external stylesheet](#import-styles-from-an-external-stylesheet)
21 | - [Destructuring import](#destructuring-import)
22 | - [kebab-case situation](#kebab-case-situation)
23 | - [Unnamed import](#unnamed-import)
24 | - [Directive and dynamic class](#directive-and-dynamic-class)
25 | - [Preprocessor Modes](#preprocessor-modes)
26 | - [Native](#native)
27 | - [Mixed](#mixed)
28 | - [Scoped](#scoped)
29 | - [Comparative](#comparative)
30 | - [Why CSS Modules over Svelte scoping?](#why-css-modules-over-svelte-scoping)
31 | - [Configuration](#configuration)
32 | - [Rollup](#rollup)
33 | - [Webpack](#webpack)
34 | - [SvelteKit](#sveltekit)
35 | - [Svelte Preprocess](#svelte-preprocess)
36 | - [Vite](#vite)
37 | - [Options](#options)
38 | - [Code example](#code-example)
39 |
40 | ## Usage
41 |
42 | Add the `module` attribute to `
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 | Ok
306 | Cancel
307 |
308 | ```
309 |
310 | *generating*
311 |
312 | ```html
313 |
321 |
322 |
323 | Ok
324 | Cancel
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 |
429 | {@render props.children?.()}
430 |
431 |
432 |
438 | ```
439 |
440 | ```html
441 |
442 |
443 |
446 |
447 |
448 |
Welcome
449 |
Lorem ipsum tut ewou tu po
450 |
Start
451 |
452 |
453 |
463 | ```
464 |
465 | _generating_
466 |
467 | ```html
468 |
469 |
Welcome
470 |
Lorem ipsum tut ewou tu po
471 |
Start
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 | isSuccess = !isSuccess}>Toggle
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 |
722 | Home
723 | About
724 |
725 |
726 |
739 |
740 |
741 |
742 |
743 | Home
744 | About
745 |
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 | Ok
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 | Ok
1013 | Cancel
1014 |
1018 | ```
1019 |
1020 | _generating_
1021 |
1022 | ```html
1023 | Ok
1024 | Cancel
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 | Red
1047 | Red or Blue
1048 |
1052 | ```
1053 |
1054 | _generating_
1055 |
1056 | ```html
1057 | Red
1058 | Red or Blue
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 |
1173 |
1174 |
1175 |
Lorem ipsum dolor sit, amet consectetur.
1176 |
1177 |
1178 | Ok
1179 | Cancel
1180 |
1181 |
1182 | ```
1183 |
1184 | *Final html code generated by svelte*
1185 |
1186 | ```html
1187 |
1204 |
1205 |
1206 |
1207 |
1208 |
Lorem ipsum dolor sit, amet consectetur.
1209 |
1210 |
1211 | Ok
1212 | Cancel
1213 |
1214 |
1215 | ```
1216 | ## License
1217 |
1218 | [MIT](https://opensource.org/licenses/MIT)
1219 |
--------------------------------------------------------------------------------