├── .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 | [](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("