├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json ├── src ├── app.d.ts ├── app.html ├── routes │ └── +page.svelte └── styles │ └── globalTokens.stylex.ts ├── static └── favicon.png ├── svelte.config.js ├── tsconfig.json ├── vite-stylex-plugin └── index.mjs └── vite.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 5 | plugins: ['svelte3', '@typescript-eslint'], 6 | ignorePatterns: ['*.cjs'], 7 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], 8 | settings: { 9 | 'svelte3/typescript': () => require('typescript') 10 | }, 11 | parserOptions: { 12 | sourceType: 'module', 13 | ecmaVersion: 2020 14 | }, 15 | env: { 16 | browser: true, 17 | es2017: true, 18 | node: true 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "pluginSearchDirs": ["."], 8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sveltekit-StyleX 2 | 3 | This is a project to get Sveltekit working with StyleX. 4 | 5 | Due to the experimental nature of the Vite integration, the project is 6 | set up to delete the cache before running each build (dev or prod). 7 | 8 | --- 9 | 10 | ## How to use StyleX within Svelte files 11 | 12 | You can define your styles in a ` 6 | 7 | 8 |
Visit kit.svelte.dev to read the documentation
16 | 17 | -------------------------------------------------------------------------------- /src/styles/globalTokens.stylex.ts: -------------------------------------------------------------------------------- 1 | import * as stylex from '@stylexjs/stylex'; 2 | 3 | const DARK_MODE = '@media (prefers-color-scheme: dark)'; 4 | 5 | export const globalTokens = stylex.defineVars({ 6 | green: { default: 'green', [DARK_MODE]: 'lightgreen' }, 7 | red: 'red', 8 | sans: 'system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif' 9 | }); 10 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmn/sveltekit-stylex/de1df6225aa1d4ff0f99cf9d81de5d68db5ba46a/static/favicon.png -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/kit/vite'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | adapter: adapter() 12 | } 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /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 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /vite-stylex-plugin/index.mjs: -------------------------------------------------------------------------------- 1 | import babel from '@babel/core'; 2 | import stylexBabelPlugin from '@stylexjs/babel-plugin'; 3 | import path from 'path'; 4 | import crypto from 'crypto'; 5 | 6 | export default function styleXVitePlugin({ 7 | unstable_moduleResolution = { type: 'commonJS', rootDir: process.cwd() }, 8 | stylexImports = ['@stylexjs/stylex'], 9 | ...options 10 | } = {}) { 11 | let stylexRules = {}; 12 | let isProd = false; 13 | let assetsDir = 'assets'; 14 | let publicBasePath = '/'; 15 | let lastStyleXCSS = { 16 | id: 0, 17 | css: '' 18 | }; 19 | 20 | let outputFileName = null; 21 | 22 | const VIRTUAL_STYLEX_MODULE_ID = 'virtual:stylex.css'; 23 | const RESOLVED_STYLEX_MODULE_ID = '\0' + VIRTUAL_STYLEX_MODULE_ID; 24 | 25 | let server; 26 | 27 | let hasRemix = false; 28 | 29 | let reloadCount = 0; 30 | function reloadStyleX() { 31 | reloadCount++; 32 | 33 | if (!server) { 34 | return; 35 | } 36 | 37 | const module = server.moduleGraph.getModuleById(RESOLVED_STYLEX_MODULE_ID); 38 | 39 | if (!module) { 40 | return; 41 | } 42 | 43 | server.moduleGraph.invalidateModule(module); 44 | server.reloadModule(module); 45 | } 46 | 47 | function compileStyleX() { 48 | if (reloadCount === lastStyleXCSS.id) { 49 | return lastStyleXCSS.css; 50 | } 51 | 52 | const rules = Object.values(stylexRules).flat(); 53 | 54 | if (rules.length === 0) { 55 | return ''; 56 | } 57 | 58 | const stylexCSS = stylexBabelPlugin.processStylexRules(rules, false); 59 | 60 | lastStyleXCSS = { 61 | id: reloadCount, 62 | css: stylexCSS 63 | }; 64 | 65 | return stylexCSS; 66 | } 67 | 68 | return { 69 | name: 'vite-plugin-stylex', 70 | 71 | config(config, env) { 72 | isProd = env.mode === 'production' || config.mode === 'production'; 73 | assetsDir = config.build?.assetsDir || 'assets'; 74 | publicBasePath = config.base || '/'; 75 | hasRemix = 76 | config.plugins?.flat().some((p) => p && 'name' in p && p.name.includes('remix')) ?? false; 77 | }, 78 | 79 | buildStart() { 80 | stylexRules = {}; 81 | }, 82 | 83 | configureServer(_server) { 84 | server = _server; 85 | server.middlewares.use((req, res, next) => { 86 | // console.log("MIDDLEWARE", req.originalUrl); 87 | // maybe better way to do this? 88 | if (/virtual:stylex\.css/.test(req.originalUrl)) { 89 | res.setHeader('Content-Type', 'text/css'); 90 | const stylexBundle = compileStyleX(); 91 | console.log('SERVE Stylex bundle'); 92 | console.log('===================='); 93 | console.log(stylexBundle); 94 | console.log('===================='); 95 | res.end(stylexBundle); 96 | return; 97 | } 98 | next(); 99 | }); 100 | }, 101 | 102 | resolveId(id) { 103 | if (id === VIRTUAL_STYLEX_MODULE_ID || id.includes(VIRTUAL_STYLEX_MODULE_ID)) { 104 | return RESOLVED_STYLEX_MODULE_ID; 105 | } 106 | }, 107 | 108 | load(id) { 109 | if ( 110 | id === RESOLVED_STYLEX_MODULE_ID || 111 | id === VIRTUAL_STYLEX_MODULE_ID || 112 | id.endsWith('stylex.css') 113 | ) { 114 | return compileStyleX(); 115 | } 116 | }, 117 | 118 | shouldTransformCachedModule({ id, meta }) { 119 | stylexRules[id] = meta.stylex; 120 | return false; 121 | }, 122 | 123 | generateBundle(_options, bundle /*, isWrite*/) { 124 | const stylexCSS = compileStyleX(); 125 | 126 | const hash = crypto.createHash('sha1').update(stylexCSS).digest('hex').slice(0, 8); 127 | 128 | outputFileName = path.join(assetsDir, `stylex.${hash}.css`); 129 | 130 | const existingCSSFileName = Object.keys(bundle).find((fileName) => fileName.endsWith('.css')); 131 | if (existingCSSFileName) { 132 | const existingCSSFile = bundle[existingCSSFileName]; 133 | 134 | // HACK: This assumes there is only StyleX being used for CSS 135 | existingCSSFile.source = stylexCSS; 136 | 137 | // Otherwise, this would work too: 138 | // existingCSSFile.source += '\n\n' + stylexCSS; 139 | } 140 | 141 | this.emitFile({ 142 | fileName: outputFileName, 143 | source: stylexCSS, 144 | type: 'asset' 145 | }); 146 | }, 147 | 148 | async transform(inputCode, id, { ssr: isSSR } = {}) { 149 | if (!stylexImports.some((importName) => inputCode.includes(importName))) { 150 | return; 151 | } 152 | 153 | const isJSLikeFile = 154 | id.endsWith('.js') || 155 | id.endsWith('.jsx') || 156 | id.endsWith('.ts') || 157 | id.endsWith('.tsx') || 158 | id.endsWith('.svelte'); 159 | 160 | if (!isJSLikeFile) { 161 | return; 162 | } 163 | 164 | const isCompileMode = isProd || isSSR || hasRemix; 165 | 166 | const result = await babel.transformAsync(inputCode, { 167 | babelrc: false, 168 | filename: id.endsWith('.svelte') ? id + '.ts' : id, 169 | plugins: [ 170 | [ 171 | stylexBabelPlugin, 172 | { 173 | dev: !isProd, 174 | unstable_moduleResolution, 175 | importSources: stylexImports, 176 | runtimeInjection: !isProd, 177 | ...options 178 | } 179 | ] 180 | ] 181 | }); 182 | 183 | if (!result) { 184 | return; 185 | } 186 | 187 | let { code, map, metadata } = result; 188 | 189 | // console.log('TRANSFORM', id, code); 190 | 191 | if (isProd) { 192 | code = 'import "virtual:stylex.css";\n' + code; 193 | } 194 | 195 | if (isCompileMode && metadata?.stylex != null && metadata?.stylex.length > 0) { 196 | stylexRules[id] = metadata.stylex; 197 | reloadStyleX(); 198 | } 199 | 200 | return { code: code ?? undefined, map, meta: metadata }; 201 | }, 202 | 203 | transformIndexHtml(html, ctx) { 204 | if (!isProd || !outputFileName) { 205 | return html; 206 | } 207 | 208 | const asset = ctx.bundle?.[outputFileName]; 209 | 210 | if (!asset) { 211 | return html; 212 | } 213 | 214 | const { fileName } = asset; 215 | const publicPath = path.join(publicBasePath, fileName); 216 | 217 | return [ 218 | { 219 | tag: 'link', 220 | attrs: { 221 | rel: 'stylesheet', 222 | href: publicPath 223 | }, 224 | injectTo: 'head' 225 | }, 226 | { 227 | tag: 'link', 228 | attrs: { 229 | rel: 'stylesheet', 230 | href: 'virtual:stylex.css' 231 | }, 232 | injectTo: 'head' 233 | } 234 | ]; 235 | } 236 | }; 237 | } 238 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import styleX from './vite-stylex-plugin/index.mjs'; 3 | 4 | /** @type {import('vite').UserConfig} */ 5 | const config = { 6 | plugins: [sveltekit(), styleX()] 7 | }; 8 | 9 | export default config; 10 | --------------------------------------------------------------------------------