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 |
196 |
197 |
198 |
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 |
249 |
257 | ```
258 |
259 | All styles just work! This is compiled to:
260 |
261 | ```svelte
262 |
263 |
264 |
265 |
266 |
267 |
275 | ```
276 | Note the random string appended to the class names. This ensures unique class names. It's generated based on the path and content of the component.
277 |
278 | # Typescript
279 |
280 | This library provides full TypeScript support. It provides a global declaration for the `$css` rune. If this is not working automatically for your setup
281 | you should reference this package in a .d.ts file in your project. The default SvelteKit app comes with src/app.d.ts.
282 |
283 | Simply add:
284 |
285 | ```typescript
286 | ///
287 |
288 | ```
289 | to the top of this file.
290 |
291 | Alternatively, you can add this package to the `types` field in your `tsconfig.json`, or add
292 |
293 | ```typescript
294 | import type {} from "svelte-css-rune";
295 |
296 | ```
297 | to every file where the rune is used.
298 |
299 |
300 | # Edge Cases
301 |
302 | Edge cases will only occur when mixing `$css` rune with native class usage. The preprocessor will emit a warning if mixed usage is detected. It will fail if it detects an edge case that it cannot handle.
303 |
304 | ## Warning: Mixed usage of $css rune and native class
305 |
306 | __If you adhere to this warning you will not encounter any of these issues.__
307 |
308 |
309 |
310 | The preprocessor will emit a warning if it detects mixed usage of the `$css` rune and native class usage. This is not recommended, as it can lead to larger bundle sizes and potential issues. This warning can be safely ignored if you are aware of the implications and can be suppressed with the `mixedUseWarnings` option.
311 | ### Note: The following edge cases is unlikely to affect most users. It's included for completeness and transparency. If you haven't been directed here by a warning from the preprocessor, you can likely skip this section.
312 |
313 | ## Rules with multiple native and $css rune classes (mixed usage only)
314 |
315 | Svelte has a limitation on global selectors. Global selectors need to be at the beginning or end of a selector list. This means that a rule like this:
316 |
317 | ```css
318 | // NOT OK
319 | .used-with-rune .used-natively .used-with-rune .used-natively {
320 | color: red;
321 | }
322 | ```
323 | will not work. The preprocessor will detect this and issue a warning. Non class selectors count as native selectors.
324 | ```css
325 | // NOT OK
326 | .used-with-rune #some-id .used-with-rune div{
327 | color: red;
328 | }
329 | ```
330 | If you use :global in the selector you might not get an error from the preprocessor, but from the svelte compiler.
331 |
332 | ```css
333 | // OK
334 | .used-with-rune #some-id .used-with-rune :global(div){
335 | color: red;
336 | }
337 | // NOT OK
338 | .used-with-rune #some-id .used-with-rune :global(div) .used-natively{
339 | color: red;
340 | }
341 | ```
342 | All combinations that respect this will compile correctly.
343 |
344 | ```css
345 | // OK
346 | .used-with-rune .used-with-rune .used-natively #used-natively .used-with-rune .used-rune {
347 | color: red;
348 | }
349 | ```
350 |
351 | ## Dynamic class names (mixed usage only)
352 |
353 | The preprocessors only detects native usage of the class name if known at compile time. If you use a dynamic class name, the preprocessor will not detect it. If you use a class name dynamically and with the $css rune, the preprocessor not duplicate the rule. The dynamic class name will not work anymore.
354 |
355 | ```svelte
356 | // NOT OK
357 |
361 |
362 |
363 | ```
364 | This will cause the second div to not have the correct styles. Avoid mixing dynamic class names with the $css rune.
365 | The preprocessor can handle dynamic class names if they are defined inside the element.
366 |
367 | ```svelte
368 | // OK
369 |
372 |
373 |
374 |
375 |
376 | ```
377 |
378 |
379 | ## Building and Testing
380 |
381 | ### Building
382 |
383 | You can build this library using either Node.js or Bun via the build script. It compiles to both ESM and CommonJS formats.
384 | ```bash
385 | npm run build
386 | ```
387 | ```bash
388 | bun run build
389 | ```
390 | ### Tests
391 | Bun is required to run the tests.
392 | ```bash
393 | bun test
394 | ```
395 |
396 | This library contains end-to-end tests that verify the functionality of the preprocessor. The transform tests executes the generated svelte components and makes sure the styles are applied correctly.
397 |
398 | The walk tests check all stages of the preprocessor to avoid regressions.
399 |
400 | All errors and warnings are tested.
401 |
402 |
403 | # Comparison to svelte-preprocess-cssmodules
404 |
405 | [`svelte-preprocess-cssmodules`](https://github.com/micantoine/svelte-preprocess-cssmodules) can be used to archive something similar. However, its primary goal is to provide CSS Modules support, not solely a mechanism for passing classes between components.
406 |
407 | It generates unique class names for every class within a style tag, transforming all styles in a file to use these generated names. This involves parsing and replacing a significant portion of your code, essentially replacing Svelte's built-in style handling. It also treats the `class` prop as a special, magical property, and adding other attributes requires global configuration for all components. Furthermore, it disables Svelte's unused class warnings.
408 |
409 | `svelte-css-rune` is a significantly simpler library. It only replaces the `$css` rune and the referenced class with a unique class name, leaving other styles untouched.
410 | It aims for simplicity and a seamless integration with the rest of the Svelte 5 syntax. It does not disable Svelte's unused class warnings.
411 |
412 | `svelte-preprocess-cssmodules` is a great library if you require more extensive features. This library draws significant inspiration from it.
413 |
414 | While I initially created a pull request to add this feature to `svelte-preprocess-cssmodules`, I decided to create this separate library for a more focused and simpler approach.
415 |
416 | # License
417 |
418 | [MIT](https://opensource.org/licenses/MIT)
419 |
--------------------------------------------------------------------------------
/lib/walk.ts:
--------------------------------------------------------------------------------
1 | import { walk, type Visitor } from "zimmerframe";
2 | import { type AST } from "svelte/compiler";
3 | import MagicString from "magic-string";
4 | import { printBelow, printLocation } from "./error.js";
5 | import { RESTRICTED_RULES } from "./rules.js";
6 |
7 | const RUNE_NAME = "$css";
8 |
9 | type NodeOf = X extends { type: T } ? X : never;
10 | type FragmentTypes = {
11 | [K in T["type"]]: NodeOf;
12 | };
13 | type SvelteFragments = FragmentTypes;
14 |
15 | const processClasses = (classNames: string) => {
16 | const classes: string[] = classNames.split(" ");
17 | return classes;
18 | };
19 | const buildError = (node: any, message: string, detail: string) => {
20 | const err = new Error(message);
21 | (err as any).start = node.start;
22 | (err as any).end = node.end;
23 | (err as any).detail = `${detail}`;
24 | return err;
25 | };
26 |
27 | /**
28 | * Visitor for function calls that only runs the provided visitor if the function is a $css rune
29 | * Validates the arguments and calls the visitor
30 | * @param visitor What to do with the rune
31 | */
32 | const runeVisitor =
33 | (
34 | visitor: (
35 | node: SvelteFragments["CallExpression"],
36 | value: string,
37 | state: T
38 | ) => T
39 | ): Visitor =>
40 | (node, ctx) => {
41 | if (node.callee.type === "Identifier" && node.callee.name === RUNE_NAME) {
42 | if (node.arguments.length !== 1 || node.arguments[0].type !== "Literal") {
43 | throw buildError(
44 | node,
45 | "Invalid $css call",
46 | "$css must have exactly one argument"
47 | );
48 | }
49 | const literal = node.arguments[0] as SvelteFragments["Literal"];
50 | if (typeof literal!.value == "string") {
51 | const res = visitor(node, literal!.value, ctx.state);
52 | ctx.next(res);
53 | return;
54 | } else {
55 | throw buildError(
56 | node,
57 | "Invalid $css call",
58 | "$css argument must be a string"
59 | );
60 | }
61 | }
62 | ctx.next(ctx.state);
63 | };
64 |
65 | /**
66 | * Find which classes are used in the svelte file
67 | * Split into classes that are used with the $css rune and classes that are used natively
68 | * @returns {classes, usedClasses} classes: used with rune, usedClasses: used without rune
69 | */
70 |
71 | export const findReferencedClasses = (ast: AST.Root) => {
72 | const classes = new Map();
73 | const usedClasses = new Map();
74 | const addClass = (
75 | node: SvelteFragments["CallExpression"],
76 | value: string,
77 | state: { inClass?: boolean }
78 | ) => {
79 | const values = processClasses(value);
80 | values.forEach((val) =>
81 | classes.set(val, { start: (node as any).start, end: (node as any).end })
82 | );
83 | return { inClass: false };
84 | };
85 | walk(
86 | ast.fragment,
87 | {},
88 | {
89 | _: (node, { next, state }) => {
90 | next(state);
91 | },
92 | CallExpression: runeVisitor(addClass),
93 | ClassDirective: (node, { next, state }) => {
94 | usedClasses.set(node.name, { start: node.start, end: node.end });
95 | next(state);
96 | },
97 | Attribute: (node, { next, state }) => {
98 | if (node.name === "class") {
99 | if (
100 | Array.isArray(node.value) &&
101 | node.value.length === 1 &&
102 | node.value[0].type === "Text"
103 | ) {
104 | const values = processClasses(node.value[0].data);
105 | values.forEach((val) =>
106 | usedClasses.set(val, { start: node.start, end: node.end })
107 | );
108 | next({ inClass: false });
109 | } else {
110 | next({ inClass: true });
111 | }
112 | } else {
113 | next(state);
114 | }
115 | },
116 | ObjectExpression: (node, { next, state }) => {
117 | node.properties.forEach((prop) => {
118 | if (prop.type === "Property") {
119 | const key = prop.key;
120 | if (key.type === "Literal") {
121 | usedClasses.set(key.value?.toString() || "", {
122 | start: (node as any).start,
123 | end: (node as any).end,
124 | });
125 | }
126 | if (key.type === "Identifier") {
127 | usedClasses.set(key.name, {
128 | start: (node as any).start,
129 | end: (node as any).end,
130 | });
131 | }
132 | }
133 | });
134 | next({ inClass: false });
135 | },
136 | Literal: (node, { next, state }) => {
137 | if (state.inClass) {
138 | const values = processClasses((node as any).value);
139 | values.forEach((val) =>
140 | usedClasses.set(val, {
141 | start: (node as any).start,
142 | end: (node as any).end,
143 | })
144 | );
145 | }
146 | next(state);
147 | },
148 | }
149 | );
150 | if (ast.instance) {
151 | walk(
152 | ast.instance as any,
153 | {},
154 | {
155 | _: (node, { next }) => {
156 | next({});
157 | },
158 | CallExpression: runeVisitor(addClass),
159 | }
160 | );
161 | }
162 | if (ast.module) {
163 | walk(
164 | ast.module as any,
165 | {},
166 | {
167 | _: (node, { next }) => {
168 | next({});
169 | },
170 | CallExpression: runeVisitor(addClass),
171 | }
172 | );
173 | }
174 | return { classes, usedClasses };
175 | };
176 |
177 | /**
178 | * Group following global selectors that are chained like .class1.class2
179 | */
180 | const groupChained = (
181 | selectors: T[]
182 | ): T[][] => {
183 | const groups: T[][] = [];
184 | let current: T[] = [];
185 | let currentChain = 0;
186 |
187 | selectors.forEach((selector) => {
188 | if ((selector as any).chain === undefined) {
189 | groups.push([selector]);
190 | return;
191 | }
192 | if ((selector as any).chain === currentChain) {
193 | current.push(selector);
194 | } else {
195 | current = [selector];
196 | groups.push(current);
197 | currentChain = (selector as any).chain;
198 | }
199 | });
200 | return groups;
201 | };
202 |
203 | type SelectorInfo = {
204 | start: number;
205 | end: number;
206 | original: string;
207 | transformed: string;
208 | chain?: number;
209 | };
210 |
211 | /**
212 | * Group following global selectors that are chained like .class1.class2
213 | * Expects a list that indicates if the a selector will be transformed
214 | */
215 | const groupPermutations = (
216 | selectors: SelectorInfo[],
217 | permutations: boolean[]
218 | ) => {
219 | const groups: SelectorInfo[][] = [];
220 | let current: SelectorInfo[] = [];
221 | let currentChain = 0;
222 | // keep track of the last character we have seen, to split on selectors we don't transform
223 | let lastChar = 0;
224 | selectors.forEach((selector, idx) => {
225 | if ((selector as any).chain === undefined || !permutations[idx]) {
226 | groups.push([selector]);
227 | currentChain = 0;
228 | lastChar = selector.end;
229 | return;
230 | }
231 | if (
232 | selector.chain === currentChain &&
233 | selector.start == lastChar &&
234 | permutations[idx]
235 | ) {
236 | current.push(selector);
237 | } else {
238 | if (permutations[idx]) {
239 | current = [selector];
240 | groups.push(current);
241 | currentChain = (selector as any).chain;
242 | } else {
243 | groups.push([selector]);
244 | currentChain = 0;
245 | }
246 | }
247 | lastChar = selector.end;
248 | });
249 | return groups;
250 | };
251 |
252 | /**
253 | * Create all possible permutations of the selectors
254 | *
255 | * Unfortunately the svelte compiler does only allow global selectors at the beginning or end of a selector list.
256 | * This function is capable of creating all possible permutations should this limitation be removed in the future.
257 | *
258 | * ToDo: Analyze possible selector combinations and remove unreachable groups
259 | *
260 | * @param content selector list as string
261 | * @param selectors information about varying selectors
262 | * @returns a string containing all possible permutations of the selectors
263 | */
264 | const createPermutations = (
265 | content: string,
266 | selectors: SelectorInfo[],
267 | addRuneClasses: string[]
268 | ) => {
269 | const count = selectors.length;
270 | const permutations: boolean[][] = [];
271 | for (let i = 0; i < 2 ** count; i++) {
272 | const permutation = i
273 | .toString(2)
274 | .padStart(count, "0")
275 | .split("")
276 | .map((val) => val === "1");
277 | permutations.push(permutation);
278 | }
279 | const rules: string[] = [];
280 | // start with global selectors, so we don'r run into the the max depth of svelte dead code elimination
281 | permutations.reverse();
282 | permutations.forEach((permutation, i) => {
283 | const magicRule = new MagicString(content);
284 | const groups = groupPermutations(selectors, permutation);
285 | let idx = 0;
286 | groups.forEach((group) => {
287 | const transformGroup = permutation[idx];
288 | const groupStart = group[0].start;
289 | const groupEnd = group[group.length - 1].end;
290 | let groupVal = new MagicString(content.substring(groupStart, groupEnd));
291 | if (transformGroup) {
292 | group.forEach((val) => {
293 | if (permutation[idx] != transformGroup) {
294 | // groups need to consist of classes that are all transformed or all not transformed
295 | throw new Error("Invalid permutation");
296 | }
297 | groupVal.overwrite(
298 | val.start - groupStart,
299 | val.end - groupStart,
300 | val.transformed
301 | );
302 | idx++;
303 | });
304 | magicRule.overwrite(
305 | groupStart,
306 | groupEnd,
307 | `:global(${
308 | addRuneClasses?.length > 0
309 | ? addRuneClasses.map((c) => "." + c).join("")
310 | : ""
311 | }${groupVal.toString()})`
312 | );
313 | }
314 | });
315 | rules.push(magicRule.toString());
316 | });
317 | return rules.join(",");
318 | };
319 |
320 | type PlacedClass = {
321 | name: string;
322 | start: number;
323 | end: number;
324 | kind: ClassPlacement;
325 | };
326 | type ClassPlacement = "native" | "rune" | "mixed";
327 |
328 | /**
329 | * Validate that we can compile the selector to valid svelte global selectors
330 | * Limitation: Svelte only allows global selectors at the beginning or end of a selector list.
331 | * @param otherSelectors All selectors that are used by this rule and are not used by the $css rune
332 | * @param runeClasses All selectors that are used by the selector and the $css rune
333 | * @param nativeClasses All classes that are used natively
334 | * @param start start of the selector list, for error reporting
335 | * @param end end of the selector list, for error reporting
336 | */
337 | const validateClassPlacement = (
338 | otherSelectors: AST.BaseNode[],
339 | runeClasses: SvelteFragments["ClassSelector"][],
340 | nativeClasses: Map,
341 | source: string,
342 | start: number,
343 | end: number
344 | ) => {
345 | const error = (cause: PlacedClass) => {
346 | const e = new Error(
347 | "Invalid class placement. Svelte only allows global classes at the beginning or end of a selector list."
348 | );
349 | (e as any).start = start;
350 | (e as any).end = end;
351 | (
352 | e as any
353 | ).detail = `Contains a selector that is used by runes and is not in the beginning or end of the selector list. \n\n`;
354 | if (cause.kind === "mixed") {
355 | (
356 | e as any
357 | ).detail += `The class "${cause.name}" is used with the $css rune and natively. It can only be used the first or last selector.\n`;
358 | (
359 | e as any
360 | ).detail += `Consider using the $css rune for all references to this class "${cause.name}".`;
361 | }
362 | if (cause.kind === "rune") {
363 | (
364 | e as any
365 | ).detail += `The class "${cause.name}" is used with the $css rune. Other selectors in this rule are used natively or mixed. Classes that are used with the $css rune must be in the beginning or end of the selector.`;
366 | (e as any).detail += `.rune .rune .native .native .rune // OK`;
367 | (e as any).detail += `.rune .native .rune .native .rune // NOT OK`;
368 | }
369 | (e as any).detail += `\n`;
370 | (
371 | e as any
372 | ).detail += `For more information see: https://github.com/JanNitschke/svelte-css-rune#edge-cases`;
373 |
374 | throw e;
375 | };
376 |
377 | const selectors: PlacedClass[] = otherSelectors.map((node) => ({
378 | name: (node as any).name,
379 | start: node.start,
380 | end: node.end,
381 | kind: "native",
382 | }));
383 | runeClasses.forEach((node) =>
384 | selectors.push({
385 | name: node.name,
386 | start: node.start,
387 | end: node.end,
388 | kind: nativeClasses.has(node.name) ? "mixed" : "rune",
389 | })
390 | );
391 | selectors.sort((a, b) => a.start - b.start);
392 |
393 | let isInStart = true; // has only seen global classes
394 | let isInEnd = false; // has seen a native class, can only be followed by global classes
395 | let lastEnd = selectors[0].start;
396 | let cause: PlacedClass | null = null;
397 |
398 | for (let i = 0; i < selectors.length; i++) {
399 | // Reset if we are in a chain
400 | if (!RESTRICTED_RULES) {
401 | const textBetween = source.substring(lastEnd, selectors[i].start);
402 | if (textBetween.includes(",")) {
403 | isInStart = true;
404 | isInEnd = false;
405 | }
406 | lastEnd = selectors[i].end;
407 | }
408 | const selector = selectors[i];
409 | cause = selector.kind === "native" ? cause : selector;
410 | // mixed can not be surrounded
411 | if (selector.kind === "mixed" && i !== 0 && i !== selectors.length - 1) {
412 | error(selector);
413 | }
414 | if (selector.kind === "mixed" || selector.kind === "native") {
415 | if (isInStart) {
416 | isInStart = false;
417 | continue;
418 | }
419 | }
420 | if ((!isInStart && selector.kind === "rune") || selector.kind === "mixed") {
421 | isInEnd = true;
422 | }
423 | if (selector.kind === "native" && isInEnd) {
424 | // we have seen a native class followed by a rune class, followed by a native class
425 | error(cause ?? selector);
426 | }
427 | }
428 | };
429 |
430 | const warnMixedUsage = (
431 | node: AST.BaseNode,
432 | filename: string | undefined,
433 | content: string,
434 | runeUsed: string[],
435 | runeNotUsed: string[]
436 | ) => {
437 | let warning = `[css rune]: This selector uses a combination of classes that are used with runes.`;
438 | const selLoc = printLocation(filename, content, node.start, node.end);
439 | warning += "\n\n" + selLoc.text;
440 | warning += printBelow(
441 | "combines selectors that are used with and without the $css rune",
442 | selLoc.startColumn
443 | );
444 | if (runeUsed.length > 1) {
445 | warning += `\n\nThe classes ${runeUsed.join(
446 | ", "
447 | )} are used with the $css rune.`;
448 | } else {
449 | warning += `\n\nThe class ${runeUsed[0]} is used with the $css rune.`;
450 | }
451 | if (runeNotUsed.length > 1) {
452 | warning += `\nThe selectors ${runeNotUsed.join(
453 | ", "
454 | )} are not used with the $css rune.`;
455 | } else {
456 | warning += `\nThe selector ${runeNotUsed[0]} is not used with the $css rune.`;
457 | }
458 | warning +=
459 | 'You can suppress this warning by setting the `mixedUseWarnings` to `false` or `"use"`.\n';
460 | warning +=
461 | "\nMore Information: https://github.com/JanNitschke/svelte-css-rune#edge-cases\n";
462 | console.warn(warning);
463 | };
464 |
465 | /**
466 | *
467 | * replace used classes with a global classname
468 | * duplicate the class if its used in the svelte file
469 | *
470 | * @param ast
471 | * @param source
472 | * @param magicContent
473 | * @param globalClasses All classes where referenced by the rune and need to bo global
474 | * @param usedClasses All classes that are used in the svelte file and need to available
475 | * @param hash A hash that is unique to the file
476 | * @param fileName File name for error reporting
477 | * @param fileContent Content of the file for error reporting
478 | * @param emitWarnings If warnings should be emitted
479 | *
480 | * @returns
481 | */
482 | export const transformCSS = (
483 | ast: AST.Root,
484 | source: string,
485 | magicContent: MagicString,
486 | globalClasses: Map,
487 | usedClasses: Map,
488 | hash: string,
489 | fileName: string | undefined,
490 | fileContent: string,
491 | emitWarnings: boolean,
492 | addRuneClasses: string[]
493 | ): Record => {
494 | const transformedClasses: Record = {};
495 | if (!ast.css) {
496 | return transformedClasses;
497 | }
498 |
499 | let ruleChangedClasses: SvelteFragments["ClassSelector"][] = [];
500 | let ruleUnchangedSelectors: AST.BaseNode[] = [];
501 | let ruleChained = false; // detect .class1.class1 selectors to wrap them into a single global class
502 | let chain = 0;
503 |
504 | walk(
505 | ast.css,
506 | {},
507 | {
508 | _: (node, { next, state }) => {
509 | next(state);
510 | },
511 | Rule: (node, { next, state }) => {
512 | ruleChangedClasses = [];
513 | ruleUnchangedSelectors = [];
514 | //walk all children to find all classes
515 | next(state);
516 | const selectors = node.prelude;
517 |
518 | if (ruleChangedClasses.length > 0) {
519 | validateClassPlacement(
520 | ruleUnchangedSelectors,
521 | ruleChangedClasses,
522 | usedClasses,
523 | source,
524 | selectors.start,
525 | selectors.end
526 | );
527 | if (ruleChangedClasses.some((val) => usedClasses.has(val.name))) {
528 | const start = selectors.start;
529 | const selectorString = source.substring(
530 | selectors.start,
531 | selectors.end
532 | );
533 | const classMap: SelectorInfo[] = ruleChangedClasses.map((val) => ({
534 | start: val.start - start,
535 | end: val.end - start,
536 | original: val.name,
537 | chain: (val as any).chain,
538 | transformed: `.${transformedClasses[val.name]}`,
539 | }));
540 | const permutations = createPermutations(
541 | selectorString,
542 | classMap,
543 | addRuneClasses
544 | );
545 | console.log("permutations", permutations);
546 | magicContent.overwrite(
547 | selectors.start,
548 | selectors.end,
549 | permutations
550 | );
551 | } else {
552 | const groups = groupChained(
553 | ruleChangedClasses as (SvelteFragments["ClassSelector"] & {
554 | chain: undefined;
555 | })[]
556 | );
557 | groups.forEach((group) => {
558 | let start = group[0].start;
559 | let end = group[group.length - 1].end;
560 | group.forEach((val) => {
561 | const transformed = transformedClasses[val.name];
562 | magicContent.overwrite(val.start, val.end, `.${transformed}`);
563 | });
564 | magicContent.appendLeft(
565 | start,
566 | `:global(${
567 | addRuneClasses?.length > 0
568 | ? addRuneClasses.map((c) => "." + c).join("")
569 | : ""
570 | }`
571 | );
572 | magicContent.appendRight(end, ")");
573 | });
574 | // ruleChangedClasses.forEach((val) => {
575 | // const transformed = transformedClasses[val.name];
576 | // magicContent.overwrite(val.start, val.end, `:global(.${transformed})`);
577 | // });
578 | }
579 | if (emitWarnings) {
580 | if (ruleUnchangedSelectors.length > 0) {
581 | warnMixedUsage(
582 | selectors,
583 | fileName,
584 | fileContent,
585 | ruleChangedClasses.map((val) => val.name),
586 | ruleUnchangedSelectors.map((val) => (val as any).name)
587 | );
588 | }
589 | }
590 | }
591 | // reset classes
592 | ruleChangedClasses = [];
593 | ruleUnchangedSelectors = [];
594 | },
595 | RelativeSelector: (node, { next, state }) => {
596 | if (node.selectors.length > 1) {
597 | ruleChained = true;
598 | chain++;
599 | next(state);
600 | ruleChained = false;
601 | } else {
602 | next(state);
603 | }
604 | },
605 | ClassSelector: (node, { next, state }) => {
606 | const name = node.name;
607 | if (ruleChained) {
608 | (node as any).chain = chain;
609 | }
610 | if (globalClasses.has(name)) {
611 | ruleChangedClasses.push(node);
612 | const transformed = `${name}-${hash}`;
613 | transformedClasses[name] = transformed;
614 | } else {
615 | ruleUnchangedSelectors.push(node);
616 | }
617 | next(state);
618 | },
619 | AttributeSelector: (node, { next, state }) => {
620 | ruleUnchangedSelectors.push(node);
621 | next(state);
622 | },
623 | IdSelector: (node, { next, state }) => {
624 | ruleUnchangedSelectors.push(node);
625 | next(state);
626 | },
627 | TypeSelector: (node, { next, state }) => {
628 | ruleUnchangedSelectors.push(node);
629 | next(state);
630 | },
631 | }
632 | );
633 | return transformedClasses;
634 | };
635 |
636 | /**
637 | * Replace all $css calls with the transformed classes
638 | * Throws an error if a rune references a class that is not defined
639 | * @param ast Ast of the svelte file
640 | * @param magicContent A magic string of the content of the whole file
641 | * @param classes A map of all classes that are with the rune and their position for error reporting
642 | * @param classNames An object that maps the original class name to the transformed class name
643 | */
644 | export const transformRunes = (
645 | ast: AST.Root,
646 | magicContent: MagicString,
647 | classes: Map,
648 | classNames: Record,
649 | addRuneClasses: string[]
650 | ) => {
651 | const replaceClass = (
652 | node: SvelteFragments["CallExpression"],
653 | value: string,
654 | state: T
655 | ) => {
656 | const values = processClasses(value);
657 | const transformed = values.map((val) => {
658 | if (classNames[val]) {
659 | return classNames[val];
660 | } else {
661 | const call = classes.get(val);
662 | throw buildError(
663 | { start: call?.start, end: call?.end },
664 | "Invalid $css call",
665 | `class ${val} is not defined`
666 | );
667 | }
668 | });
669 | const transformedValue = transformed.join(" ");
670 | const positions = node as unknown as AST.BaseNode;
671 | magicContent.overwrite(
672 | positions.start,
673 | positions.end,
674 | `"${
675 | addRuneClasses?.length > 0 ? addRuneClasses.join(" ") + " " : ""
676 | }${transformedValue}"`
677 | );
678 | return state;
679 | };
680 | walk(
681 | ast.fragment,
682 | {},
683 | {
684 | _: (node, { next }) => {
685 | next({});
686 | },
687 | CallExpression: runeVisitor(replaceClass),
688 | }
689 | );
690 | if (ast.instance) {
691 | walk(
692 | ast.instance as any,
693 | {},
694 | {
695 | _: (node, { next }) => {
696 | next({});
697 | },
698 | CallExpression: runeVisitor(replaceClass),
699 | }
700 | );
701 | }
702 | if (ast.module) {
703 | walk(
704 | ast.module as any,
705 | {},
706 | {
707 | _: (node, { next }) => {
708 | next({});
709 | },
710 | CallExpression: runeVisitor(replaceClass),
711 | }
712 | );
713 | }
714 | };
715 |
--------------------------------------------------------------------------------