├── .npmrc ├── static └── favicon.png ├── .gitignore ├── .eslintignore ├── .prettierignore ├── vite.config.js ├── .prettierrc ├── src ├── app.d.ts ├── styles │ └── globalTokens.stylex.ts ├── app.html └── routes │ └── +page.svelte ├── svelte.config.js ├── .eslintrc.cjs ├── tsconfig.json ├── README.md ├── package.json └── vite-stylex-plugin └── index.mjs /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmn/sveltekit-stylex/HEAD/static/favicon.png -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | // and what to do when importing types 4 | declare namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |Visit kit.svelte.dev to read the documentation
16 | 17 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------