@octanejs/stylex 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +89 -0
- package/package.json +44 -0
- package/src/index.ts +30 -0
- package/src/transform.js +58 -0
- package/src/vite.js +172 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dominic Gannaway
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @octanejs/stylex
|
|
2
|
+
|
|
3
|
+
[StyleX](https://stylexjs.com) for the [octane](https://github.com/octanejs/octane) renderer.
|
|
4
|
+
|
|
5
|
+
StyleX is a **build-time** CSS-in-JS system: a compiler turns `stylex.create({...})`
|
|
6
|
+
into atomic class names and extracts the CSS, and `stylex.props(...)` merges those at
|
|
7
|
+
runtime into the props you spread onto an element. So — unlike the zustand/motion
|
|
8
|
+
_runtime_ bindings — the octane integration is the **compiler pass**, shipped in two
|
|
9
|
+
parts:
|
|
10
|
+
|
|
11
|
+
- **`@octanejs/stylex`** — the authoring surface you import in components. It
|
|
12
|
+
re-exports `@stylexjs/stylex` (so the runtime `props`/`attrs` and all the types
|
|
13
|
+
work) and is registered as a StyleX _import source_ so the compiler finds your
|
|
14
|
+
`stylex.*` call sites.
|
|
15
|
+
- **`@octanejs/stylex/vite`** — the Vite plugin that runs the StyleX compiler over
|
|
16
|
+
octane's compiled `.tsrx` output and emits one static atomic stylesheet
|
|
17
|
+
(`virtual:stylex.css`). **Zero StyleX runtime ships in your bundle.**
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
Add the plugin **after** `octane()` and import the generated sheet once:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// vite.config.ts
|
|
25
|
+
import { octane } from 'octane/compiler/vite';
|
|
26
|
+
import { stylex } from '@octanejs/stylex/vite';
|
|
27
|
+
|
|
28
|
+
export default {
|
|
29
|
+
plugins: [octane(), stylex()],
|
|
30
|
+
};
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// app entry
|
|
35
|
+
import 'virtual:stylex.css';
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import * as stylex from '@octanejs/stylex';
|
|
42
|
+
|
|
43
|
+
const styles = stylex.create({
|
|
44
|
+
root: { padding: 16, color: 'tomato' },
|
|
45
|
+
active: { color: 'blue' },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export function Card(props) @{
|
|
49
|
+
<div {...stylex.props(styles.root, props.on && styles.active)}>{'Card'}</div>
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`stylex.props()` returns `{ className?, style? }`. Octane host elements take that
|
|
54
|
+
directly — the spread handler maps both `className` and `class` to the `class`
|
|
55
|
+
attribute and applies a `style` object — so there's no octane-specific `props()`
|
|
56
|
+
variant to learn. `stylex.attrs()` (which returns `{ class, style }` as a string) is
|
|
57
|
+
also re-exported for raw-attribute contexts.
|
|
58
|
+
|
|
59
|
+
## How it works
|
|
60
|
+
|
|
61
|
+
`octane()` (`enforce: 'pre'`) compiles `.tsrx` → JS, preserving the `stylex.*` calls.
|
|
62
|
+
`stylex()` (`enforce: 'post'`) then runs `@stylexjs/babel-plugin` over that output:
|
|
63
|
+
each `stylex.create`/`props`/`keyframes`/`defineVars`/`createTheme` call is replaced
|
|
64
|
+
with its compiled atomic form, and the extracted rules from every module are folded
|
|
65
|
+
into `virtual:stylex.css` — deduped by content-hashed key and ordered by StyleX's
|
|
66
|
+
baked-in cascade priority (so import order is irrelevant). In dev, editing a file
|
|
67
|
+
re-aggregates the sheet (HMR full-reload). In a production build, where the virtual
|
|
68
|
+
module can be loaded before every styled module has been transformed, the sheet is
|
|
69
|
+
finalized in `generateBundle` once all rules are collected — so the shipped CSS always
|
|
70
|
+
contains every rule regardless of module/transform order.
|
|
71
|
+
|
|
72
|
+
## Options
|
|
73
|
+
|
|
74
|
+
`stylex(options)`:
|
|
75
|
+
|
|
76
|
+
- `include` — files to scan (default: `.tsrx`/`.tsx`/`.jsx`/`.ts`/`.js`).
|
|
77
|
+
- `importSources` — specifiers treated as StyleX (default: `@octanejs/stylex` +
|
|
78
|
+
`@stylexjs/stylex`).
|
|
79
|
+
- `dev` — force dev/prod compilation (default: dev while Vite is serving).
|
|
80
|
+
- `useCSSLayers` — emit `@layer` rules instead of the `:not(#\#)` specificity hack.
|
|
81
|
+
- `unstable_moduleResolution` — StyleX cross-file token (`.stylex.ts`) resolution.
|
|
82
|
+
- `stylexOptions` — escape hatch for any other `@stylexjs/babel-plugin` option.
|
|
83
|
+
|
|
84
|
+
## Divergences
|
|
85
|
+
|
|
86
|
+
- StyleX's `sx` JSX prop is not supported — use the `{...stylex.props(...)}` spread
|
|
87
|
+
(which is also StyleX's own recommended pattern).
|
|
88
|
+
- The plugin runs the StyleX compiler on octane's _output_, so StyleX's own
|
|
89
|
+
source-scanning tools (the PostCSS plugin) are not used and not needed.
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octanejs/stylex",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "StyleX bindings for the octane renderer — reuses StyleX's framework-agnostic compiler core and routes the generated atomic CSS through octane's stylesheet channel (client dedupe + SSR render().css).",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Dominic Gannaway",
|
|
9
|
+
"email": "dg@domgan.com"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/octanejs/octane.git",
|
|
17
|
+
"directory": "packages/stylex"
|
|
18
|
+
},
|
|
19
|
+
"main": "src/index.ts",
|
|
20
|
+
"module": "src/index.ts",
|
|
21
|
+
"types": "src/index.ts",
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./src/index.ts",
|
|
28
|
+
"./vite": "./src/vite.js"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@babel/core": "7.29.7",
|
|
32
|
+
"@stylexjs/babel-plugin": "0.19.0",
|
|
33
|
+
"@stylexjs/stylex": "0.19.0",
|
|
34
|
+
"octane": "0.1.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/babel__core": "7.20.5",
|
|
38
|
+
"vite": "^8.0.16",
|
|
39
|
+
"vitest": "^4.1.9"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"test": "vitest run"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// @octanejs/stylex — StyleX for the octane renderer.
|
|
2
|
+
//
|
|
3
|
+
// StyleX is a BUILD-TIME CSS-in-JS system: `@stylexjs/babel-plugin` compiles
|
|
4
|
+
// `stylex.create({...})` into atomic class-name maps and extracts the CSS, and
|
|
5
|
+
// `stylex.props(...)` / `stylex.attrs(...)` merge those at runtime into the props
|
|
6
|
+
// you spread onto an element. Unlike zustand/motion there is no React runtime to
|
|
7
|
+
// reimplement — the integration is the COMPILER pass. So this package is two parts:
|
|
8
|
+
//
|
|
9
|
+
// • `@octanejs/stylex` — this module: the authoring surface you import in
|
|
10
|
+
// components. It re-exports `@stylexjs/stylex` so
|
|
11
|
+
// types + the runtime `props`/`attrs` work, and so
|
|
12
|
+
// the babel plugin (configured to treat
|
|
13
|
+
// `@octanejs/stylex` as an import source) can find
|
|
14
|
+
// and compile the `stylex.*` call sites.
|
|
15
|
+
// • `@octanejs/stylex/vite` — the Vite plugin that runs the StyleX compiler over
|
|
16
|
+
// octane's compiled `.tsrx` output and emits one
|
|
17
|
+
// static atomic stylesheet (`virtual:stylex.css`).
|
|
18
|
+
//
|
|
19
|
+
// Octane host elements accept StyleX's output directly: `props()` returns
|
|
20
|
+
// `{ className?, style? }`, and octane's spread handler maps both `className` and
|
|
21
|
+
// `class` to the `class` attribute and applies a `style` object — so
|
|
22
|
+
// `<div {...stylex.props(styles.root)}>` just works.
|
|
23
|
+
import * as stylex from '@stylexjs/stylex';
|
|
24
|
+
|
|
25
|
+
export * from '@stylexjs/stylex';
|
|
26
|
+
|
|
27
|
+
// `@stylexjs/stylex` has no default export; provide the namespace as the default so
|
|
28
|
+
// `import stylex from '@octanejs/stylex'` (the common StyleX usage) also resolves —
|
|
29
|
+
// the babel plugin recognizes default, namespace, and named imports alike.
|
|
30
|
+
export default stylex;
|
package/src/transform.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The StyleX compiler pass, factored out of the Vite plugin so it can be unit
|
|
2
|
+
// tested directly. `transformStylex` runs `@stylexjs/babel-plugin` over a module
|
|
3
|
+
// (octane's compiled `.tsrx` output, or plain `.ts`/`.tsx`) — replacing every
|
|
4
|
+
// `stylex.create`/`props`/`keyframes`/`defineVars`/`createTheme` call with its
|
|
5
|
+
// compiled form and collecting the extracted atomic rules. `generateStylexCSS`
|
|
6
|
+
// folds the rules from ALL modules into one deduped, priority-ordered stylesheet.
|
|
7
|
+
//
|
|
8
|
+
// Authored in `.js` (like octane's `compiler/` tooling) so it loads cleanly when a
|
|
9
|
+
// consuming app's `vite.config.ts` pulls the plugin in through Node's ESM loader —
|
|
10
|
+
// build tooling that Node executes directly cannot be raw `.ts` source.
|
|
11
|
+
import babel from '@babel/core';
|
|
12
|
+
import stylexBabelPlugin from '@stylexjs/babel-plugin';
|
|
13
|
+
|
|
14
|
+
// `@stylexjs/babel-plugin` emits one tuple per atomic rule —
|
|
15
|
+
// `[key, { ltr, rtl? }, priority]`: a stable content-hashed key, the LTR (and
|
|
16
|
+
// optional RTL) CSS text, and a numeric cascade priority. The key dedupes identical
|
|
17
|
+
// rules across files; the priority is baked into the selector by `processStylexRules`,
|
|
18
|
+
// so cascade order is independent of injection order.
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_IMPORT_SOURCES = ['@octanejs/stylex', '@stylexjs/stylex'];
|
|
21
|
+
|
|
22
|
+
export function transformStylex(code, opts) {
|
|
23
|
+
const res = babel.transformSync(code, {
|
|
24
|
+
filename: opts.filename,
|
|
25
|
+
babelrc: false,
|
|
26
|
+
configFile: false,
|
|
27
|
+
sourceMaps: opts.sourceMaps ?? true,
|
|
28
|
+
plugins: [
|
|
29
|
+
[
|
|
30
|
+
stylexBabelPlugin,
|
|
31
|
+
{
|
|
32
|
+
dev: opts.dev ?? false,
|
|
33
|
+
// Never the runtime-injection path — CSS is collected from
|
|
34
|
+
// `metadata.stylex` and emitted as one static sheet (the build-time route).
|
|
35
|
+
runtimeInjection: false,
|
|
36
|
+
importSources: opts.importSources ?? DEFAULT_IMPORT_SOURCES,
|
|
37
|
+
unstable_moduleResolution: opts.unstable_moduleResolution ?? {
|
|
38
|
+
type: 'commonJS',
|
|
39
|
+
rootDir: process.cwd(),
|
|
40
|
+
},
|
|
41
|
+
...opts.stylexOptions,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
code: res?.code ?? code,
|
|
48
|
+
map: res?.map ?? null,
|
|
49
|
+
rules: res?.metadata?.stylex ?? [],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Fold every module's collected rules into one stylesheet: deduped by key, ordered
|
|
54
|
+
// by StyleX's baked-in cascade priority (so source/import order is irrelevant).
|
|
55
|
+
export function generateStylexCSS(rules, useCSSLayers = false) {
|
|
56
|
+
if (rules.length === 0) return '';
|
|
57
|
+
return stylexBabelPlugin.processStylexRules(rules, useCSSLayers);
|
|
58
|
+
}
|
package/src/vite.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// @octanejs/stylex/vite — the build-time StyleX integration for octane apps.
|
|
2
|
+
//
|
|
3
|
+
// Add it AFTER `octane()` in your Vite config and import the generated sheet once:
|
|
4
|
+
//
|
|
5
|
+
// import { octane } from 'octane/compiler/vite';
|
|
6
|
+
// import { stylex } from '@octanejs/stylex/vite';
|
|
7
|
+
// export default { plugins: [octane(), stylex()] };
|
|
8
|
+
//
|
|
9
|
+
// // app entry:
|
|
10
|
+
// import 'virtual:stylex.css';
|
|
11
|
+
//
|
|
12
|
+
// `enforce: 'post'` makes this run on octane's COMPILED output (octane is
|
|
13
|
+
// `enforce: 'pre'`), where the `stylex.*` call sites still live. Each module is run
|
|
14
|
+
// through the StyleX compiler (`./transform`): the calls are replaced with their
|
|
15
|
+
// atomic form and the extracted rules are accumulated per file. `virtual:stylex.css`
|
|
16
|
+
// resolves to the deduped, priority-ordered concatenation of every file's rules — one
|
|
17
|
+
// static atomic stylesheet, zero StyleX runtime in the shipped bundle.
|
|
18
|
+
//
|
|
19
|
+
// SERVE vs BUILD ordering. In dev, `virtual:stylex.css`'s `load` returns the live
|
|
20
|
+
// aggregate and an HMR full-reload re-pulls it whenever a file's rules change. In a
|
|
21
|
+
// production build the virtual module can be loaded BEFORE every styled module has
|
|
22
|
+
// been transformed (Rollup traverses the graph in parallel), so `load` returns a
|
|
23
|
+
// stable PLACEHOLDER rule and `generateBundle` — which runs after ALL transforms are
|
|
24
|
+
// done — swaps that placeholder for the now-complete sheet. Either way the shipped
|
|
25
|
+
// stylesheet contains every rule.
|
|
26
|
+
//
|
|
27
|
+
// Authored in `.js` (like octane's `compiler/vite.js`) so the plugin loads when a
|
|
28
|
+
// consuming app's `vite.config.ts` pulls it in through Node's ESM loader.
|
|
29
|
+
import { transformStylex, generateStylexCSS, DEFAULT_IMPORT_SOURCES } from './transform.js';
|
|
30
|
+
|
|
31
|
+
const VIRTUAL_CSS_ID = 'virtual:stylex.css';
|
|
32
|
+
const RESOLVED_VIRTUAL_CSS_ID = '\0' + VIRTUAL_CSS_ID;
|
|
33
|
+
|
|
34
|
+
// A unique, minification-surviving rule emitted in a build so the virtual module can
|
|
35
|
+
// load early; `generateBundle` finds and replaces it with the final sheet. The custom
|
|
36
|
+
// property keeps the rule non-empty (minifiers drop empty rules) and never collides
|
|
37
|
+
// with StyleX's hashed `x…` class names.
|
|
38
|
+
const PLACEHOLDER_RULE = '.__stylex_sheet__{--stylex-sheet:1}';
|
|
39
|
+
const PLACEHOLDER_RE = /\.__stylex_sheet__\s*\{[^}]*\}/g;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {object} [options]
|
|
43
|
+
* @param {RegExp} [options.include] Files to scan (default: `.tsrx`/`.tsx`/`.jsx`/`.ts`/`.js`).
|
|
44
|
+
* @param {Array<string | { from: string, as: string }>} [options.importSources] StyleX import specifiers.
|
|
45
|
+
* @param {boolean} [options.dev] Force dev/prod compilation (default: dev when Vite is serving).
|
|
46
|
+
* @param {boolean} [options.useCSSLayers] Use `@layer` rules instead of the `:not(#\#)` specificity hack.
|
|
47
|
+
* @param {Record<string, unknown>} [options.unstable_moduleResolution] StyleX cross-file token resolution.
|
|
48
|
+
* @param {Record<string, unknown>} [options.stylexOptions] Escape hatch for other `@stylexjs/babel-plugin` options.
|
|
49
|
+
*/
|
|
50
|
+
export function stylex(options = {}) {
|
|
51
|
+
const include = options.include ?? /\.(tsrx|tsx|jsx|ts|js)(\?|$)/;
|
|
52
|
+
const importSources = options.importSources ?? DEFAULT_IMPORT_SOURCES;
|
|
53
|
+
const useCSSLayers = options.useCSSLayers ?? false;
|
|
54
|
+
// Per-module rule sets, so re-transforming one file (HMR) replaces only its rules.
|
|
55
|
+
const rulesByFile = new Map();
|
|
56
|
+
let isDev = false;
|
|
57
|
+
let isBuild = false;
|
|
58
|
+
let root = process.cwd();
|
|
59
|
+
let server;
|
|
60
|
+
|
|
61
|
+
const aggregate = () => {
|
|
62
|
+
const all = [];
|
|
63
|
+
for (const rules of rulesByFile.values()) for (const r of rules) all.push(r);
|
|
64
|
+
return generateStylexCSS(all, useCSSLayers);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
name: '@octanejs/stylex',
|
|
69
|
+
// Run after octane's `.tsrx` -> JS transform, where `stylex.*` calls survive.
|
|
70
|
+
enforce: 'post',
|
|
71
|
+
|
|
72
|
+
configResolved(config) {
|
|
73
|
+
isDev = options.dev ?? config.command === 'serve';
|
|
74
|
+
isBuild = config.command === 'build';
|
|
75
|
+
if (config.root) root = config.root;
|
|
76
|
+
},
|
|
77
|
+
configureServer(s) {
|
|
78
|
+
server = s;
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
resolveId(id) {
|
|
82
|
+
if (id === VIRTUAL_CSS_ID) return RESOLVED_VIRTUAL_CSS_ID;
|
|
83
|
+
return null;
|
|
84
|
+
},
|
|
85
|
+
load(id) {
|
|
86
|
+
if (id !== RESOLVED_VIRTUAL_CSS_ID) return null;
|
|
87
|
+
// In a build the graph may not be fully transformed yet — emit a placeholder
|
|
88
|
+
// that `generateBundle` replaces with the complete sheet. In serve, the live
|
|
89
|
+
// aggregate is correct (HMR re-pulls on change).
|
|
90
|
+
return isBuild ? PLACEHOLDER_RULE : aggregate();
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
transform(code, id) {
|
|
94
|
+
const file = id.split('?')[0];
|
|
95
|
+
if (!include.test(file) || file.includes('/node_modules/')) return null;
|
|
96
|
+
// Cheap gate: skip files that can't reference StyleX at all.
|
|
97
|
+
if (!importSources.some((s) => code.includes(typeof s === 'string' ? s : s.from))) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const {
|
|
101
|
+
code: out,
|
|
102
|
+
map,
|
|
103
|
+
rules,
|
|
104
|
+
} = transformStylex(code, {
|
|
105
|
+
filename: file,
|
|
106
|
+
dev: isDev,
|
|
107
|
+
importSources,
|
|
108
|
+
unstable_moduleResolution: options.unstable_moduleResolution ?? {
|
|
109
|
+
type: 'commonJS',
|
|
110
|
+
rootDir: root,
|
|
111
|
+
},
|
|
112
|
+
stylexOptions: options.stylexOptions,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const prevKey = keyOf(rulesByFile.get(id));
|
|
116
|
+
if (rules.length > 0) rulesByFile.set(id, rules);
|
|
117
|
+
else rulesByFile.delete(id);
|
|
118
|
+
|
|
119
|
+
// HMR: if this file's rule set changed, invalidate the virtual sheet so the
|
|
120
|
+
// next request re-aggregates. A full reload keeps it simple and correct.
|
|
121
|
+
if (server && prevKey !== keyOf(rulesByFile.get(id))) {
|
|
122
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_CSS_ID);
|
|
123
|
+
if (mod) {
|
|
124
|
+
server.moduleGraph.invalidateModule(mod);
|
|
125
|
+
server.ws.send({ type: 'full-reload' });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { code: out, map };
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// After every module is transformed, the aggregate is complete — swap the
|
|
133
|
+
// placeholder for the real sheet wherever it landed (a CSS asset normally, or a
|
|
134
|
+
// JS chunk if the CSS was inlined). `enforce: 'post'` runs this after Vite has
|
|
135
|
+
// emitted its CSS asset.
|
|
136
|
+
generateBundle(_options, bundle) {
|
|
137
|
+
if (!isBuild) return;
|
|
138
|
+
const css = aggregate();
|
|
139
|
+
for (const fileName in bundle) {
|
|
140
|
+
const file = bundle[fileName];
|
|
141
|
+
if (file.type === 'asset' && typeof file.source === 'string') {
|
|
142
|
+
if (PLACEHOLDER_RE.test(file.source)) {
|
|
143
|
+
file.source = file.source.replace(PLACEHOLDER_RE, () => css);
|
|
144
|
+
}
|
|
145
|
+
} else if (file.type === 'chunk' && typeof file.code === 'string') {
|
|
146
|
+
if (PLACEHOLDER_RE.test(file.code)) {
|
|
147
|
+
// CSS inlined into JS: the placeholder sits inside a string literal,
|
|
148
|
+
// so the replacement must be string-escaped to match.
|
|
149
|
+
file.code = file.code.replace(PLACEHOLDER_RE, () => jsStringEscape(css));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function keyOf(rules) {
|
|
158
|
+
return rules ? rules.map((r) => r[0]).join('|') : '';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Escape a CSS string so it can be spliced inside an existing JS string literal
|
|
162
|
+
// (covers the rarer case of Vite inlining the sheet into a JS chunk). Backslash must
|
|
163
|
+
// be escaped first; quotes/backticks cover the literal styles esbuild emits.
|
|
164
|
+
function jsStringEscape(css) {
|
|
165
|
+
return css
|
|
166
|
+
.replace(/\\/g, '\\\\')
|
|
167
|
+
.replace(/'/g, "\\'")
|
|
168
|
+
.replace(/"/g, '\\"')
|
|
169
|
+
.replace(/`/g, '\\`')
|
|
170
|
+
.replace(/\n/g, '\\n')
|
|
171
|
+
.replace(/\r/g, '\\r');
|
|
172
|
+
}
|