├── example ├── .npmrc ├── bun.lockb ├── src │ ├── lib │ │ └── index.ts │ ├── app.d.ts │ ├── app.html │ ├── components │ │ └── test.svelte │ └── routes │ │ └── +page.svelte ├── static │ └── favicon.png ├── vite.config.ts ├── .gitignore ├── tsconfig.json ├── package.json ├── svelte.config.js └── README.md ├── .DS_Store ├── .vscode └── settings.json ├── .gitattributes ├── .npmignore ├── tests ├── environment │ ├── E2E.svelte │ ├── Styled.svelte │ └── Child.svelte ├── dummy.ts ├── e2e.test.ts ├── transform.test.ts └── walk.test.ts ├── lib ├── rules.ts ├── state.ts ├── error.ts ├── index.ts └── walk.ts ├── types └── index.d.ts ├── .github └── workflows │ └── test.yml ├── tsconfig.json ├── LICENSE ├── package.json ├── .gitignore ├── bun.lock └── README.md /example/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanNitschke/svelte-css-rune/HEAD/.DS_Store -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "rebeccapurple" 4 | ] 5 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | lib 3 | tests 4 | node_modules 5 | bun.lockb 6 | package-lock.json -------------------------------------------------------------------------------- /example/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanNitschke/svelte-css-rune/HEAD/example/bun.lockb -------------------------------------------------------------------------------- /example/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. -------------------------------------------------------------------------------- /example/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanNitschke/svelte-css-rune/HEAD/example/static/favicon.png -------------------------------------------------------------------------------- /tests/environment/E2E.svelte: -------------------------------------------------------------------------------- 1 |
2 | test 3 |
4 | -------------------------------------------------------------------------------- /lib/rules.ts: -------------------------------------------------------------------------------- 1 | import { lt } from "semver"; 2 | import { VERSION } from "svelte/compiler"; 3 | 4 | export const RESTRICTED_RULES = lt(VERSION, "5.28.2"); 5 | -------------------------------------------------------------------------------- /lib/state.ts: -------------------------------------------------------------------------------- 1 | import type MagicString from "magic-string"; 2 | import type { AST } from "svelte/compiler"; 3 | 4 | export type ProcessorState = { 5 | ast: AST.Root; 6 | filename: string; 7 | magicContent: MagicString; 8 | }; -------------------------------------------------------------------------------- /tests/environment/Styled.svelte: -------------------------------------------------------------------------------- 1 | 4 |
5 | test 6 |
7 | -------------------------------------------------------------------------------- /example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | import "svelte-css-rune"; 4 | import "svelte"; 5 | 6 | 7 | export default defineConfig({ 8 | plugins: [sveltekit()] 9 | }); 10 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | 11 | # OS 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Env 16 | .env 17 | .env.* 18 | !.env.example 19 | !.env.test 20 | 21 | # Vite 22 | vite.config.js.timestamp-* 23 | vite.config.ts.timestamp-* 24 | -------------------------------------------------------------------------------- /example/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | 4 | declare global { 5 | namespace App { 6 | // interface Error {} 7 | // interface Locals {} 8 | // interface PageData {} 9 | // interface PageState {} 10 | // interface Platform {} 11 | } 12 | } 13 | 14 | export {}; 15 | -------------------------------------------------------------------------------- /tests/environment/Child.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 10 |
11 | 12 | 17 | -------------------------------------------------------------------------------- /example/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare global { 3 | 4 | 5 | /** 6 | * Declares usage of a CSS class 7 | * 8 | * 9 | * Example: 10 | * ```ts 11 | * let dark = boolean; 12 | * let class = dark?$css("black"):$css("white"); 13 | * ``` 14 | * ```svelte 15 | * 16 | * ``` 17 | * 18 | * @param classNames The name of the classes you want to use 19 | */ 20 | function $css(classNames: T): T; 21 | } 22 | export {}; -------------------------------------------------------------------------------- /example/src/components/test.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | {@render children?.()} 15 | 16 |
17 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Setup Bun 17 | uses: oven-sh/setup-bun@v1 18 | with: 19 | bun-version: latest 20 | 21 | - name: Install dependencies 22 | run: bun install 23 | 24 | - name: Build 25 | run: bun run build 26 | 27 | - name: Test 28 | run: bun test -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": true, 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "removeComments": false, 9 | "sourceMap": false, 10 | "strict": true, 11 | "baseUrl": ".", 12 | "skipLibCheck": true, 13 | "resolveJsonModule": true, 14 | "declaration": true, 15 | "lib": [ 16 | "esnext" 17 | ], 18 | "typeRoots": [ 19 | "./src/types", 20 | "./node_modules/@types", 21 | ], 22 | "types": [ 23 | "node" 24 | ] 25 | }, 26 | "include": [ 27 | "lib/**/*.ts" 28 | ], 29 | "exclude": [ 30 | "node_modules", 31 | "dist", 32 | "tasks" 33 | ] 34 | } -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | }, 14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite dev", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" 12 | }, 13 | "devDependencies": { 14 | "@sveltejs/adapter-auto": "^3.0.0", 15 | "@sveltejs/kit": "^2.0.0", 16 | "@sveltejs/vite-plugin-svelte": "^4.0.0", 17 | "sass-embedded": "^1.83.4", 18 | "svelte": "^5.0.0", 19 | "svelte-check": "^4.0.0", 20 | "svelte-css-rune": "link:svelte-css-rune", 21 | "typescript": "^5.0.0", 22 | "vite": "^5.4.11" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | clicked = !clicked} /> 7 | T 8 |
9 | clicked = !clicked} /> 10 |
11 | 12 | -------------------------------------------------------------------------------- /example/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | import { processCssRune } from 'svelte-css-rune'; 4 | 5 | /** @type {import('@sveltejs/kit').Config} */ 6 | const config = { 7 | // Consult https://svelte.dev/docs/kit/integrations 8 | // for more information about preprocessors 9 | 10 | preprocess: [ 11 | vitePreprocess(), 12 | processCssRune() 13 | ], 14 | 15 | kit: { 16 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 17 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 18 | // See https://svelte.dev/docs/kit/adapters for more information about adapters. 19 | adapter: adapter() 20 | } 21 | }; 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /tests/dummy.ts: -------------------------------------------------------------------------------- 1 | export const source = ` 4 | 7 | test`.trim();; 8 | 9 | 10 | export const used = ` 16 | test 17 | test 18 | test`.trim(); 19 | 20 | 21 | export const child = ``.trim(); 22 | 23 | export const baseStyles = ``.trim(); -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # sv 2 | 3 | Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). 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 | npx sv create 12 | 13 | # create a new project in my-app 14 | npx sv create my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Jan Nitschke 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-css-rune", 3 | "module": "dist/esm/index.js", 4 | "author": "Jan Nitschke", 5 | "license": "MIT", 6 | "readme": "README.md", 7 | "description": "$css rune for svelte. Allows you to pass classes between your svelte components.", 8 | "keywords": [ 9 | "svelte", 10 | "css", 11 | "rune", 12 | "classes", 13 | "preprocessor", 14 | "global" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/JanNitschke/svelte-css-rune.git" 19 | }, 20 | "type": "module", 21 | "version": "0.3.0", 22 | "scripts": { 23 | "prebuild": "rm -rf dist/", 24 | "build": "npm run build:cjs && npm run build:esm", 25 | "build:cjs": "tsc --module commonjs --target es6 --outDir dist/cjs --declaration true", 26 | "build:esm": "tsc --module esnext --target esnext --outDir dist/esm", 27 | "dev": "npm run build:esm -- -w" 28 | }, 29 | "devDependencies": { 30 | "@types/bun": "latest", 31 | "@types/semver": "^7.7.0", 32 | "estree-walker": "^3.0.3", 33 | "happy-dom": "^16.6.0", 34 | "module-from-string": "^3.3.1", 35 | "semver": "^7.7.1", 36 | "typescript": "^5.7.3", 37 | "zimmerframe": "^1.1.2" 38 | }, 39 | "peerDependencies": { 40 | "svelte": "5.x" 41 | }, 42 | "dependencies": { 43 | "magic-string": "^0.30.17" 44 | }, 45 | "types": "./types/index.d.ts", 46 | "exports": { 47 | ".": { 48 | "import": "./dist/esm/index.js", 49 | "require": "./dist/cjs/index.js", 50 | "types": "./dist/esm/index.d.ts" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /lib/error.ts: -------------------------------------------------------------------------------- 1 | type Info ={ 2 | start: number; 3 | end: number; 4 | message: string; 5 | detail?: string; 6 | } 7 | 8 | const MAX_WIDTH = 80; 9 | 10 | export type FormattedLocation = {text: string, startColumn: number}; 11 | 12 | function overflowLine(line: string) { 13 | if(!line){ 14 | return ""; 15 | } 16 | if(line.length > MAX_WIDTH){ 17 | return line.substring(0, MAX_WIDTH - 3) + "..."; 18 | } 19 | return line; 20 | } 21 | 22 | function chunkSubstr(str: string, maxLine?: number) { 23 | const numChunks = Math.ceil(str.length / MAX_WIDTH) 24 | const chunks = new Array(numChunks) 25 | 26 | for (let i = 0; i < numChunks; ++i) { 27 | chunks[i] = str.substring(i * MAX_WIDTH, (i + 1) * MAX_WIDTH) 28 | } 29 | if(maxLine){ 30 | chunks.splice(maxLine, chunks.length - (maxLine - 1)); 31 | chunks[maxLine] = overflowLine(chunks[maxLine]); 32 | } 33 | return chunks.filter(c => c).join("\n"); 34 | } 35 | 36 | 37 | export const printLocation = (filename: string|undefined, content: string,start: number, end:number, height: number = 5) => { 38 | let message = ""; 39 | 40 | // if the warning/error is from the compiler, pretty print it 41 | if(filename){ 42 | message += filename + "\n\n"; 43 | } 44 | 45 | const preError = content.substring(0, start); 46 | const length = (end ?? start + 1) - start; 47 | const preLines = preError.split("\n"); 48 | preLines.pop(); // get the lines before the location 49 | 50 | const startLine = preLines.length; 51 | const lines = content.split("\n"); 52 | const line = lines[startLine].replaceAll("\t", " "); 53 | // get the index of the start of the line. 54 | const lineStart = preLines.reduce((acc, val) => acc + val.length, 0) + preLines.length; // add back the new line characters 55 | const lineCountLength = startLine.toString().length; 56 | const baseColumn = (start - lineStart + lineCountLength + 2); 57 | const startColumn = (baseColumn % MAX_WIDTH); 58 | const locOverflowLine = Math.floor((baseColumn + length) / MAX_WIDTH) + 1; 59 | 60 | // print the lines before the location 61 | for(let i = Math.max(0, startLine - height); i < startLine; i++){ 62 | let warnLine = i.toString().padStart(lineCountLength, " "); 63 | warnLine += " |"; 64 | warnLine += lines[i].replaceAll("\t", " "); 65 | message += overflowLine(warnLine) + "\n"; 66 | } 67 | 68 | // print the line with the location 69 | let locLine = startLine.toString(); 70 | locLine += " |"; 71 | locLine += line; 72 | message += chunkSubstr(locLine, locOverflowLine) + "\n"; 73 | // mark the location 74 | message += " ".repeat(Math.max(startColumn, 0)); 75 | message += "^".repeat(length); 76 | message += "\n"; 77 | 78 | return { 79 | text: message, 80 | startColumn: startColumn + (length / 2) 81 | }; 82 | }; 83 | 84 | export const printBelow = (message: string, index: number) => { 85 | const lines = message.split("\n"); 86 | const aligned = lines.map((line) => { 87 | const startPadding = Math.floor(index - (line.length / 2)); 88 | if(startPadding + line.length >= MAX_WIDTH){ 89 | return line; 90 | } 91 | return line.padStart(startPadding + line.length, " "); 92 | }); 93 | return aligned.join("\n"); 94 | } 95 | 96 | export const prettyMessage = (filename: string|undefined, content: string, info: Info) => { 97 | let message = info.message; 98 | 99 | message += "\n\n"; 100 | const {text, startColumn} = printLocation(filename, content, info.start, info.end); 101 | message += text; 102 | 103 | // print warning/error details centered in the warning/error range 104 | if(info.detail){ 105 | message += printBelow(info.detail, startColumn); 106 | } 107 | 108 | return message; 109 | } -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "svelte/compiler"; 2 | import type { AST, MarkupPreprocessor } from "svelte/compiler"; 3 | import { findReferencedClasses, transformCSS, transformRunes } from "./walk.js"; 4 | import MagicString from "magic-string"; 5 | import { prettyMessage, printLocation, printBelow } from "./error.js"; 6 | 7 | const RUNE_CLASSES = ["__css_rune", "__css_rune_specific"]; 8 | declare global { 9 | /** 10 | * Declares usage of a CSS class 11 | * 12 | * 13 | * Example: 14 | * ```ts 15 | * let dark = boolean; 16 | * let class = dark?$css("black"):$css("white"); 17 | * ``` 18 | * ```svelte 19 | * 20 | * ``` 21 | * 22 | * @param classNames The name of the classes you want to use 23 | */ 24 | function $css(classNames: T): T; 25 | } 26 | 27 | const regex_return_characters = /\r/g; 28 | 29 | function genHash(str: string) { 30 | str = str.replace(regex_return_characters, ""); 31 | let hash = 5381; 32 | let i = str.length; 33 | 34 | while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i); 35 | return (hash >>> 0).toString(36); 36 | } 37 | 38 | export type Options = { 39 | hash: (str: string) => string; 40 | mixedUseWarnings: false | "use" | true; 41 | increaseSpecificity: boolean; 42 | }; 43 | 44 | const markup: (options: Options) => MarkupPreprocessor = 45 | ({ hash, mixedUseWarnings, increaseSpecificity }) => 46 | ({ content, filename }) => { 47 | let ast: AST.Root; 48 | try { 49 | ast = parse(content, { modern: true, filename }); 50 | } catch (err) { 51 | throw new Error(`${err}\n\nThe svelte component failed to be parsed.`); 52 | } 53 | const runeClasses = increaseSpecificity ? RUNE_CLASSES : []; 54 | try { 55 | const { classes, usedClasses } = findReferencedClasses(ast); 56 | // skip if rune is not used 57 | if (classes.size === 0) { 58 | return { code: content }; 59 | } 60 | const hashed = hash(filename + content); 61 | const magicContent = new MagicString(content); 62 | if (mixedUseWarnings) { 63 | classes.forEach(({ start, end }, className) => { 64 | const used = usedClasses.get(className); 65 | if (used) { 66 | let warning = `[css rune]: The class "${className}" is used directly and with the $css rune. Consider using the $css rune for all classes.`; 67 | const runeLoc = printLocation(filename, content, start, end, 3); 68 | const usedLoc = printLocation("", content, used.start, used.end, 3); 69 | warning += "\n\n" + runeLoc.text; 70 | warning += printBelow("used with $css rune", runeLoc.startColumn); 71 | warning += "\n" + usedLoc.text; 72 | warning += printBelow( 73 | "used without $css rune", 74 | usedLoc.startColumn 75 | ); 76 | warning += "\n\n"; 77 | warning += 78 | "You can suppress this warning by setting the `mixedUseWarnings` option to `false`.\n"; 79 | warning += 80 | "More Information: https://github.com/JanNitschke/svelte-css-rune#edge-cases \n"; 81 | warning += "\n"; 82 | console.warn(warning); 83 | } 84 | }); 85 | } 86 | 87 | const transformedClasses = transformCSS( 88 | ast, 89 | content, 90 | magicContent, 91 | classes, 92 | usedClasses, 93 | hashed, 94 | filename, 95 | content, 96 | mixedUseWarnings === true, 97 | runeClasses 98 | ); 99 | transformRunes( 100 | ast, 101 | magicContent, 102 | classes, 103 | transformedClasses, 104 | runeClasses 105 | ); 106 | const code = magicContent.toString(); 107 | 108 | return { 109 | code, 110 | map: magicContent.generateMap({ hires: true }), 111 | }; 112 | } catch (err: any) { 113 | // pretty print the error 114 | err.filename = filename; 115 | 116 | // if the error is not from the compiler, throw it 117 | if (err.start === undefined || err.end === undefined) { 118 | // this in an internal error 119 | // this should never happen 120 | // throw it to let the user know 121 | // this line should always be unreachable and untested 122 | throw err; 123 | } 124 | 125 | const e = new Error("\n\n[$css rune]: " + err.message + "\n\n"); 126 | e.message = prettyMessage(filename, content, err); 127 | delete e.stack; 128 | throw e; 129 | } 130 | }; 131 | export const processCssRune = (options: Partial = {}) => { 132 | const defaultOptions: Options = { 133 | hash: genHash, 134 | mixedUseWarnings: "use", 135 | increaseSpecificity: false, 136 | }; 137 | const o = { ...defaultOptions, ...options }; 138 | 139 | // allow to override hash option with undefined to use the default hash function (for testing) 140 | if (!o.hash) { 141 | o.hash = genHash; 142 | } 143 | 144 | return { 145 | markup: markup(o), 146 | }; 147 | }; 148 | 149 | export default processCssRune; 150 | -------------------------------------------------------------------------------- /tests/e2e.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "bun:test"; 2 | import { compile, Processed } from "svelte/compiler"; 3 | import cssRune, { type Options } from "../lib/index"; 4 | import { Window, type CSSStyleDeclaration, type Element } from "happy-dom"; 5 | import { render } from "svelte/server"; 6 | import { importFromStringSync } from "module-from-string"; 7 | import path from "path"; 8 | import { readFileSync } from "fs"; 9 | 10 | /** 11 | * @module tests/e2e.test.ts 12 | * 13 | * @desc This Module tests the end-to-end functionality of the preprocessor. Components are transformed and the resulting css is loaded into a DOM. 14 | * 15 | */ 16 | 17 | /** 18 | * @constant DEFAULT_OPTIONS 19 | * 20 | * @desc Test options for the preprocessor. 21 | */ 22 | 23 | const DEFAULT_OPTIONS: Partial = {}; 24 | 25 | /** 26 | * @function transform 27 | * 28 | * @desc This function transforms a svelte component and returns the compiled code. 29 | */ 30 | const transform = (code: string, options: Partial) => { 31 | const filename = path.join(__dirname, "environment", "test.svelte"); 32 | const { markup } = cssRune(options); 33 | const preprocessed = markup({ content: code, filename }) as Processed; 34 | 35 | const compiled = compile(preprocessed.code, { filename, generate: "server" }); 36 | 37 | return compiled; 38 | }; 39 | 40 | /** 41 | * @function transformEnv 42 | * 43 | * @desc This function transforms all the components in the environment folder and returns a Map that can be used to import them. 44 | * @param options - The options for the preprocessor 45 | * @param load - If true, the components are loaded into the environment. If false, only the css is returned. 46 | */ 47 | 48 | const transformEnv = ( 49 | options: Partial, 50 | load: boolean = true 51 | ) => { 52 | const dirName = path.join(__dirname, "environment"); 53 | const compiledFiles: Record = {}; 54 | 55 | const envCss: string[] = []; 56 | 57 | for (const file of options.env ?? []) { 58 | const filename = path.join(dirName, file); 59 | const content = readFileSync(filename, "utf-8"); 60 | const { markup } = cssRune(options); 61 | const preprocessed = markup({ content, filename }) as Processed; 62 | const compiled = compile(preprocessed.code, { 63 | filename, 64 | generate: "server", 65 | }); 66 | const exportName = file.replace(".svelte", ""); 67 | envCss.push(compiled.css?.code || ""); 68 | if (load) { 69 | const mod = importFromStringSync(compiled.js.code); 70 | Object.assign(mod.default, mod); 71 | compiledFiles[exportName] = mod.default; 72 | } 73 | } 74 | return { env: compiledFiles, css: envCss.join("\n") }; 75 | }; 76 | 77 | /** 78 | * @function toCSSOnly 79 | * 80 | * @desc This function transforms a svelte component and creates a document with only the css loaded. 81 | */ 82 | export const toCSSOnly = ( 83 | code: string, 84 | options: Partial = DEFAULT_OPTIONS 85 | ) => { 86 | const combinedOptions = { ...DEFAULT_OPTIONS, ...options }; 87 | const { css } = options.env 88 | ? transformEnv(combinedOptions, false) 89 | : { css: "" }; 90 | const compiled = transform(code, combinedOptions); 91 | const window = new Window({ url: "https://localhost:8080" }); 92 | const document = window.document; 93 | if (!compiled.css?.code) { 94 | return { 95 | document: null, 96 | getStyle: ( 97 | node?: Element | null | undefined 98 | ): CSSStyleDeclaration | null => null, 99 | }; 100 | } 101 | document.head.appendChild(document.createElement("style")).textContent = 102 | compiled.css.code; 103 | document.head.appendChild(document.createElement("style")).textContent = css; 104 | 105 | return { 106 | document, 107 | getStyle: (node?: Element | null | undefined): CSSStyleDeclaration | null => 108 | !node || !(node as any).style 109 | ? null 110 | : window.getComputedStyle(node as any), 111 | }; 112 | }; 113 | 114 | /** 115 | * @function toDOM 116 | * 117 | * @desc This function transforms a svelte component and creates a document with the css and html loaded. Injects the environment components. 118 | */ 119 | export const toDOM = ( 120 | code: string, 121 | options: Partial = DEFAULT_OPTIONS, 122 | receive?: (content: any) => void 123 | ) => { 124 | const combinedOptions = { ...DEFAULT_OPTIONS, ...options }; 125 | const compiled = transform(code, combinedOptions); 126 | const window = new Window({ url: "https://localhost:8080" }); 127 | const document = window.document; 128 | 129 | const { env, css } = options.env 130 | ? transformEnv(combinedOptions, true) 131 | : { env: {}, css: "" }; 132 | 133 | const send = (content: any) => { 134 | if (receive) { 135 | receive(content); 136 | } 137 | }; 138 | 139 | const module = importFromStringSync(compiled.js.code, { 140 | globals: { env, console, send }, 141 | }); 142 | const { body, head } = render(module.default); 143 | document.head.innerHTML = head; 144 | if (compiled.css?.code) { 145 | document.head.appendChild(document.createElement("style")).textContent = 146 | compiled.css.code; 147 | } 148 | document.head.appendChild(document.createElement("style")).textContent = css; 149 | document.body.innerHTML = body; 150 | return { 151 | document, 152 | getStyle: (node?: Element | null | undefined): CSSStyleDeclaration | null => 153 | !node || !(node as any).style 154 | ? null 155 | : window.getComputedStyle(node as any), 156 | }; 157 | }; 158 | 159 | /** 160 | * Makes sure the transform function works 161 | */ 162 | describe("e2e transform", () => { 163 | it("should transform a component to rendered DOM", () => { 164 | const code = ` 165 | 170 |
Hello World
171 | `; 172 | const { document, getStyle } = toDOM(code); 173 | const testDiv = document?.querySelector(".test"); 174 | expect(testDiv).toBeTruthy(); 175 | expect(testDiv?.textContent).toBe("Hello World"); 176 | expect(getStyle(testDiv)?.color).toBe("red"); 177 | }); 178 | it("should transform a component to rendered CSS", () => { 179 | const code = ` 180 | 185 | `; 186 | const { document } = toCSSOnly(code); 187 | expect(document).toBeTruthy(); 188 | expect(document?.head.innerHTML).toInclude("(unused)"); 189 | }); 190 | 191 | it("should resolve imports from the environment folder", () => { 192 | const code = ` 193 | 196 | 197 | `; 198 | const { document } = toDOM(code, { env: ["E2E.svelte"] }); 199 | expect(document).toBeTruthy(); 200 | expect(document?.querySelector("#e2e")).toBeTruthy(); 201 | }); 202 | 203 | it("should receive messages from the component", () => { 204 | const code = ` 205 | 208 | `; 209 | let received = ""; 210 | const receive = (content: any) => { 211 | received = content; 212 | }; 213 | toDOM(code, DEFAULT_OPTIONS, receive); 214 | expect(received).toBe("Hello World"); 215 | }); 216 | 217 | it("should load child css", () => { 218 | const code = ` 219 | 222 | 223 | `; 224 | const { document, getStyle } = toDOM(code, { env: ["E2E.svelte"] }); 225 | expect(document).toBeTruthy(); 226 | const child = document?.querySelector("#e2e"); 227 | expect(child).toBeTruthy(); 228 | expect(getStyle(child)?.color).toBe("red"); 229 | }); 230 | }); 231 | -------------------------------------------------------------------------------- /tests/transform.test.ts: -------------------------------------------------------------------------------- 1 | import processCssRune from "../lib/index.ts"; 2 | import { describe, expect, it } from "bun:test"; 3 | import { baseStyles } from "./dummy.ts"; 4 | import { Processed } from "svelte/compiler"; 5 | import { toDOM, toCSSOnly } from "./e2e.test.ts"; 6 | import { RESTRICTED_RULES } from "../lib/rules.ts"; 7 | 8 | const hash = () => "hash"; 9 | 10 | const transform = (content, warn = false) => 11 | processCssRune({ hash, mixedUseWarnings: warn }).markup({ 12 | content, 13 | filename: "test.svelte", 14 | }) as Processed; 15 | 16 | describe("preprocessor", () => { 17 | it("should return the same code if no runes are used", () => { 18 | const result = transform(baseStyles); 19 | expect(result.code).toEqual(baseStyles); 20 | }); 21 | 22 | it("should make classes that are referenced by a rune global", () => { 23 | const { document, getStyle } = toDOM( 24 | ` 25 | test 26 | `, 27 | { hash: () => "hash" } 28 | ); 29 | const testSpan = document.createElement("span"); 30 | testSpan.className = "test-hash"; 31 | document.body.appendChild(testSpan); 32 | const style = getStyle(testSpan); 33 | expect(style?.color).toEqual("rebeccapurple"); 34 | }); 35 | 36 | it("should allow passing classes to child components", () => { 37 | const { document, getStyle } = toDOM( 38 | ` 39 | 40 | 41 | `, 45 | { env: ["Child.svelte"] } 46 | ); 47 | const child = document.querySelector("#child-component"); 48 | const childButton = document.querySelector("#child-button"); 49 | expect(getStyle(child)?.color).toEqual("rebeccapurple"); 50 | expect(getStyle(childButton)?.color).toEqual("red"); 51 | }); 52 | 53 | it("should make generated rules more specific than the svelte default when using increaseSpecificity", () => { 54 | const { document, getStyle } = toDOM( 55 | ` 56 | 57 | 58 | `, 61 | { env: ["Child.svelte"], increaseSpecificity: true } 62 | ); 63 | const childButton = document.querySelector("#child-button"); 64 | console.log(document.body.innerHTML, document.head.innerHTML); 65 | expect(getStyle(childButton)?.border).toInclude("none"); 66 | }); 67 | 68 | it("should avoid style collisions", () => { 69 | const { document, getStyle } = toDOM( 70 | ` 71 | 72 | 73 | test 74 | `, 77 | { env: ["Styled.svelte"] } 78 | ); 79 | const styled = document.querySelector("#styled"); 80 | const span = document.querySelector("span"); 81 | expect(getStyle(styled)?.color).toEqual("red"); 82 | expect(getStyle(span)?.color).toEqual("rebeccapurple"); 83 | }); 84 | 85 | it("should allow passing classes to javascript", () => { 86 | let received: any = null; 87 | const receive = (value) => { 88 | received = value; 89 | }; 90 | const { document, getStyle } = toDOM( 91 | ` 92 | 93 | `, 96 | {}, 97 | receive 98 | ); 99 | expect(received).toBeTruthy(); 100 | expect(received).not.toEqual("test"); 101 | expect(received).toStartWith("test"); 102 | }); 103 | 104 | it("should allow passing classes to javascript modules", () => { 105 | let received: any = null; 106 | const receive = (value) => { 107 | received = value; 108 | }; 109 | toDOM( 110 | ` 111 | 112 | `, 115 | {}, 116 | receive 117 | ); 118 | expect(received).toBeTruthy(); 119 | expect(received).not.toEqual("test"); 120 | expect(received).toStartWith("test"); 121 | }); 122 | 123 | it("should allow mixing of global and local class usage", () => { 124 | const { document, getStyle } = toDOM(` 125 | test 126 | test 127 | 128 | `); 129 | const spans = document.querySelectorAll("span"); 130 | expect(getStyle(spans[0])?.color).toEqual("rebeccapurple"); 131 | expect(getStyle(spans[1])?.color).toEqual("rebeccapurple"); 132 | }); 133 | 134 | it("should allow mixing of global and local class usage with complex selectors", () => { 135 | const { document, getStyle } = toDOM(` 136 |
137 | test 138 | test 139 |
140 | 141 | `); 142 | const spans = document.querySelectorAll("span"); 143 | expect(getStyle(spans[0])?.color).toEqual("rebeccapurple"); 144 | expect(getStyle(spans[1])?.color).toEqual("rebeccapurple"); 145 | }); 146 | it("should allow chaining of global classes", () => { 147 | const { document, getStyle } = toDOM(` 148 | test 149 | 150 | `); 151 | const span = document.querySelector("span"); 152 | expect(getStyle(span)?.color).toEqual("rebeccapurple"); 153 | }); 154 | 155 | it("should allow chaining of global and local classes", () => { 156 | const { document, getStyle } = toDOM(` 157 | 158 |
test
159 | test 160 | 161 | `); 162 | const span = document.querySelector("span"); 163 | expect(getStyle(span)?.color).toEqual("rebeccapurple"); 164 | }); 165 | it("should fail on illegal mixed css selector", () => { 166 | const exec = () => 167 | toDOM(` 168 |
169 |

170 | text 171 |

172 | test 173 |
174 |
175 |

inner

176 |
177 | 178 | `); 179 | 180 | const exec2 = () => 181 | toDOM(` 182 |
183 |
184 |
185 |
186 |
187 | 188 | 189 | `); 190 | 191 | const exec3 = () => 192 | toDOM(` 193 |
194 |

195 |

196 |
197 | 198 | `); 199 | 200 | expect(exec).toThrow( 201 | "Invalid class placement. Svelte only allows global classes at the beginning or end of a selector list." 202 | ); 203 | expect(exec2).not.toThrow(); 204 | expect(exec3).toThrow( 205 | "Invalid class placement. Svelte only allows global classes at the beginning or end of a selector list." 206 | ); 207 | }); 208 | 209 | if (!RESTRICTED_RULES) { 210 | it("should allow multiple selectors", () => { 211 | const { document, getStyle } = toDOM(` 212 | 213 | test 214 | test 215 | 216 | `); 217 | const span = document.querySelector("span"); 218 | expect(getStyle(span)?.color).toEqual("rebeccapurple"); 219 | }); 220 | } 221 | 222 | it("should detect incorrect usage with non class selectors", () => { 223 | const exec = () => 224 | toDOM(` 225 |
226 |
227 |
228 | 229 | 230 | `); 231 | 232 | expect(exec).toThrow( 233 | "Invalid class placement. Svelte only allows global classes at the beginning or end of a selector list." 234 | ); 235 | }); 236 | 237 | it("should fail on invalid components", () => { 238 | const exec = () => 239 | transform(` 240 | <
241 | `); 242 | expect(exec).toThrow(); 243 | }); 244 | }); 245 | -------------------------------------------------------------------------------- /tests/walk.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, spyOn } from "bun:test"; 2 | import { findReferencedClasses, transformCSS, transformRunes } from "../lib/walk.ts"; 3 | import { parse } from "svelte/compiler"; 4 | import { baseStyles, source, used } from "./dummy.ts"; 5 | import MagicString from "magic-string"; 6 | 7 | describe("findReferencedClasses", () => { 8 | 9 | const run = (source: string) => { 10 | const ast = parse(source, { filename: "test.svelte", modern: true }); 11 | return findReferencedClasses(ast); 12 | } 13 | 14 | it("should find all classes referenced by runes", () => { 15 | const {classes} = run(source); 16 | expect(classes.size).toBe(3); 17 | expect(classes.has("test")).toBe(true); 18 | expect(classes.has("test2")).toBe(true); 19 | expect(classes.has("test3")).toBe(true); 20 | }); 21 | 22 | it("should find all classes used natively on class attributes as string", () => { 23 | const {usedClasses} = run(used); 24 | expect(usedClasses.has("test")).toBe(true); 25 | }); 26 | 27 | it("should find all classes used natively on class attributes as array", () => { 28 | const {usedClasses} = run(used); 29 | expect(usedClasses.has("test4")).toBe(true); 30 | }); 31 | 32 | 33 | it("should find all classes used natively on style bindings as object", () => { 34 | const {usedClasses} = run(used); 35 | expect(usedClasses.has("test8")).toBe(true); 36 | expect(usedClasses.has("test9")).toBe(true); 37 | }); 38 | 39 | it("not mistake unused classes as used", () => { 40 | const {usedClasses} = run(used); 41 | expect(usedClasses.has("test2")).toBe(false); 42 | expect(usedClasses.has("test3")).toBe(false); 43 | }); 44 | 45 | it("finds classes used by runes and natively", () => { 46 | const {usedClasses, classes} = run(used); 47 | expect(classes.has("test")).toBe(true); 48 | expect(usedClasses.has("test")).toBe(true); 49 | }); 50 | 51 | it("should detect multiple classes in a single rune", () => { 52 | const {usedClasses, classes} = run(`testtest`); 53 | 54 | expect(usedClasses.has("c")).toBe(true); 55 | expect(usedClasses.has("d")).toBe(true); 56 | expect(usedClasses.has("a")).toBe(false); 57 | expect(usedClasses.has("b")).toBe(false); 58 | 59 | expect(classes.has("a")).toBe(true); 60 | expect(classes.has("b")).toBe(true); 61 | expect(classes.has("c")).toBe(true); 62 | expect(classes.has("d")).toBe(false); 63 | 64 | }); 65 | 66 | it("should throw on improper rune usage", () => { 67 | expect(() => run(``)).toThrowError(); 68 | expect(() => run(``)).toThrowError(); 69 | expect(() => run(``)).toThrowError(); 70 | }); 71 | 72 | 73 | }); 74 | 75 | const loc = {start: 0, end: 0}; 76 | 77 | describe("transformCSS", () => { 78 | const run = (content: string, overrideClasses?: Map, overrideUsedClasses?: Map, warn = false) => { 79 | const magicContent = new MagicString(content); 80 | const ast = parse(content, { filename: "test.svelte", modern: true }); 81 | const h ="hash"; 82 | const {classes, usedClasses} = findReferencedClasses(ast); 83 | const transformedClasses = transformCSS(ast, content, magicContent, overrideClasses ?? classes, overrideUsedClasses ?? usedClasses, h, "test.svelte", content, warn); 84 | return {content: magicContent.toString(), transformedClasses}; 85 | } 86 | 87 | const spy = spyOn(console, "warn"); 88 | 89 | it("should transform only used classes", () => { 90 | const {content} = run(baseStyles, new Map(), new Map()); 91 | expect(content).toBe(baseStyles); 92 | }); 93 | it("should transform only used classes", () => { 94 | const {content, transformedClasses} = run(source + baseStyles); 95 | expect(content).not.toInclude("unused-hash"); 96 | expect(content).not.toInclude(":global(unused"); 97 | }); 98 | 99 | it("should map old styles", () => { 100 | const {transformedClasses} = run(source + baseStyles); 101 | expect(transformedClasses).toContainKeys(["test", "test2", "test3"]); 102 | expect(transformedClasses).not.toContainKey(["unused"]); 103 | const pairs = Object.entries(transformedClasses); 104 | expect(pairs.find(([key, value]) => value != key + "-hash")).toBeFalsy(); 105 | }); 106 | it("should duplicate styles if used by runes and native", () => { 107 | const {content} = run(used + baseStyles); 108 | expect(content).toMatch(/test[^-]/g); 109 | expect(content).toInclude(":global(.test-hash)"); 110 | }); 111 | 112 | it("should not warn if no rule uses multiple classes that are used by rune and natively", () => { 113 | expect(spy).toHaveBeenCalledTimes(0); 114 | }); 115 | 116 | it("should fail on illegal mixed css selector", () => { 117 | const globalClasses = new Map([["global1", loc], ["global2", loc], ["global3", loc], ["global4", loc]]); 118 | const exec = () => run("", globalClasses, new Map([["local", loc], ["test2",loc]])); 119 | const exec2 = () => run("", globalClasses, new Map([["global2", loc], ["test2",loc]])); 120 | const exec3 = () => run("", globalClasses, new Map([["local", loc], ["test2",loc]])); 121 | expect(exec).not.toThrowError(); 122 | expect(exec2).toThrowError(); 123 | expect(exec3).toThrowError(); 124 | }); 125 | 126 | it("should warn on mixed usage if mixedUseWarnings are enabled", () => { 127 | const warn = spyOn(console, "warn"); 128 | const globalClasses = new Map([["global1", loc], ["global2", loc], ["global3", loc], ["global4", loc]]); 129 | const exec = () => run("", globalClasses, new Map([["local", loc], ["local2",loc]]), true); 130 | const exec2 = () => run("", globalClasses, new Map([["local", loc], ["local2",loc]]), true); 131 | const exec3 = () => run("", globalClasses, new Map([["local", loc], ["local2",loc]]), true); 132 | const exec4 = () => run("", globalClasses, new Map([["local", loc], ["local2",loc]]), true); 133 | 134 | expect(exec).not.toThrowError(); 135 | expect(warn).toHaveBeenCalledTimes(0); 136 | expect(exec2).not.toThrowError(); 137 | expect(warn).toHaveBeenCalledTimes(0); 138 | expect(exec3).not.toThrowError(); 139 | expect(warn).toHaveBeenCalledTimes(1); 140 | expect(exec4).not.toThrowError(); 141 | expect(warn).toHaveBeenCalledTimes(2); 142 | }); 143 | }); 144 | 145 | 146 | describe("transformRunes", () => { 147 | const run = (content: string, runes: string[]) => { 148 | const magicContent = new MagicString(content); 149 | transformRunes(parse(content, { filename: "test.svelte", modern: true }), magicContent, new Map(runes.map((r) => [r, {start: 0, end: 0}])), Object.fromEntries(runes.map((r) => [r, r + "-hash"]))); 150 | return magicContent.toString(); 151 | }; 152 | 153 | 154 | it("should transform runes", () => { 155 | const content = ``; 156 | const runes = ["test"]; 157 | expect(run(content, runes)).toInclude("test-hash"); 158 | }); 159 | 160 | it ("should transform multiple runes", () => { 161 | const content = `test`; 162 | const runes = ["test"]; 163 | expect(run(content, runes)).toInclude("test-hash"); 164 | }); 165 | 166 | it ("should throw on runes with undefined classes", () => { 167 | const content = `test`; 168 | const runes = ["test2"]; 169 | expect(() => run(content, runes)).toThrowError(); 170 | }); 171 | 172 | it("should transform runes with multiple classes", () => { 173 | const content = `test`; 174 | const runes = ["test", "test2"]; 175 | expect(run(content, runes)).toInclude("test-hash"); 176 | expect(run(content, runes)).toInclude("test2-hash"); 177 | }); 178 | 179 | it("should transform runes in markup", () => { 180 | const content = `test`; 181 | const runes = ["test"]; 182 | expect(run(content, runes)).toInclude("test-hash"); 183 | }); 184 | 185 | it("should transform runes in markup with multiple classes", () => { 186 | const content = `test`; 187 | const runes = ["test", "test2"]; 188 | expect(run(content, runes)).toInclude("test-hash"); 189 | expect(run(content, runes)).toInclude("test2-hash"); 190 | }); 191 | 192 | it("should transform runes in modules", () => { 193 | const content = ``; 194 | const runes = ["test"]; 195 | expect(run(content, runes)).toInclude("test-hash"); 196 | }); 197 | 198 | it("should transform runes in modules with multiple classes", () => { 199 | const content = ``; 200 | const runes = ["test", "test2"]; 201 | expect(run(content, runes)).toInclude("test-hash"); 202 | expect(run(content, runes)).toInclude("test2-hash"); 203 | }); 204 | 205 | it("should transform runes in modules, scripts and markup", () => { 206 | const content = `test`; 207 | const runes = ["test", "test2", "test3", "test4"]; 208 | expect(run(content, runes)).toInclude("test-hash"); 209 | expect(run(content, runes)).toInclude("test2-hash"); 210 | expect(run(content, runes)).toInclude("test3-hash"); 211 | expect(run(content, runes)).toInclude("test4-hash"); 212 | }); 213 | }); -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "svelte-css-rune", 6 | "dependencies": { 7 | "magic-string": "^0.30.17", 8 | }, 9 | "devDependencies": { 10 | "@types/bun": "latest", 11 | "@types/semver": "^7.7.0", 12 | "estree-walker": "^3.0.3", 13 | "happy-dom": "^16.6.0", 14 | "module-from-string": "^3.3.1", 15 | "semver": "^7.7.1", 16 | "typescript": "^5.7.3", 17 | "zimmerframe": "^1.1.2", 18 | }, 19 | "peerDependencies": { 20 | "svelte": "5.x", 21 | }, 22 | }, 23 | }, 24 | "packages": { 25 | "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], 26 | 27 | "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.23.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ=="], 28 | 29 | "@esbuild/android-arm": ["@esbuild/android-arm@0.23.1", "", { "os": "android", "cpu": "arm" }, "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ=="], 30 | 31 | "@esbuild/android-arm64": ["@esbuild/android-arm64@0.23.1", "", { "os": "android", "cpu": "arm64" }, "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw=="], 32 | 33 | "@esbuild/android-x64": ["@esbuild/android-x64@0.23.1", "", { "os": "android", "cpu": "x64" }, "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg=="], 34 | 35 | "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.23.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q=="], 36 | 37 | "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.23.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw=="], 38 | 39 | "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.23.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA=="], 40 | 41 | "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.23.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g=="], 42 | 43 | "@esbuild/linux-arm": ["@esbuild/linux-arm@0.23.1", "", { "os": "linux", "cpu": "arm" }, "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ=="], 44 | 45 | "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.23.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g=="], 46 | 47 | "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.23.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ=="], 48 | 49 | "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.23.1", "", { "os": "linux", "cpu": "none" }, "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw=="], 50 | 51 | "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.23.1", "", { "os": "linux", "cpu": "none" }, "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q=="], 52 | 53 | "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.23.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw=="], 54 | 55 | "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.23.1", "", { "os": "linux", "cpu": "none" }, "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA=="], 56 | 57 | "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.23.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw=="], 58 | 59 | "@esbuild/linux-x64": ["@esbuild/linux-x64@0.23.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ=="], 60 | 61 | "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.23.1", "", { "os": "none", "cpu": "x64" }, "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA=="], 62 | 63 | "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.23.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q=="], 64 | 65 | "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.23.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA=="], 66 | 67 | "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.23.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA=="], 68 | 69 | "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.23.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A=="], 70 | 71 | "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.23.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ=="], 72 | 73 | "@esbuild/win32-x64": ["@esbuild/win32-x64@0.23.1", "", { "os": "win32", "cpu": "x64" }, "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg=="], 74 | 75 | "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], 76 | 77 | "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], 78 | 79 | "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], 80 | 81 | "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], 82 | 83 | "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], 84 | 85 | "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="], 86 | 87 | "@types/bun": ["@types/bun@1.2.12", "", { "dependencies": { "bun-types": "1.2.12" } }, "sha512-lY/GQTXDGsolT/TiH72p1tuyUORuRrdV7VwOTOjDOt8uTBJQOJc5zz3ufwwDl0VBaoxotSk4LdP0hhjLJ6ypIQ=="], 88 | 89 | "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], 90 | 91 | "@types/node": ["@types/node@22.15.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw=="], 92 | 93 | "@types/semver": ["@types/semver@7.7.0", "", {}, "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA=="], 94 | 95 | "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], 96 | 97 | "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], 98 | 99 | "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], 100 | 101 | "bun-types": ["bun-types@1.2.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-tvWMx5vPqbRXgE8WUZI94iS1xAYs8bkqESR9cxBB1Wi+urvfTrF1uzuDgBHFAdO0+d2lmsbG3HmeKMvUyj6pWA=="], 102 | 103 | "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], 104 | 105 | "esbuild": ["esbuild@0.23.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.23.1", "@esbuild/android-arm": "0.23.1", "@esbuild/android-arm64": "0.23.1", "@esbuild/android-x64": "0.23.1", "@esbuild/darwin-arm64": "0.23.1", "@esbuild/darwin-x64": "0.23.1", "@esbuild/freebsd-arm64": "0.23.1", "@esbuild/freebsd-x64": "0.23.1", "@esbuild/linux-arm": "0.23.1", "@esbuild/linux-arm64": "0.23.1", "@esbuild/linux-ia32": "0.23.1", "@esbuild/linux-loong64": "0.23.1", "@esbuild/linux-mips64el": "0.23.1", "@esbuild/linux-ppc64": "0.23.1", "@esbuild/linux-riscv64": "0.23.1", "@esbuild/linux-s390x": "0.23.1", "@esbuild/linux-x64": "0.23.1", "@esbuild/netbsd-x64": "0.23.1", "@esbuild/openbsd-arm64": "0.23.1", "@esbuild/openbsd-x64": "0.23.1", "@esbuild/sunos-x64": "0.23.1", "@esbuild/win32-arm64": "0.23.1", "@esbuild/win32-ia32": "0.23.1", "@esbuild/win32-x64": "0.23.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg=="], 106 | 107 | "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], 108 | 109 | "esrap": ["esrap@1.4.6", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw=="], 110 | 111 | "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], 112 | 113 | "happy-dom": ["happy-dom@16.8.1", "", { "dependencies": { "webidl-conversions": "^7.0.0", "whatwg-mimetype": "^3.0.0" } }, "sha512-n0QrmT9lD81rbpKsyhnlz3DgnMZlaOkJPpgi746doA+HvaMC79bdWkwjrNnGJRvDrWTI8iOcJiVTJ5CdT/AZRw=="], 114 | 115 | "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], 116 | 117 | "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], 118 | 119 | "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], 120 | 121 | "module-from-string": ["module-from-string@3.3.1", "", { "dependencies": { "esbuild": "^0.23.0", "nanoid": "^3.3.7" } }, "sha512-nFdOQ8NHJXR7ITj2JAwjpPSgX3vjbG2LfBL1YA5gil8sLkFTFa5pmV9P1NBGRik65u+NNyGEeUMcwkbqwPJ/ew=="], 122 | 123 | "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 124 | 125 | "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], 126 | 127 | "svelte": ["svelte@5.28.2", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^1.4.6", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-FbWBxgWOpQfhKvoGJv/TFwzqb4EhJbwCD17dB0tEpQiw1XyUEKZJtgm4nA4xq3LLsMo7hu5UY/BOFmroAxKTMg=="], 128 | 129 | "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], 130 | 131 | "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 132 | 133 | "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], 134 | 135 | "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], 136 | 137 | "zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="], 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # svelte-css-rune 2 | 3 | [![npm version](https://img.shields.io/npm/v/svelte-css-rune.svg)](https://www.npmjs.com/package/svelte-css-rune) 4 | [![npm downloads](https://img.shields.io/npm/dm/svelte-css-rune.svg)](https://www.npmjs.com/package/svelte-css-rune) 5 | [![Test Status](https://img.shields.io/github/actions/workflow/status/JanNitschke/svelte-css-rune/test.yml?branch=main)](https://github.com/JanNitschke/svelte-css-rune/actions) 6 | 7 | 8 | **`svelte-css-rune` is a Svelte library that allows you to effortlessly pass styles between components by introducing a new `$css` rune.** 9 | 10 | Svelte provides an elegant way to scope styles to components, but passing styles between parent and child components can be challenging. There's no built-in mechanism for this, often leading to workarounds like declaring classes as global and carefully managing potential naming conflicts. This library introduces a simple way to pass styles between components. It solves the problem of style conflicts and promotes better style encapsulation when working with nested components. 11 | 12 | The `$css` rune creates a globally unique class name for a given class, ensuring that it is unique to the file and the original class name. The style tag is modified to use the generated class name, and the class is made globally accessible with the `:global` selector. If the same class is used both with the `$css` rune and without it, the preprocessor respects both usages, ensuring that the styles are applied correctly. 13 | 14 | `svelte-css-rune` does not support css nesting. If you want to use css nesting please use a preprocessor that compiles this to normal css, ex. scss. 15 | 16 | ## Example 17 | 18 | Child.svelte 19 | ```svelte 20 | 23 | 24 |
25 | 26 |
27 | ``` 28 | Parent.svelte 29 | ```svelte 30 | 33 | 34 | 35 | 36 | 44 | ``` 45 | 46 | ### Use with component libraries 47 | 48 | This library was designed for component libraries. Effortlessly customize the style of your components! 49 | 50 | #### Customizing Existing Components 51 | 52 | ```svelte 53 | 56 | 57 | 58 | Unlimited 59 | 60 | 61 | 66 | ``` 67 | 68 | #### Creating Customizable Components 69 | 70 | Popup.svelte 71 | ```svelte 72 | 75 |
76 |
78 | 86 | ``` 87 | 88 | 89 | Usage.svelte 90 | ```svelte 91 | 94 | 95 | 96 | 97 | 105 | ``` 106 | # Install 107 | 108 | ### **Svelte 5** is required, but it is compatible with both rune and legacy syntaxes. 109 | 110 | 1) Add `svelte-css-rune` as devDependency. Use the appropriate command for your package manager: 111 | ```bash 112 | npm install --save-dev svelte-css-rune 113 | ``` 114 | ``` 115 | bun add --dev svelte-css-rune 116 | ``` 117 | ```bash 118 | yarn add --dev svelte-css-rune 119 | ``` 120 | ```bash 121 | pnpm add -D svelte-css-rune 122 | ``` 123 | 2) Add the preprocessor to your Svelte configuration. This is usually in `svelte.config.js`/`ts`, but can also be in `rollup.config.js`/`ts` or `vite.config.js`/`ts`. SvelteKit uses a `svelte.config.js`/`ts` file. 124 | ```javascript 125 | import cssRune from "svelte-css-rune"; 126 | export default { 127 | preprocess: cssRune(), 128 | // Rest of the config 129 | } 130 | ``` 131 | If you are using other preprocessors, such as `svelte-preprocess`, you can pass an array of preprocessors. 132 | 133 | **The order is important**: `svelte-css-rune` should be the **LAST** one in the array." 134 | ```javascript 135 | import cssRune from "svelte-css-rune"; 136 | import preprocess from "svelte-preprocess"; 137 | export default { 138 | preprocess: [preprocess(), cssRune()], 139 | // Rest of the config 140 | } 141 | ``` 142 | 143 | 3) You can pass options to the preprocessor. For a list of options see the [Options](#Options) section. 144 | ```javascript 145 | import cssRune from "svelte-css-rune"; 146 | export default { 147 | preprocess: cssRune({ 148 | mixedUseWarnings: true, 149 | increaseSpecificity: true 150 | }), 151 | // Rest of the config 152 | } 153 | ``` 154 | 4) Use the `$css` rune in your components. 155 | 156 | 157 | See the [Typescript](#Typescript) section for typescript support. 158 | You can find a svelte kit example in the [example](example) folder. 159 | 160 | # Options 161 | 162 | The preprocessor can be configured with the following options: 163 | 164 | - `mixedUseWarnings` (default: `"use"`): Emit warnings when a class is used with the $css rune and without it. Setting this to `true` will warn on mixed usage in script tags, markup and when defining mixed css rules. Setting it to `"use"` will not warn when defining mixed css rules. Setting it to `false` will disable all warnings. 165 | 166 | - `hash` can be used to override the hash function. Expects a function that takes a string and returns a string. The default hash function is the same svelte uses. 167 | 168 | - `increaseSpecificity` if true the generated class will be a combined class selector that has higher specificity then svelte native class selectors. 169 | Set this to true if you want to override local styles with rune styles. 170 | 171 | # How it works and advanced usage 172 | 173 | The `$css` rune is a function that takes a **string literal** as an argument. The rune is replaced with a unique class name that is generated by the preprocessor. This class name is unique to the file and the original name, preventing naming conflicts when passing styles between components. It modifies class names within style tags to match the generated names and utilizes the `:global` selector to make these generated classes globally accessible. It only affects classes that are referenced with the `$css` rune. Classes used both with the `$css` rune and natively (i.e., directly within the class attribute without the rune) are duplicated. This should be avoided as it results in larger bundle sizes and can potentially cause issues. The preprocessor will warn you if such an issue ever occurs. 174 | 175 | ## Usage 176 | You can use the `$css` rune inside script tags, script module tags, and within the markup. It integrates seamlessly with all Svelte style features, including the new `clsx` integration. It's statically replaced with the generated class name. The content of the `$css` rune must be a string literal; unquoted strings are not supported. The preprocessor will issue a warning if the `$css`rune is used in an unsupported way. 177 | 178 | ```svelte 179 | 182 | 186 | 187 |
188 | 189 | 190 |
191 | 192 |
193 | 194 | 195 | 199 | 200 | 201 | 202 | 203 | 204 | 221 | ``` 222 | 223 | ### Errors and Warnings 224 | 225 | This preprocessor does not interfere with or disable Svelte's unused class warnings. It will produce an error if the `$css` rune is misused or references a non-existent class. Error messages are descriptive and pinpoint the exact location of the issue. 226 | 227 | ``` 228 | /example/Component.svelte 229 | 230 | 202| }); 231 | 203| 232 | 204| const className = $css("i-dont-exist") 233 | ^^^^^^^^^^^^^^^^^^^^ 234 | class i-dont-exist is not defined 235 | 236 | ``` 237 | 238 | 239 | ## Example Transpilation 240 | 241 | Consider a component with mixed usage like this: 242 | 243 | ```svelte 244 |
245 | 246 |
247 | 248 |