├── .gitignore ├── dev ├── .gitignore ├── public │ ├── favicon.png │ ├── index.html │ └── global.css ├── src │ ├── main.js │ ├── Square.svelte │ └── App.svelte ├── package.json ├── rollup.config.js ├── README.md ├── scripts │ └── setupTypeScript.js └── package-lock.json ├── package.json ├── README.md └── src └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /dev/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /public/build/ 3 | 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /dev/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srmullen/svelte-reactive-css-preprocess/HEAD/dev/public/favicon.png -------------------------------------------------------------------------------- /dev/src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | const app = new App({ 4 | target: document.body, 5 | props: { 6 | name: 'world' 7 | } 8 | }); 9 | 10 | export default app; -------------------------------------------------------------------------------- /dev/src/Square.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 10 | 17 | 18 | -------------------------------------------------------------------------------- /dev/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Svelte app 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "rollup -c", 7 | "dev": "rollup -c -w", 8 | "start": "sirv public --no-clear" 9 | }, 10 | "devDependencies": { 11 | "@rollup/plugin-commonjs": "^17.0.0", 12 | "@rollup/plugin-node-resolve": "^11.0.0", 13 | "rollup": "^2.3.4", 14 | "rollup-plugin-css-only": "^3.1.0", 15 | "rollup-plugin-livereload": "^2.0.0", 16 | "rollup-plugin-svelte": "^7.0.0", 17 | "rollup-plugin-terser": "^7.0.0", 18 | "svelte": "^3.0.0" 19 | }, 20 | "dependencies": { 21 | "sirv-cli": "^1.0.0", 22 | "svelte-reactive-css-preprocess": "file:.." 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-reactive-css-preprocess", 3 | "version": "0.0.2", 4 | "description": "Svelte preprocessor to automatically create and update css variables using svelte's reactivity", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "svelte", 11 | "preprocess", 12 | "css", 13 | "styles" 14 | ], 15 | "author": "Sean Mullen ", 16 | "license": "ISC", 17 | "dependencies": { 18 | "svelte": "^3.44.0" 19 | }, 20 | "files": [ 21 | "src" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/srmullen/svelte-reactive-css-preprocess.git" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /dev/src/App.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 |
24 |

Hello {name}!

25 |

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

26 | 27 | 28 |
29 | 30 | -------------------------------------------------------------------------------- /dev/public/global.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | body { 8 | color: #333; 9 | margin: 0; 10 | padding: 8px; 11 | box-sizing: border-box; 12 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 13 | } 14 | 15 | a { 16 | color: rgb(0,100,200); 17 | text-decoration: none; 18 | } 19 | 20 | a:hover { 21 | text-decoration: underline; 22 | } 23 | 24 | a:visited { 25 | color: rgb(0,80,160); 26 | } 27 | 28 | label { 29 | display: block; 30 | } 31 | 32 | input, button, select, textarea { 33 | font-family: inherit; 34 | font-size: inherit; 35 | -webkit-padding: 0.4em 0; 36 | padding: 0.4em; 37 | margin: 0 0 0.5em 0; 38 | box-sizing: border-box; 39 | border: 1px solid #ccc; 40 | border-radius: 2px; 41 | } 42 | 43 | input:disabled { 44 | color: #ccc; 45 | } 46 | 47 | button { 48 | color: #333; 49 | background-color: #f4f4f4; 50 | outline: none; 51 | } 52 | 53 | button:disabled { 54 | color: #999; 55 | } 56 | 57 | button:not(:disabled):active { 58 | background-color: #ddd; 59 | } 60 | 61 | button:focus { 62 | border-color: #666; 63 | } 64 | -------------------------------------------------------------------------------- /dev/rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import livereload from 'rollup-plugin-livereload'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import css from 'rollup-plugin-css-only'; 7 | import reactiveCSSPreprocess from 'svelte-reactive-css-preprocess'; 8 | 9 | const production = !process.env.ROLLUP_WATCH; 10 | 11 | function serve() { 12 | let server; 13 | 14 | function toExit() { 15 | if (server) server.kill(0); 16 | } 17 | 18 | return { 19 | writeBundle() { 20 | if (server) return; 21 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { 22 | stdio: ['ignore', 'inherit', 'inherit'], 23 | shell: true 24 | }); 25 | 26 | process.on('SIGTERM', toExit); 27 | process.on('exit', toExit); 28 | } 29 | }; 30 | } 31 | 32 | export default { 33 | input: 'src/main.js', 34 | output: { 35 | sourcemap: true, 36 | format: 'iife', 37 | name: 'app', 38 | file: 'public/build/bundle.js' 39 | }, 40 | plugins: [ 41 | svelte({ 42 | compilerOptions: { 43 | // enable run-time checks when not in production 44 | dev: !production 45 | }, 46 | preprocess: [ 47 | reactiveCSSPreprocess() 48 | ] 49 | }), 50 | // we'll extract any component CSS out into 51 | // a separate file - better for performance 52 | css({ output: 'bundle.css' }), 53 | 54 | // If you have external dependencies installed from 55 | // npm, you'll most likely need these plugins. In 56 | // some cases you'll need additional configuration - 57 | // consult the documentation for details: 58 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 59 | resolve({ 60 | browser: true, 61 | dedupe: ['svelte'] 62 | }), 63 | commonjs(), 64 | 65 | // In dev mode, call `npm run start` once 66 | // the bundle has been generated 67 | !production && serve(), 68 | 69 | // Watch the `public` directory and refresh the 70 | // browser on changes when not in production 71 | !production && livereload('public'), 72 | 73 | // If we're building for production (npm run build 74 | // instead of npm run dev), minify 75 | production && terser() 76 | ], 77 | watch: { 78 | clearScreen: false 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | svelte-reactive-css-preprocess 2 | ============================== 3 | 4 | [![npm package](https://img.shields.io/npm/v/svelte-reactive-css-preprocess)](https://www.npmjs.com/package/svelte-reactive-css-preprocess) 5 | 6 | Have you ever wished you could use your svelte variables in your component's styles. Now you can! 7 | 8 | ### Installation 9 | 10 | `npm install --save-dev svelte-reactive-css-preprocess` 11 | 12 | ### Usage 13 | 14 | In your svelte config 15 | 16 | ```javascript 17 | import reactiveCSSPreprocessor from 'svelte-reactive-css-preprocess'; 18 | 19 | svelte({ 20 | preprocess: [ 21 | reactiveCSSPreprocessor() 22 | ] 23 | }) 24 | ``` 25 | 26 | If you're using [svelte-preprocess](https://github.com/sveltejs/svelte-preprocess) you need to run `svelte-reactive-css-preprocess` after all tasks for `svelte-preproccess complete. To do that use ['svelte-sequential-preprocessor'](https://github.com/pchynoweth/svelte-sequential-preprocessor). 27 | 28 | `npm install --save-dev svelte-sequential-preprocessor.` 29 | 30 | ```javascript 31 | import reactiveCSSPreprocessor from 'svelte-reactive-css-preprocess'; 32 | import sveltePreprocess from 'svelte-preprocess'; 33 | import seqPreprocess from 'svelte-sequential-preprocessor'; 34 | 35 | svelte({ 36 | preprocess: seqPreprocess([ 37 | sveltePreprocess({ 38 | defaults: { 39 | style: "postcss", 40 | }, 41 | postcss: true 42 | }), 43 | reactiveCSSPreprocess() 44 | ]) 45 | }) 46 | ``` 47 | 48 | Now in your component's style you can reference the reactive variables using css variable syntax. 49 | 50 | ```html 51 | 58 | 59 |
60 | 61 | 69 | ``` 70 | 71 | Now your styles update when your variables do! 72 | 73 | ### How it works 74 | 75 | The preprocessor reads through the variables in each component's script and style tags. If a variable name appears in both the script and styles then a css variables that is scoped to the component is created and added to the `:root` pseudo-selector. In the component the css variables are replaced with the scoped variables. Variable scoping works similarly to how Svelte handles css scoping. The style tag for the above example would end up looking something like this... 76 | 77 | ```html 78 | 90 | ``` 91 | 92 | In the script tag code is injected that handles updating the scoped css variables using Svelte's reactivity. -------------------------------------------------------------------------------- /dev/README.md: -------------------------------------------------------------------------------- 1 | *Psst — looking for a more complete solution? Check out [SvelteKit](https://kit.svelte.dev), the official framework for building web applications of all sizes, with a beautiful development experience and flexible filesystem-based routing.* 2 | 3 | *Looking for a shareable component template instead? You can [use SvelteKit for that as well](https://kit.svelte.dev/docs#packaging) or the older [sveltejs/component-template](https://github.com/sveltejs/component-template)* 4 | 5 | --- 6 | 7 | # svelte app 8 | 9 | This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template. 10 | 11 | To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit): 12 | 13 | ```bash 14 | npx degit sveltejs/template svelte-app 15 | cd svelte-app 16 | ``` 17 | 18 | *Note that you will need to have [Node.js](https://nodejs.org) installed.* 19 | 20 | 21 | ## Get started 22 | 23 | Install the dependencies... 24 | 25 | ```bash 26 | cd svelte-app 27 | npm install 28 | ``` 29 | 30 | ...then start [Rollup](https://rollupjs.org): 31 | 32 | ```bash 33 | npm run dev 34 | ``` 35 | 36 | Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes. 37 | 38 | By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`. 39 | 40 | If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense. 41 | 42 | ## Building and running in production mode 43 | 44 | To create an optimised version of the app: 45 | 46 | ```bash 47 | npm run build 48 | ``` 49 | 50 | You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com). 51 | 52 | 53 | ## Single-page app mode 54 | 55 | By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere. 56 | 57 | If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json: 58 | 59 | ```js 60 | "start": "sirv public --single" 61 | ``` 62 | 63 | ## Using TypeScript 64 | 65 | This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with: 66 | 67 | ```bash 68 | node scripts/setupTypeScript.js 69 | ``` 70 | 71 | Or remove the script via: 72 | 73 | ```bash 74 | rm scripts/setupTypeScript.js 75 | ``` 76 | 77 | If you want to use `baseUrl` or `path` aliases within your `tsconfig`, you need to set up `@rollup/plugin-alias` to tell Rollup to resolve the aliases. For more info, see [this StackOverflow question](https://stackoverflow.com/questions/63427935/setup-tsconfig-path-in-svelte). 78 | 79 | ## Deploying to the web 80 | 81 | ### With [Vercel](https://vercel.com) 82 | 83 | Install `vercel` if you haven't already: 84 | 85 | ```bash 86 | npm install -g vercel 87 | ``` 88 | 89 | Then, from within your project folder: 90 | 91 | ```bash 92 | cd public 93 | vercel deploy --name my-project 94 | ``` 95 | 96 | ### With [surge](https://surge.sh/) 97 | 98 | Install `surge` if you haven't already: 99 | 100 | ```bash 101 | npm install -g surge 102 | ``` 103 | 104 | Then, from within your project folder: 105 | 106 | ```bash 107 | npm run build 108 | surge public my-project.surge.sh 109 | ``` 110 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { parse, walk } = require('svelte/compiler'); 2 | 3 | // The variable name to inject into components to bind to an html element. Used to check that the reactive statement is running client-side. 4 | const documentBinding = '__reactivecssbinding__'; 5 | 6 | function intersection(arrA, arrB) { 7 | let _intersection = []; 8 | for (let elem of arrB) { 9 | if (arrA.includes(elem)) { 10 | _intersection.push(elem); 11 | } 12 | } 13 | return _intersection; 14 | } 15 | 16 | // https://github.com/sveltejs/svelte/blob/master/src/compiler/compile/utils/hash.ts 17 | function hash(str) { 18 | str = str.replace(/\r/g, ''); 19 | let hash = 5381; 20 | let i = str.length; 21 | 22 | while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i); 23 | return (hash >>> 0).toString(36); 24 | } 25 | 26 | function toCSSVariables(vars) { 27 | let out = ''; 28 | for (let name of vars.variables) { 29 | out += `--${name}-${vars.hash}: inherit;\n`; 30 | } 31 | return out; 32 | } 33 | 34 | function createVariableUpdaters(vars) { 35 | let out = `let ${documentBinding};\n`; 36 | for (let name of vars.variables) { 37 | out += `$: if (${documentBinding}) { 38 | const r = document.querySelector(':root'); 39 | r.style.setProperty('--${name}-${vars.hash}', ${name}); 40 | }\n`; 41 | } 42 | return out; 43 | } 44 | 45 | function createDocumentBinding() { 46 | return ``; 47 | } 48 | 49 | module.exports = function cssUpdatePreprocessor() { 50 | const files = {}; 51 | 52 | return { 53 | markup: ({ content, filename }) => { 54 | const ast = parse(content); 55 | 56 | const scriptVars = []; 57 | const styleVars = []; 58 | 59 | const nodeTypes = ['Script', 'Program', 'ExportNamedDeclaration', 'LabeledStatement', 'VariableDeclaration', 'VariableDeclarator']; 60 | 61 | walk(ast.instance, { 62 | enter(node) { 63 | if (!nodeTypes.includes(node.type)) { 64 | this.skip(); 65 | } 66 | 67 | if (node.type === 'VariableDeclarator') { 68 | scriptVars.push(node.id.name); 69 | } 70 | 71 | // handle `$: myvar = 'something'` syntax 72 | if (node.type === 'ExpressionStatement') { 73 | walk(node.expression, { 74 | enter(node) { 75 | if (['AssignmentExpression'].includes(node.type)) { 76 | if (node.left.type === 'Identifier') { 77 | scriptVars.push(node.left.name); 78 | } 79 | 80 | this.skip(); 81 | } 82 | } 83 | }); 84 | } 85 | } 86 | }); 87 | 88 | walk(ast.css, { 89 | enter(node) { 90 | if (node.type === 'Function' && node.name === 'var') { 91 | // substr to remove leading '--' 92 | styleVars.push(node.children[0].name.substr(2)); 93 | } 94 | } 95 | }); 96 | 97 | // Find variables that are referenced in the css vars and set them in the files object. 98 | const variables = intersection(scriptVars, styleVars); 99 | if (variables.length) { 100 | // append the document binding tag to the markup 101 | const code = content + createDocumentBinding(); 102 | 103 | files[filename] = { 104 | variables, 105 | hash: hash(filename) 106 | }; 107 | 108 | return { 109 | code 110 | }; 111 | } 112 | }, 113 | script: ({ content, filename }) => { 114 | if (!files[filename]) { 115 | return; 116 | } 117 | 118 | // insert style updaters 119 | const code = content + createVariableUpdaters(files[filename]); 120 | return { 121 | code 122 | }; 123 | }, 124 | style: ({ content, filename }) => { 125 | if (!files[filename]) { 126 | return; 127 | } 128 | 129 | const file = files[filename]; 130 | 131 | // add hash to variables 132 | let code = content; 133 | 134 | for (let name of file.variables) { 135 | const re = new RegExp(`var\\(\\s*--${name}\\s*\\)`, 'g'); 136 | code = code.replace(re, `var(--${name}-${file.hash})`); 137 | } 138 | 139 | // insert style variables 140 | let varsDeclaration = `:root {\n${toCSSVariables(files[filename])}}\n`; 141 | 142 | code = varsDeclaration + code; 143 | return { 144 | code 145 | }; 146 | } 147 | } 148 | } -------------------------------------------------------------------------------- /dev/scripts/setupTypeScript.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** This script modifies the project to support TS code in .svelte files like: 4 | 5 | 8 | 9 | As well as validating the code for CI. 10 | */ 11 | 12 | /** To work on this script: 13 | rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template 14 | */ 15 | 16 | const fs = require("fs") 17 | const path = require("path") 18 | const { argv } = require("process") 19 | 20 | const projectRoot = argv[2] || path.join(__dirname, "..") 21 | 22 | // Add deps to pkg.json 23 | const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8")) 24 | packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, { 25 | "svelte-check": "^2.0.0", 26 | "svelte-preprocess": "^4.0.0", 27 | "@rollup/plugin-typescript": "^8.0.0", 28 | "typescript": "^4.0.0", 29 | "tslib": "^2.0.0", 30 | "@tsconfig/svelte": "^2.0.0" 31 | }) 32 | 33 | // Add script for checking 34 | packageJSON.scripts = Object.assign(packageJSON.scripts, { 35 | "check": "svelte-check --tsconfig ./tsconfig.json" 36 | }) 37 | 38 | // Write the package JSON 39 | fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " ")) 40 | 41 | // mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too 42 | const beforeMainJSPath = path.join(projectRoot, "src", "main.js") 43 | const afterMainTSPath = path.join(projectRoot, "src", "main.ts") 44 | fs.renameSync(beforeMainJSPath, afterMainTSPath) 45 | 46 | // Switch the app.svelte file to use TS 47 | const appSveltePath = path.join(projectRoot, "src", "App.svelte") 48 | let appFile = fs.readFileSync(appSveltePath, "utf8") 49 | appFile = appFile.replace("