diff --git a/.changeset/twelve-suns-hope.md b/.changeset/twelve-suns-hope.md
new file mode 100644
index 000000000..d2e3a5c03
--- /dev/null
+++ b/.changeset/twelve-suns-hope.md
@@ -0,0 +1,14 @@
+---
+'@vanilla-extract/rollup-plugin': minor
+---
+
+Add "extract" option which bundles CSS into one bundle. Removes .css imports.
+
+**EXAMPLE USAGE**:
+```ts
+vanillaExtractPlugin({
+ extract: {
+ name: 'bundle.css',
+ sourcemap: false
+ }
+});
diff --git a/fixtures/react-library-example/package.json b/fixtures/react-library-example/package.json
new file mode 100644
index 000000000..cdca84738
--- /dev/null
+++ b/fixtures/react-library-example/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@fixtures/react-library-example",
+ "description": "React design system library",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "main": "./dist/index.js",
+ "dependencies": {
+ "@vanilla-extract/css": "workspace:*",
+ "clsx": "^2.1.1",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.55",
+ "@types/react-dom": "^18.2.19"
+ }
+}
diff --git a/fixtures/react-library-example/src/button/button.css.ts b/fixtures/react-library-example/src/button/button.css.ts
new file mode 100644
index 000000000..dc699930c
--- /dev/null
+++ b/fixtures/react-library-example/src/button/button.css.ts
@@ -0,0 +1,11 @@
+import { style } from '@vanilla-extract/css';
+import { vars } from '../styles/vars.css.js';
+
+export const btn = style({
+ background: vars.color.background.brand,
+ borderRadius: vars.size.radius[200],
+ color: vars.color.text.brand,
+ ...vars.typography.body.medium,
+ paddingBlock: vars.size.space[200],
+ paddingInline: vars.size.space[300],
+});
diff --git a/fixtures/react-library-example/src/button/button.tsx b/fixtures/react-library-example/src/button/button.tsx
new file mode 100644
index 000000000..469cd23fa
--- /dev/null
+++ b/fixtures/react-library-example/src/button/button.tsx
@@ -0,0 +1,15 @@
+import clsx from 'clsx';
+import * as styles from './button.css.js';
+
+export default function Button({
+ className,
+ children,
+ type = 'button',
+ ...props
+}: React.ComponentProps<'button'>) {
+ return (
+
+ );
+}
diff --git a/fixtures/react-library-example/src/checkbox/checkbox.css.ts b/fixtures/react-library-example/src/checkbox/checkbox.css.ts
new file mode 100644
index 000000000..d2fe75cae
--- /dev/null
+++ b/fixtures/react-library-example/src/checkbox/checkbox.css.ts
@@ -0,0 +1,22 @@
+import { style } from '@vanilla-extract/css';
+import { vars } from '../styles/vars.css.js';
+
+export const label = style({
+ display: 'block',
+ ...vars.typography.body.medium,
+
+ '::before': {
+ background: vars.color.background.brand,
+ borderColor: vars.color.border.default,
+ borderWidth: 1,
+ borderStyle: 'solid',
+ content: '',
+ borderRadius: vars.size.radius['200'],
+ marginRight: vars.size.space['300'],
+ },
+});
+
+export const input = style({
+ width: '1.5rem',
+ height: '1.5rem',
+});
diff --git a/fixtures/react-library-example/src/checkbox/checkbox.tsx b/fixtures/react-library-example/src/checkbox/checkbox.tsx
new file mode 100644
index 000000000..9a480d897
--- /dev/null
+++ b/fixtures/react-library-example/src/checkbox/checkbox.tsx
@@ -0,0 +1,25 @@
+import { useId } from 'react';
+import clsx from 'clsx';
+import * as styles from './checkbox.css.js';
+
+export default function Radio({
+ children,
+ className,
+ id,
+ ...props
+}: React.ComponentProps<'input'> & { children: React.ReactNode }) {
+ const randomID = useId();
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/fixtures/react-library-example/src/index.ts b/fixtures/react-library-example/src/index.ts
new file mode 100644
index 000000000..bec9c4ba1
--- /dev/null
+++ b/fixtures/react-library-example/src/index.ts
@@ -0,0 +1,10 @@
+// 1. Style reset
+import './styles/reset.css.js';
+
+// 2. Design library
+export { default as Button } from './button/button.js';
+export { default as Checkbox } from './checkbox/checkbox.js';
+export { default as Radio } from './radio/radio.js';
+
+// 3. Utility CSS should be last
+import './styles/utility.css.js';
diff --git a/fixtures/react-library-example/src/radio/radio.css.ts b/fixtures/react-library-example/src/radio/radio.css.ts
new file mode 100644
index 000000000..d5b54ba74
--- /dev/null
+++ b/fixtures/react-library-example/src/radio/radio.css.ts
@@ -0,0 +1,22 @@
+import { style } from '@vanilla-extract/css';
+import { vars } from '../styles/vars.css.js';
+
+export const label = style({
+ display: 'block',
+ ...vars.typography.body.medium,
+
+ '::before': {
+ background: vars.color.background.brand,
+ borderColor: vars.color.border.default,
+ borderWidth: 1,
+ borderStyle: 'solid',
+ content: '',
+ borderRadius: vars.size.radius.full,
+ marginRight: vars.size.space['300'],
+ },
+});
+
+export const input = style({
+ width: '1.5rem',
+ height: '1.5rem',
+});
diff --git a/fixtures/react-library-example/src/radio/radio.tsx b/fixtures/react-library-example/src/radio/radio.tsx
new file mode 100644
index 000000000..b752cda57
--- /dev/null
+++ b/fixtures/react-library-example/src/radio/radio.tsx
@@ -0,0 +1,25 @@
+import { useId } from 'react';
+import clsx from 'clsx';
+import * as styles from './radio.css.js';
+
+export default function Radio({
+ children,
+ className,
+ id,
+ ...props
+}: React.ComponentProps<'input'> & { children: React.ReactNode }) {
+ const randomID = useId();
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/fixtures/react-library-example/src/styles/reset.css.ts b/fixtures/react-library-example/src/styles/reset.css.ts
new file mode 100644
index 000000000..0b7a0c769
--- /dev/null
+++ b/fixtures/react-library-example/src/styles/reset.css.ts
@@ -0,0 +1,8 @@
+import { globalStyle } from '@vanilla-extract/css';
+
+globalStyle('html, body', {
+ fontSize: '100%',
+ height: '100%',
+ lineHeight: 1,
+ margin: 0,
+});
diff --git a/fixtures/react-library-example/src/styles/utility.css.ts b/fixtures/react-library-example/src/styles/utility.css.ts
new file mode 100644
index 000000000..81e1bcab6
--- /dev/null
+++ b/fixtures/react-library-example/src/styles/utility.css.ts
@@ -0,0 +1,10 @@
+import { style } from '@vanilla-extract/css';
+import { vars } from './vars.css.js';
+
+// Note: these aren’t meant to be intended as best practice over Sprinkles,
+// these are only testing style() declarations loaded differently
+
+export const mt100 = style({ marginTop: vars.size.space[100] });
+export const mt200 = style({ marginTop: vars.size.space[200] });
+export const mt300 = style({ marginTop: vars.size.space[300] });
+export const mt400 = style({ marginTop: vars.size.space[400] });
diff --git a/fixtures/react-library-example/src/styles/vars.css.ts b/fixtures/react-library-example/src/styles/vars.css.ts
new file mode 100644
index 000000000..b12072ce4
--- /dev/null
+++ b/fixtures/react-library-example/src/styles/vars.css.ts
@@ -0,0 +1,566 @@
+import { createGlobalThemeContract, createTheme } from '@vanilla-extract/css';
+
+/**
+ * Figma’s Simple Design System
+ * Licensed under Creative Commons Attribution 4.0
+ * @see https://www.figma.com/community/file/1380235722331273046
+ */
+
+export const vars = createGlobalThemeContract({
+ color: {
+ black: {
+ '100': 'color-black-100',
+ '200': 'color-black-200',
+ '300': 'color-black-300',
+ '400': 'color-black-400',
+ '500': 'color-black-500',
+ '600': 'color-black-600',
+ '700': 'color-black-700',
+ '800': 'color-black-800',
+ '900': 'color-black-900',
+ '1000': 'color-black-1000',
+ },
+ brand: {
+ '100': 'color-brand-100',
+ '200': 'color-brand-200',
+ '300': 'color-brand-300',
+ '400': 'color-brand-400',
+ '500': 'color-brand-500',
+ '600': 'color-brand-600',
+ '700': 'color-brand-700',
+ '800': 'color-brand-800',
+ '900': 'color-brand-900',
+ '1000': 'color-brand-1000',
+ },
+ gray: {
+ '100': 'color-gray-100',
+ '200': 'color-gray-200',
+ '300': 'color-gray-300',
+ '400': 'color-gray-400',
+ '500': 'color-gray-500',
+ '600': 'color-gray-600',
+ '700': 'color-gray-700',
+ '800': 'color-gray-800',
+ '900': 'color-gray-900',
+ '1000': 'color-gray-1000',
+ },
+ green: {
+ '100': 'color-green-100',
+ '200': 'color-green-200',
+ '300': 'color-green-300',
+ '400': 'color-green-400',
+ '500': 'color-green-500',
+ '600': 'color-green-600',
+ '700': 'color-green-700',
+ '800': 'color-green-800',
+ '900': 'color-green-900',
+ '1000': 'color-green-1000',
+ },
+ pink: {
+ '100': 'color-pink-100',
+ '200': 'color-pink-200',
+ '300': 'color-pink-300',
+ '400': 'color-pink-400',
+ '500': 'color-pink-500',
+ '600': 'color-pink-600',
+ '700': 'color-pink-700',
+ '800': 'color-pink-800',
+ '900': 'color-pink-900',
+ '1000': 'color-pink-1000',
+ },
+ red: {
+ '100': 'color-red-100',
+ '200': 'color-red-200',
+ '300': 'color-red-300',
+ '400': 'color-red-400',
+ '500': 'color-red-500',
+ '600': 'color-red-600',
+ '700': 'color-red-700',
+ '800': 'color-red-800',
+ '900': 'color-red-900',
+ '1000': 'color-red-1000',
+ },
+ slate: {
+ '100': 'color-slate-100',
+ '200': 'color-slate-200',
+ '300': 'color-slate-300',
+ '400': 'color-slate-400',
+ '500': 'color-slate-500',
+ '600': 'color-slate-600',
+ '700': 'color-slate-700',
+ '800': 'color-slate-800',
+ '900': 'color-slate-900',
+ '1000': 'color-slate-1000',
+ },
+ white: {
+ '100': 'color-white-100',
+ '200': 'color-white-200',
+ '300': 'color-white-300',
+ '400': 'color-white-400',
+ '500': 'color-white-500',
+ '600': 'color-white-600',
+ '700': 'color-white-700',
+ '800': 'color-white-800',
+ '900': 'color-white-900',
+ '1000': 'color-white-1000',
+ },
+ yellow: {
+ '100': 'color-yellow-100',
+ '200': 'color-yellow-200',
+ '300': 'color-yellow-300',
+ '400': 'color-yellow-400',
+ '500': 'color-yellow-500',
+ '600': 'color-yellow-600',
+ '700': 'color-yellow-700',
+ '800': 'color-yellow-800',
+ '900': 'color-yellow-900',
+ '1000': 'color-yellow-1000',
+ },
+ background: {
+ brand: 'color-background-brand-default',
+ default: 'color-background-default-default',
+ },
+ border: {
+ default: 'color-border-default-default',
+ },
+ text: {
+ brand: 'color-text-brand-default',
+ default: 'color-text-default-default',
+ },
+ },
+ size: {
+ blur: {
+ '100': 'size-blur-100',
+ },
+ depth: {
+ '0': 'size-depth-0',
+ '025': 'size-depth-025',
+ '100': 'size-depth-100',
+ '200': 'size-depth-200',
+ '400': 'size-depth-400',
+ '800': 'size-depth-800',
+ '1200': 'size-depth-1200',
+ negative025: 'size-depth-negative-025',
+ negative100: 'size-depth-negative-100',
+ negative200: 'size-depth-negative-200',
+ negative400: 'size-depth-negative-400',
+ negative800: 'size-depth-negative-800',
+ negative1200: 'size-depth-negative-1200',
+ },
+ icon: {
+ small: 'size-icon-small',
+ medium: 'size-icon-medium',
+ large: 'size-icon-large',
+ },
+ radius: {
+ '100': 'size-radius-100',
+ '200': 'size-radius-200',
+ '400': 'size-radius-400',
+ full: 'size-radius-full',
+ },
+ space: {
+ '0': 'size-space-0',
+ '050': 'size-space-050',
+ '100': 'size-space-100',
+ '150': 'size-space-150',
+ '200': 'size-space-200',
+ '300': 'size-space-300',
+ '400': 'size-space-400',
+ '600': 'size-space-600',
+ '800': 'size-space-800',
+ '1200': 'size-space-1200',
+ '1600': 'size-space-1600',
+ '2400': 'size-space-2400',
+ '4000': 'size-space-4000',
+ negative100: 'size-space-negative-100',
+ negative200: 'size-space-negative-200',
+ negative300: 'size-space-negative-300',
+ negative400: 'size-space-negative-400',
+ negative600: 'size-space-negative-600',
+ },
+ stroke: {
+ border: 'size-stroke-border',
+ focusRing: 'size-stroke-focus-ring',
+ },
+ },
+ typography: {
+ body: {
+ small: {
+ fontFamily: 'typography-body-small-font-family',
+ fontSize: 'typography-body-small-font-size',
+ fontWeight: 'typography-body-small-font-weight',
+ },
+ medium: {
+ fontFamily: 'typography-body-medium-font-family',
+ fontSize: 'typography-body-medium-font-size',
+ fontWeight: 'typography-body-medium-font-weight',
+ },
+ large: {
+ fontFamily: 'typography-body-large-font-family',
+ fontSize: 'typography-body-large-font-size',
+ fontWeight: 'typography-body-large-font-weight',
+ },
+ },
+ code: {
+ small: {
+ fontFamily: 'typography-code-small-font-family',
+ fontSize: 'typography-code-small-font-size',
+ fontWeight: 'typography-code-small-font-weight',
+ },
+ medium: {
+ fontFamily: 'typography-code-medium-font-family',
+ fontSize: 'typography-code-medium-font-size',
+ fontWeight: 'typography-code-medium-font-weight',
+ },
+ large: {
+ fontFamily: 'typography-code-large-font-family',
+ fontSize: 'typography-code-large-font-size',
+ fontWeight: 'typography-code-large-font-weight',
+ },
+ },
+ family: {
+ mono: 'typography-family-mono',
+ sans: 'typography-family-sans',
+ serif: 'typography-family-serif',
+ },
+ scale: {
+ '10': 'typography-scale-10',
+ '01': 'typography-scale-01',
+ '02': 'typography-scale-02',
+ '03': 'typography-scale-03',
+ '04': 'typography-scale-04',
+ '05': 'typography-scale-05',
+ '06': 'typography-scale-06',
+ '07': 'typography-scale-07',
+ '08': 'typography-scale-08',
+ '09': 'typography-scale-09',
+ },
+ weight: {
+ thin: 'typography-weight-thin',
+ extralight: 'typography-weight-extralight',
+ light: 'typography-weight-light',
+ regular: 'typography-weight-regular',
+ medium: 'typography-weight-medium',
+ semibold: 'typography-weight-semibold',
+ bold: 'typography-weight-bold',
+ extrabold: 'typography-weight-extrabold',
+ black: 'typography-weight-black',
+ },
+ },
+});
+
+createTheme(vars, {
+ color: {
+ black: {
+ '100':
+ 'color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744 / 0.050980392156862744)',
+ '200':
+ 'color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744 / 0.10196078431372549)',
+ '300':
+ 'color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744 / 0.2)',
+ '400':
+ 'color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744 / 0.4)',
+ '500':
+ 'color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744 / 0.6980392156862745)',
+ '600':
+ 'color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744 / 0.8)',
+ '700':
+ 'color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744 / 0.8509803921568627)',
+ '800':
+ 'color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744 / 0.8980392156862745)',
+ '900':
+ 'color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744 / 0.9490196078431372)',
+ '1000':
+ 'color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744)',
+ },
+ brand: {
+ '100':
+ 'color(srgb 0.9607843137254902 0.9607843137254902 0.9607843137254902)',
+ '200':
+ 'color(srgb 0.9019607843137255 0.9019607843137255 0.9019607843137255)',
+ '300':
+ 'color(srgb 0.8509803921568627 0.8509803921568627 0.8509803921568627)',
+ '400':
+ 'color(srgb 0.7019607843137254 0.7019607843137254 0.7019607843137254)',
+ '500':
+ 'color(srgb 0.4588235294117647 0.4588235294117647 0.4588235294117647)',
+ '600':
+ 'color(srgb 0.26666666666666666 0.26666666666666666 0.26666666666666666)',
+ '700':
+ 'color(srgb 0.2196078431372549 0.2196078431372549 0.2196078431372549)',
+ '800':
+ 'color(srgb 0.17254901960784313 0.17254901960784313 0.17254901960784313)',
+ '900':
+ 'color(srgb 0.11764705882352941 0.11764705882352941 0.11764705882352941)',
+ '1000':
+ 'color(srgb 0.06666666666666667 0.06666666666666667 0.06666666666666667)',
+ },
+ gray: {
+ '100':
+ 'color(srgb 0.9607843137254902 0.9607843137254902 0.9607843137254902)',
+ '200':
+ 'color(srgb 0.9019607843137255 0.9019607843137255 0.9019607843137255)',
+ '300':
+ 'color(srgb 0.8509803921568627 0.8509803921568627 0.8509803921568627)',
+ '400':
+ 'color(srgb 0.7019607843137254 0.7019607843137254 0.7019607843137254)',
+ '500':
+ 'color(srgb 0.4588235294117647 0.4588235294117647 0.4588235294117647)',
+ '600':
+ 'color(srgb 0.26666666666666666 0.26666666666666666 0.26666666666666666)',
+ '700':
+ 'color(srgb 0.2196078431372549 0.2196078431372549 0.2196078431372549)',
+ '800':
+ 'color(srgb 0.17254901960784313 0.17254901960784313 0.17254901960784313)',
+ '900':
+ 'color(srgb 0.11764705882352941 0.11764705882352941 0.11764705882352941)',
+ '1000':
+ 'color(srgb 0.06666666666666667 0.06666666666666667 0.06666666666666667)',
+ },
+ green: {
+ '100': 'color(srgb 0.9215686274509803 1 0.9333333333333333)',
+ '200':
+ 'color(srgb 0.8117647058823529 0.9686274509803922 0.8274509803921568)',
+ '300':
+ 'color(srgb 0.6862745098039216 0.9568627450980393 0.7764705882352941)',
+ '400':
+ 'color(srgb 0.5215686274509804 0.8784313725490196 0.6392156862745098)',
+ '500':
+ 'color(srgb 0.0784313725490196 0.6823529411764706 0.3607843137254902)',
+ '600': 'color(srgb 0 0.6 0.3176470588235294)',
+ '700': 'color(srgb 0 0.5019607843137255 0.2627450980392157)',
+ '800':
+ 'color(srgb 0.00784313725490196 0.32941176470588235 0.17647058823529413)',
+ '900':
+ 'color(srgb 0.00784313725490196 0.25098039215686274 0.13725490196078433)',
+ '1000':
+ 'color(srgb 0.023529411764705882 0.17647058823529413 0.10588235294117647)',
+ },
+ pink: {
+ '100':
+ 'color(srgb 0.9882352941176471 0.9450980392156862 0.9921568627450981)',
+ '200':
+ 'color(srgb 0.9803921568627451 0.8823529411764706 0.9803921568627451)',
+ '300':
+ 'color(srgb 0.9607843137254902 0.7529411764705882 0.9372549019607843)',
+ '400':
+ 'color(srgb 0.9450980392156862 0.6196078431372549 0.8627450980392157)',
+ '500':
+ 'color(srgb 0.9176470588235294 0.24705882352941178 0.7215686274509804)',
+ '600':
+ 'color(srgb 0.8431372549019608 0.19607843137254902 0.6588235294117647)',
+ '700':
+ 'color(srgb 0.7294117647058823 0.16470588235294117 0.5725490196078431)',
+ '800':
+ 'color(srgb 0.5411764705882353 0.13333333333333333 0.43529411764705883)',
+ '900':
+ 'color(srgb 0.3411764705882353 0.09411764705882353 0.2901960784313726)',
+ '1000':
+ 'color(srgb 0.24705882352941178 0.08235294117647059 0.21176470588235294)',
+ },
+ red: {
+ '100':
+ 'color(srgb 0.996078431372549 0.9137254901960784 0.9058823529411765)',
+ '200':
+ 'color(srgb 0.9921568627450981 0.8274509803921568 0.8156862745098039)',
+ '300':
+ 'color(srgb 0.9882352941176471 0.7019607843137254 0.6784313725490196)',
+ '400':
+ 'color(srgb 0.9568627450980393 0.4666666666666667 0.41568627450980394)',
+ '500':
+ 'color(srgb 0.9254901960784314 0.13333333333333333 0.12156862745098039)',
+ '600':
+ 'color(srgb 0.7529411764705882 0.058823529411764705 0.047058823529411764)',
+ '700':
+ 'color(srgb 0.5647058823529412 0.043137254901960784 0.03529411764705882)',
+ '800':
+ 'color(srgb 0.4117647058823529 0.03137254901960784 0.027450980392156862)',
+ '900':
+ 'color(srgb 0.30196078431372547 0.043137254901960784 0.0392156862745098)',
+ '1000':
+ 'color(srgb 0.18823529411764706 0.023529411764705882 0.011764705882352941)',
+ },
+ slate: {
+ '100':
+ 'color(srgb 0.9529411764705882 0.9529411764705882 0.9529411764705882)',
+ '200':
+ 'color(srgb 0.8901960784313725 0.8901960784313725 0.8901960784313725)',
+ '300':
+ 'color(srgb 0.803921568627451 0.803921568627451 0.803921568627451)',
+ '400':
+ 'color(srgb 0.6980392156862745 0.6980392156862745 0.6980392156862745)',
+ '500':
+ 'color(srgb 0.5803921568627451 0.5803921568627451 0.5803921568627451)',
+ '600':
+ 'color(srgb 0.4627450980392157 0.4627450980392157 0.4627450980392157)',
+ '700':
+ 'color(srgb 0.35294117647058826 0.35294117647058826 0.35294117647058826)',
+ '800':
+ 'color(srgb 0.2627450980392157 0.2627450980392157 0.2627450980392157)',
+ '900':
+ 'color(srgb 0.18823529411764706 0.18823529411764706 0.18823529411764706)',
+ '1000':
+ 'color(srgb 0.1411764705882353 0.1411764705882353 0.1411764705882353)',
+ },
+ white: {
+ '100': 'color(srgb 1 1 1 / 0.050980392156862744)',
+ '200': 'color(srgb 1 1 1 / 0.10196078431372549)',
+ '300': 'color(srgb 1 1 1 / 0.2)',
+ '400': 'color(srgb 1 1 1 / 0.4)',
+ '500': 'color(srgb 1 1 1 / 0.6980392156862745)',
+ '600': 'color(srgb 1 1 1 / 0.8)',
+ '700': 'color(srgb 1 1 1 / 0.8509803921568627)',
+ '800': 'color(srgb 1 1 1 / 0.8980392156862745)',
+ '900': 'color(srgb 1 1 1 / 0.9490196078431372)',
+ '1000': 'color(srgb 1 1 1)',
+ },
+ yellow: {
+ '100': 'color(srgb 1 0.984313725490196 0.9215686274509803)',
+ '200': 'color(srgb 1 0.9450980392156862 0.7607843137254902)',
+ '300': 'color(srgb 1 0.9098039215686274 0.6392156862745098)',
+ '400':
+ 'color(srgb 0.9098039215686274 0.7254901960784313 0.19215686274509805)',
+ '500': 'color(srgb 0.8980392156862745 0.6274509803921569 0)',
+ '600':
+ 'color(srgb 0.7490196078431373 0.41568627450980394 0.00784313725490196)',
+ '700':
+ 'color(srgb 0.592156862745098 0.3176470588235294 0.00784313725490196)',
+ '800':
+ 'color(srgb 0.40784313725490196 0.17647058823529413 0.011764705882352941)',
+ '900':
+ 'color(srgb 0.3215686274509804 0.1450980392156863 0.01568627450980392)',
+ '1000':
+ 'color(srgb 0.25098039215686274 0.10588235294117647 0.00392156862745098)',
+ },
+ background: {
+ brand: vars.color.brand[800],
+ default: vars.color.white[1000],
+ },
+ border: {
+ default: vars.color.gray[300],
+ },
+ text: {
+ brand: vars.color.brand[800],
+ default: vars.color.gray[900],
+ },
+ },
+ size: {
+ blur: {
+ '100': '0.25rem',
+ },
+ depth: {
+ '0': '0',
+ '025': '0.0625rem',
+ '100': '0.25rem',
+ '200': '0.5rem',
+ '400': '1rem',
+ '800': '2rem',
+ '1200': '3rem',
+ negative025: '-0.0625rem',
+ negative100: '-0.25rem',
+ negative200: '-0.5rem',
+ negative400: '-1rem',
+ negative800: '-2rem',
+ negative1200: '-3rem',
+ },
+ icon: {
+ small: '1.5rem',
+ medium: '2rem',
+ large: '2.5rem',
+ },
+ radius: {
+ '100': '0.25rem',
+ '200': '0.5rem',
+ '400': '1rem',
+ full: '624.9375rem',
+ },
+ space: {
+ '0': '0',
+ '050': '0.125rem',
+ '100': '0.25rem',
+ '150': '0.375rem',
+ '200': '0.5rem',
+ '300': '0.75rem',
+ '400': '1rem',
+ '600': '1.5rem',
+ '800': '2rem',
+ '1200': '3rem',
+ '1600': '4rem',
+ '2400': '6rem',
+ '4000': '0',
+ negative100: '-0.25rem',
+ negative200: '-0.5rem',
+ negative300: '-0.75rem',
+ negative400: '-1rem',
+ negative600: '-1.5rem',
+ },
+ stroke: {
+ border: '0.0625rem',
+ focusRing: '0.125rem',
+ },
+ },
+ typography: {
+ body: {
+ small: {
+ fontFamily: vars.typography.family.sans,
+ fontSize: vars.typography.scale['02'],
+ fontWeight: vars.typography.weight.regular,
+ },
+ medium: {
+ fontFamily: vars.typography.family.sans,
+ fontSize: vars.typography.scale['03'],
+ fontWeight: vars.typography.weight.regular,
+ },
+ large: {
+ fontFamily: vars.typography.family.sans,
+ fontSize: vars.typography.scale['04'],
+ fontWeight: vars.typography.weight.regular,
+ },
+ },
+ code: {
+ small: {
+ fontFamily: vars.typography.family.mono,
+ fontSize: vars.typography.scale['02'],
+ fontWeight: vars.typography.weight.regular,
+ },
+ medium: {
+ fontFamily: vars.typography.family.mono,
+ fontSize: vars.typography.scale['03'],
+ fontWeight: vars.typography.weight.regular,
+ },
+ large: {
+ fontFamily: vars.typography.family.mono,
+ fontSize: vars.typography.scale['04'],
+ fontWeight: vars.typography.weight.regular,
+ },
+ },
+ family: {
+ mono: '"roboto mono", monospace',
+ sans: 'inter, sans-serif',
+ serif: '"noto serif", serif',
+ },
+ scale: {
+ '01': '0.75rem',
+ '02': '0.875rem',
+ '03': '1rem',
+ '04': '1.25rem',
+ '05': '1.5rem',
+ '06': '2rem',
+ '07': '2.5rem',
+ '08': '3rem',
+ '09': '4rem',
+ '10': '4.5rem',
+ },
+ weight: {
+ thin: '100',
+ extralight: '200',
+ light: '300',
+ regular: '400',
+ medium: '500',
+ semibold: '600',
+ bold: '700',
+ extrabold: '800',
+ black: '900',
+ },
+ },
+});
diff --git a/fixtures/react-library-example/tsconfig.json b/fixtures/react-library-example/tsconfig.json
new file mode 100644
index 000000000..b026eaa7a
--- /dev/null
+++ b/fixtures/react-library-example/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "baseUrl": "src",
+ "module": "NodeNext",
+ "moduleResolution": "nodenext",
+ "incremental": true,
+ "strict": true,
+ "jsx": "react-jsx",
+ "verbatimModuleSyntax": true
+ },
+ "include": ["src"]
+}
diff --git a/packages/rollup-plugin/package.json b/packages/rollup-plugin/package.json
index cbbeb14c8..ff04d361e 100644
--- a/packages/rollup-plugin/package.json
+++ b/packages/rollup-plugin/package.json
@@ -16,7 +16,8 @@
"author": "SEEK",
"license": "MIT",
"dependencies": {
- "@vanilla-extract/integration": "workspace:^"
+ "@vanilla-extract/integration": "workspace:^",
+ "magic-string": "^0.30.17"
},
"devDependencies": {
"@fixtures/themed": "workspace:*",
diff --git a/packages/rollup-plugin/src/index.ts b/packages/rollup-plugin/src/index.ts
index 41cbd45c4..84764be9c 100644
--- a/packages/rollup-plugin/src/index.ts
+++ b/packages/rollup-plugin/src/index.ts
@@ -9,31 +9,74 @@ import {
type CompileOptions,
} from '@vanilla-extract/integration';
import { posix } from 'path';
+import { generateCssBundle, stripSideEffectImportsMatching } from './lib';
const { relative, normalize, dirname } = posix;
-interface Options {
+export interface Options {
+ /**
+ * Different formatting of identifiers (e.g. class names, keyframes, CSS Vars, etc) can be configured by selecting from the following options:
+ * - "short": 7+ character hash. e.g. hnw5tz3
+ * - "debug": human readable prefixes representing the owning filename and a potential rule level debug name. e.g. myfile_mystyle_hnw5tz3
+ * - custom function: takes an object parameter with `hash`, `filePath`, `debugId`, and `packageName`, and returns a customized identifier.
+ * @default "short"
+ * @example ({ hash }) => `prefix_${hash}`
+ */
identifiers?: IdentifierOption;
+ /**
+ * Current working directory
+ * @default process.cwd()
+ */
cwd?: string;
+ /**
+ * Options forwarded to esbuild
+ * @see https://esbuild.github.io/
+ */
esbuildOptions?: CompileOptions['esbuildOptions'];
+ /**
+ * Extract .css bundle to a specified filename
+ * @default false
+ */
+ extract?:
+ | {
+ /**
+ * Name of emitted .css file.
+ * @default "bundle.css"
+ */
+ name?: string;
+ /**
+ * Generate a .css.map file?
+ * @default false
+ */
+ sourcemap?: boolean;
+ }
+ | false;
}
+
export function vanillaExtractPlugin({
identifiers,
cwd = process.cwd(),
esbuildOptions,
+ extract = false,
}: Options = {}): Plugin {
const isProduction = process.env.NODE_ENV === 'production';
+ let extractedCssIds = new Set(); // only for `extract`
+
return {
name: 'vanilla-extract',
+
+ buildStart() {
+ extractedCssIds = new Set(); // refresh every build
+ },
+
// Transform .css.js to .js
async transform(_code, id) {
if (!cssFileFilter.test(id)) {
return null;
}
- const index = id.indexOf('?');
- const filePath = index === -1 ? id : id.substring(0, index);
+ const [filePath] = id.split('?');
const identOption = identifiers ?? (isProduction ? 'short' : 'debug');
@@ -58,6 +101,7 @@ export function vanillaExtractPlugin({
map: { mappings: '' },
};
},
+
// Resolve .css to external module
async resolveId(id) {
if (!virtualCssFileFilter.test(id)) {
@@ -72,10 +116,11 @@ export function vanillaExtractPlugin({
},
};
},
+
// Emit .css assets
moduleParsed(moduleInfo) {
moduleInfo.importedIdResolutions.forEach((resolution) => {
- if (resolution.meta.css) {
+ if (resolution.meta.css && !extract) {
resolution.meta.assetId = this.emitFile({
type: 'asset',
name: resolution.id,
@@ -84,6 +129,7 @@ export function vanillaExtractPlugin({
}
});
},
+
// Replace .css import paths with relative paths to emitted css files
renderChunk(code, chunkInfo) {
const chunkPath = dirname(chunkInfo.fileName);
@@ -92,6 +138,7 @@ export function vanillaExtractPlugin({
if (!moduleInfo?.meta.assetId) {
return codeResult;
}
+
const assetPath = this.getFileName(moduleInfo?.meta.assetId);
const relativeAssetPath = `./${normalize(
relative(chunkPath, assetPath),
@@ -104,5 +151,52 @@ export function vanillaExtractPlugin({
map: null,
};
},
+
+ // Generate bundle (if extracting)
+ async buildEnd() {
+ if (!extract) {
+ return;
+ }
+ // Note: generateBundle() can’t happen earlier than buildEnd
+ // because the graph hasn’t fully settled until this point.
+ const { bundle, extractedCssIds: extractedIds } = generateCssBundle(this);
+ extractedCssIds = extractedIds;
+ const name = extract.name || 'bundle.css';
+ this.emitFile({
+ type: 'asset',
+ name,
+ originalFileName: name,
+ source: bundle.toString(),
+ });
+ if (extract.sourcemap) {
+ const sourcemapName = `${name}.map`;
+ this.emitFile({
+ type: 'asset',
+ name: sourcemapName,
+ originalFileName: sourcemapName,
+ source: bundle.generateMap({ file: name }).toString(),
+ });
+ }
+ },
+
+ // Remove side effect imports (if extracting)
+ async generateBundle(_options, bundle) {
+ if (!extract) {
+ return;
+ }
+ await Promise.all(
+ Object.entries(bundle).map(async ([id, chunk]) => {
+ if (
+ chunk.type === 'chunk' &&
+ id.endsWith('.js') &&
+ chunk.imports.some((specifier) => extractedCssIds.has(specifier))
+ ) {
+ chunk.code = await stripSideEffectImportsMatching(chunk.code, [
+ ...extractedCssIds,
+ ]);
+ }
+ }),
+ );
+ },
};
}
diff --git a/packages/rollup-plugin/src/lib.ts b/packages/rollup-plugin/src/lib.ts
new file mode 100644
index 000000000..ca711afc6
--- /dev/null
+++ b/packages/rollup-plugin/src/lib.ts
@@ -0,0 +1,124 @@
+import { cssFileFilter } from '@vanilla-extract/integration';
+import MagicString, { Bundle as MagicStringBundle } from 'magic-string';
+import type { ModuleInfo, PluginContext } from 'rollup';
+
+/** Generate a CSS bundle from Rollup context */
+export function generateCssBundle({
+ getModuleIds,
+ getModuleInfo,
+ warn,
+}: Pick): {
+ bundle: MagicStringBundle;
+ extractedCssIds: Set;
+} {
+ const cssBundle = new MagicStringBundle();
+ const extractedCssIds = new Set();
+
+ // 1. identify CSS files to bundle
+ const cssFiles: Record = {};
+ for (const id of getModuleIds()) {
+ if (cssFileFilter.test(id)) {
+ cssFiles[id] = buildImportChain(id, { getModuleInfo, warn });
+ }
+ }
+
+ // 2. build bundle from import order
+ for (const id of sortModules(cssFiles)) {
+ const { importedIdResolutions } = getModuleInfo(id) ?? {};
+ for (const resolution of importedIdResolutions ?? []) {
+ if (resolution.meta.css && !extractedCssIds.has(resolution.id)) {
+ extractedCssIds.add(resolution.id);
+ cssBundle.addSource({
+ filename: resolution.id,
+ content: new MagicString(resolution.meta.css),
+ });
+ }
+ }
+ }
+
+ return { bundle: cssBundle, extractedCssIds };
+}
+
+/** [id, order] tuple meant for ordering imports */
+export type ImportChain = [id: string, order: number][];
+
+/** Trace a file back through its importers, building an ordered list */
+export function buildImportChain(
+ id: string,
+ { getModuleInfo, warn }: Pick,
+): ImportChain {
+ let mod: ModuleInfo | null = getModuleInfo(id)!;
+ if (!mod) {
+ return [];
+ }
+ /** [id, order] */
+ const chain: ImportChain = [[id, -1]];
+ // resolve upwards to root entry
+ while (!mod.isEntry) {
+ const { id: currentId, importers } = mod;
+ const lastImporterId = importers.at(-1);
+ if (!lastImporterId) {
+ break;
+ }
+ if (chain.some(([id]) => id === lastImporterId)) {
+ warn(
+ `Circular import detected. Can’t determine ideal import order of module.\n${chain
+ .reverse()
+ .join('\n → ')}`,
+ );
+ break;
+ }
+ mod = getModuleInfo(lastImporterId);
+ if (!mod) {
+ break;
+ }
+ // importedIds preserves the import order within each module
+ chain.push([lastImporterId, mod.importedIds.indexOf(currentId)]);
+ }
+ return chain.reverse();
+}
+
+/** Compare import chains to determine a flat ordering for modules */
+export function sortModules(modules: Record): string[] {
+ const sortedModules = Object.entries(modules);
+
+ // 2. sort CSS by import order
+ sortedModules.sort(([_idA, chainA], [_idB, chainB]) => {
+ const shorterChain = Math.min(chainA.length, chainB.length);
+ for (let i = 0; i < shorterChain; i++) {
+ const [moduleA, orderA] = chainA[i];
+ const [moduleB, orderB] = chainB[i];
+ // on same node, continue to next one
+ if (moduleA === moduleB && orderA === orderB) {
+ continue;
+ }
+ if (orderA !== orderB) {
+ return orderA - orderB;
+ }
+ }
+ return 0;
+ });
+
+ return sortedModules.map(([id]) => id);
+}
+
+const SIDE_EFFECT_IMPORT_RE = /^\s*import\s+['"]([^'"]+)['"]\s*;?\s*/gm;
+
+/** Remove specific side effect imports from JS */
+export function stripSideEffectImportsMatching(
+ code: string,
+ sources: string[],
+): string {
+ const matches = code.matchAll(SIDE_EFFECT_IMPORT_RE);
+ if (!matches) {
+ return code;
+ }
+ let output = code;
+ for (const match of matches) {
+ if (!match[1] || !sources.includes(match[1])) {
+ continue;
+ }
+ output = output.replace(match[0], '');
+ }
+ return output;
+}
diff --git a/packages/rollup-plugin/test/__snapshots__/rollup-plugin.test.ts.snap b/packages/rollup-plugin/test/__snapshots__/rollup-plugin.test.ts.snap
index d0b548da0..67a42ac12 100644
--- a/packages/rollup-plugin/test/__snapshots__/rollup-plugin.test.ts.snap
+++ b/packages/rollup-plugin/test/__snapshots__/rollup-plugin.test.ts.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`rollup-plugin should build with preserveModules 1`] = `
+exports[`rollup-plugin Rollup settings should build with preserveModules 1`] = `
[
[
"assets/src/shared.css.ts.vanilla-G_Gyt4-e.css",
@@ -277,7 +277,7 @@ export { altButton, altContainer, testNodes as default, dynamicVarsButton, dynam
]
`;
-exports[`rollup-plugin should build with preserveModules and assetFileNames 1`] = `
+exports[`rollup-plugin Rollup settings should build with preserveModules and assetFileNames 1`] = `
[
[
"index.js",
@@ -554,7 +554,7 @@ export { altTheme, responsiveTheme, theme, vars };
]
`;
-exports[`rollup-plugin should build with sourcemaps 1`] = `
+exports[`rollup-plugin Rollup settings should build with sourcemaps 1`] = `
[
[
"assets/src/shared.css.ts.vanilla-G_Gyt4-e.css",
@@ -611,7 +611,7 @@ exports[`rollup-plugin should build with sourcemaps 1`] = `
]
`;
-exports[`rollup-plugin should build without preserveModules 1`] = `
+exports[`rollup-plugin Rollup settings should build without preserveModules 1`] = `
[
[
"assets/src/shared.css.ts.vanilla-G_Gyt4-e.css",
@@ -858,3 +858,365 @@ render();
],
]
`;
+
+exports[`rollup-plugin options extract generates .css bundle 1`] = `
+[
+ [
+ "app.css",
+ "html, body {
+ font-size: 100%;
+ height: 100%;
+ line-height: 1;
+ margin: 0;
+}
+.vars__15dpm870 {
+ --color-black-100: color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744 / 0.050980392156862744);
+ --color-black-200: color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744 / 0.10196078431372549);
+ --color-black-300: color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744 / 0.2);
+ --color-black-400: color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744 / 0.4);
+ --color-black-500: color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744 / 0.6980392156862745);
+ --color-black-600: color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744 / 0.8);
+ --color-black-700: color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744 / 0.8509803921568627);
+ --color-black-800: color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744 / 0.8980392156862745);
+ --color-black-900: color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744 / 0.9490196078431372);
+ --color-black-1000: color(srgb 0.047058823529411764 0.047058823529411764 0.050980392156862744);
+ --color-brand-100: color(srgb 0.9607843137254902 0.9607843137254902 0.9607843137254902);
+ --color-brand-200: color(srgb 0.9019607843137255 0.9019607843137255 0.9019607843137255);
+ --color-brand-300: color(srgb 0.8509803921568627 0.8509803921568627 0.8509803921568627);
+ --color-brand-400: color(srgb 0.7019607843137254 0.7019607843137254 0.7019607843137254);
+ --color-brand-500: color(srgb 0.4588235294117647 0.4588235294117647 0.4588235294117647);
+ --color-brand-600: color(srgb 0.26666666666666666 0.26666666666666666 0.26666666666666666);
+ --color-brand-700: color(srgb 0.2196078431372549 0.2196078431372549 0.2196078431372549);
+ --color-brand-800: color(srgb 0.17254901960784313 0.17254901960784313 0.17254901960784313);
+ --color-brand-900: color(srgb 0.11764705882352941 0.11764705882352941 0.11764705882352941);
+ --color-brand-1000: color(srgb 0.06666666666666667 0.06666666666666667 0.06666666666666667);
+ --color-gray-100: color(srgb 0.9607843137254902 0.9607843137254902 0.9607843137254902);
+ --color-gray-200: color(srgb 0.9019607843137255 0.9019607843137255 0.9019607843137255);
+ --color-gray-300: color(srgb 0.8509803921568627 0.8509803921568627 0.8509803921568627);
+ --color-gray-400: color(srgb 0.7019607843137254 0.7019607843137254 0.7019607843137254);
+ --color-gray-500: color(srgb 0.4588235294117647 0.4588235294117647 0.4588235294117647);
+ --color-gray-600: color(srgb 0.26666666666666666 0.26666666666666666 0.26666666666666666);
+ --color-gray-700: color(srgb 0.2196078431372549 0.2196078431372549 0.2196078431372549);
+ --color-gray-800: color(srgb 0.17254901960784313 0.17254901960784313 0.17254901960784313);
+ --color-gray-900: color(srgb 0.11764705882352941 0.11764705882352941 0.11764705882352941);
+ --color-gray-1000: color(srgb 0.06666666666666667 0.06666666666666667 0.06666666666666667);
+ --color-green-100: color(srgb 0.9215686274509803 1 0.9333333333333333);
+ --color-green-200: color(srgb 0.8117647058823529 0.9686274509803922 0.8274509803921568);
+ --color-green-300: color(srgb 0.6862745098039216 0.9568627450980393 0.7764705882352941);
+ --color-green-400: color(srgb 0.5215686274509804 0.8784313725490196 0.6392156862745098);
+ --color-green-500: color(srgb 0.0784313725490196 0.6823529411764706 0.3607843137254902);
+ --color-green-600: color(srgb 0 0.6 0.3176470588235294);
+ --color-green-700: color(srgb 0 0.5019607843137255 0.2627450980392157);
+ --color-green-800: color(srgb 0.00784313725490196 0.32941176470588235 0.17647058823529413);
+ --color-green-900: color(srgb 0.00784313725490196 0.25098039215686274 0.13725490196078433);
+ --color-green-1000: color(srgb 0.023529411764705882 0.17647058823529413 0.10588235294117647);
+ --color-pink-100: color(srgb 0.9882352941176471 0.9450980392156862 0.9921568627450981);
+ --color-pink-200: color(srgb 0.9803921568627451 0.8823529411764706 0.9803921568627451);
+ --color-pink-300: color(srgb 0.9607843137254902 0.7529411764705882 0.9372549019607843);
+ --color-pink-400: color(srgb 0.9450980392156862 0.6196078431372549 0.8627450980392157);
+ --color-pink-500: color(srgb 0.9176470588235294 0.24705882352941178 0.7215686274509804);
+ --color-pink-600: color(srgb 0.8431372549019608 0.19607843137254902 0.6588235294117647);
+ --color-pink-700: color(srgb 0.7294117647058823 0.16470588235294117 0.5725490196078431);
+ --color-pink-800: color(srgb 0.5411764705882353 0.13333333333333333 0.43529411764705883);
+ --color-pink-900: color(srgb 0.3411764705882353 0.09411764705882353 0.2901960784313726);
+ --color-pink-1000: color(srgb 0.24705882352941178 0.08235294117647059 0.21176470588235294);
+ --color-red-100: color(srgb 0.996078431372549 0.9137254901960784 0.9058823529411765);
+ --color-red-200: color(srgb 0.9921568627450981 0.8274509803921568 0.8156862745098039);
+ --color-red-300: color(srgb 0.9882352941176471 0.7019607843137254 0.6784313725490196);
+ --color-red-400: color(srgb 0.9568627450980393 0.4666666666666667 0.41568627450980394);
+ --color-red-500: color(srgb 0.9254901960784314 0.13333333333333333 0.12156862745098039);
+ --color-red-600: color(srgb 0.7529411764705882 0.058823529411764705 0.047058823529411764);
+ --color-red-700: color(srgb 0.5647058823529412 0.043137254901960784 0.03529411764705882);
+ --color-red-800: color(srgb 0.4117647058823529 0.03137254901960784 0.027450980392156862);
+ --color-red-900: color(srgb 0.30196078431372547 0.043137254901960784 0.0392156862745098);
+ --color-red-1000: color(srgb 0.18823529411764706 0.023529411764705882 0.011764705882352941);
+ --color-slate-100: color(srgb 0.9529411764705882 0.9529411764705882 0.9529411764705882);
+ --color-slate-200: color(srgb 0.8901960784313725 0.8901960784313725 0.8901960784313725);
+ --color-slate-300: color(srgb 0.803921568627451 0.803921568627451 0.803921568627451);
+ --color-slate-400: color(srgb 0.6980392156862745 0.6980392156862745 0.6980392156862745);
+ --color-slate-500: color(srgb 0.5803921568627451 0.5803921568627451 0.5803921568627451);
+ --color-slate-600: color(srgb 0.4627450980392157 0.4627450980392157 0.4627450980392157);
+ --color-slate-700: color(srgb 0.35294117647058826 0.35294117647058826 0.35294117647058826);
+ --color-slate-800: color(srgb 0.2627450980392157 0.2627450980392157 0.2627450980392157);
+ --color-slate-900: color(srgb 0.18823529411764706 0.18823529411764706 0.18823529411764706);
+ --color-slate-1000: color(srgb 0.1411764705882353 0.1411764705882353 0.1411764705882353);
+ --color-white-100: color(srgb 1 1 1 / 0.050980392156862744);
+ --color-white-200: color(srgb 1 1 1 / 0.10196078431372549);
+ --color-white-300: color(srgb 1 1 1 / 0.2);
+ --color-white-400: color(srgb 1 1 1 / 0.4);
+ --color-white-500: color(srgb 1 1 1 / 0.6980392156862745);
+ --color-white-600: color(srgb 1 1 1 / 0.8);
+ --color-white-700: color(srgb 1 1 1 / 0.8509803921568627);
+ --color-white-800: color(srgb 1 1 1 / 0.8980392156862745);
+ --color-white-900: color(srgb 1 1 1 / 0.9490196078431372);
+ --color-white-1000: color(srgb 1 1 1);
+ --color-yellow-100: color(srgb 1 0.984313725490196 0.9215686274509803);
+ --color-yellow-200: color(srgb 1 0.9450980392156862 0.7607843137254902);
+ --color-yellow-300: color(srgb 1 0.9098039215686274 0.6392156862745098);
+ --color-yellow-400: color(srgb 0.9098039215686274 0.7254901960784313 0.19215686274509805);
+ --color-yellow-500: color(srgb 0.8980392156862745 0.6274509803921569 0);
+ --color-yellow-600: color(srgb 0.7490196078431373 0.41568627450980394 0.00784313725490196);
+ --color-yellow-700: color(srgb 0.592156862745098 0.3176470588235294 0.00784313725490196);
+ --color-yellow-800: color(srgb 0.40784313725490196 0.17647058823529413 0.011764705882352941);
+ --color-yellow-900: color(srgb 0.3215686274509804 0.1450980392156863 0.01568627450980392);
+ --color-yellow-1000: color(srgb 0.25098039215686274 0.10588235294117647 0.00392156862745098);
+ --color-background-brand-default: var(--color-brand-800);
+ --color-background-default-default: var(--color-white-1000);
+ --color-border-default-default: var(--color-gray-300);
+ --color-text-brand-default: var(--color-brand-800);
+ --color-text-default-default: var(--color-gray-900);
+ --size-blur-100: 0.25rem;
+ --size-depth-0: 0;
+ --size-depth-100: 0.25rem;
+ --size-depth-200: 0.5rem;
+ --size-depth-400: 1rem;
+ --size-depth-800: 2rem;
+ --size-depth-1200: 3rem;
+ --size-depth-025: 0.0625rem;
+ --size-depth-negative-025: -0.0625rem;
+ --size-depth-negative-100: -0.25rem;
+ --size-depth-negative-200: -0.5rem;
+ --size-depth-negative-400: -1rem;
+ --size-depth-negative-800: -2rem;
+ --size-depth-negative-1200: -3rem;
+ --size-icon-small: 1.5rem;
+ --size-icon-medium: 2rem;
+ --size-icon-large: 2.5rem;
+ --size-radius-100: 0.25rem;
+ --size-radius-200: 0.5rem;
+ --size-radius-400: 1rem;
+ --size-radius-full: 624.9375rem;
+ --size-space-0: 0;
+ --size-space-100: 0.25rem;
+ --size-space-150: 0.375rem;
+ --size-space-200: 0.5rem;
+ --size-space-300: 0.75rem;
+ --size-space-400: 1rem;
+ --size-space-600: 1.5rem;
+ --size-space-800: 2rem;
+ --size-space-1200: 3rem;
+ --size-space-1600: 4rem;
+ --size-space-2400: 6rem;
+ --size-space-4000: 0;
+ --size-space-050: 0.125rem;
+ --size-space-negative-100: -0.25rem;
+ --size-space-negative-200: -0.5rem;
+ --size-space-negative-300: -0.75rem;
+ --size-space-negative-400: -1rem;
+ --size-space-negative-600: -1.5rem;
+ --size-stroke-border: 0.0625rem;
+ --size-stroke-focus-ring: 0.125rem;
+ --typography-body-small-font-family: var(--typography-family-sans);
+ --typography-body-small-font-size: var(--typography-scale-02);
+ --typography-body-small-font-weight: var(--typography-weight-regular);
+ --typography-body-medium-font-family: var(--typography-family-sans);
+ --typography-body-medium-font-size: var(--typography-scale-03);
+ --typography-body-medium-font-weight: var(--typography-weight-regular);
+ --typography-body-large-font-family: var(--typography-family-sans);
+ --typography-body-large-font-size: var(--typography-scale-04);
+ --typography-body-large-font-weight: var(--typography-weight-regular);
+ --typography-code-small-font-family: var(--typography-family-mono);
+ --typography-code-small-font-size: var(--typography-scale-02);
+ --typography-code-small-font-weight: var(--typography-weight-regular);
+ --typography-code-medium-font-family: var(--typography-family-mono);
+ --typography-code-medium-font-size: var(--typography-scale-03);
+ --typography-code-medium-font-weight: var(--typography-weight-regular);
+ --typography-code-large-font-family: var(--typography-family-mono);
+ --typography-code-large-font-size: var(--typography-scale-04);
+ --typography-code-large-font-weight: var(--typography-weight-regular);
+ --typography-family-mono: "roboto mono", monospace;
+ --typography-family-sans: inter, sans-serif;
+ --typography-family-serif: "noto serif", serif;
+ --typography-scale-10: 4.5rem;
+ --typography-scale-01: 0.75rem;
+ --typography-scale-02: 0.875rem;
+ --typography-scale-03: 1rem;
+ --typography-scale-04: 1.25rem;
+ --typography-scale-05: 1.5rem;
+ --typography-scale-06: 2rem;
+ --typography-scale-07: 2.5rem;
+ --typography-scale-08: 3rem;
+ --typography-scale-09: 4rem;
+ --typography-weight-thin: 100;
+ --typography-weight-extralight: 200;
+ --typography-weight-light: 300;
+ --typography-weight-regular: 400;
+ --typography-weight-medium: 500;
+ --typography-weight-semibold: 600;
+ --typography-weight-bold: 700;
+ --typography-weight-extrabold: 800;
+ --typography-weight-black: 900;
+}
+.button_btn__s626q60 {
+ background: var(--color-background-brand-default);
+ border-radius: var(--size-radius-200);
+ color: var(--color-text-brand-default);
+ font-family: var(--typography-body-medium-font-family);
+ font-size: var(--typography-body-medium-font-size);
+ font-weight: var(--typography-body-medium-font-weight);
+ padding-block: var(--size-space-200);
+ padding-inline: var(--size-space-300);
+}
+.checkbox_label__8y0ume0 {
+ display: block;
+ font-family: var(--typography-body-medium-font-family);
+ font-size: var(--typography-body-medium-font-size);
+ font-weight: var(--typography-body-medium-font-weight);
+}
+.checkbox_label__8y0ume0::before {
+ content: "";
+ background: var(--color-background-brand-default);
+ border-color: var(--color-border-default-default);
+ border-width: 1px;
+ border-style: solid;
+ border-radius: var(--size-radius-200);
+ margin-right: var(--size-space-300);
+}
+.checkbox_input__8y0ume1 {
+ width: 1.5rem;
+ height: 1.5rem;
+}
+.radio_label__1uatvdb0 {
+ display: block;
+ font-family: var(--typography-body-medium-font-family);
+ font-size: var(--typography-body-medium-font-size);
+ font-weight: var(--typography-body-medium-font-weight);
+}
+.radio_label__1uatvdb0::before {
+ content: "";
+ background: var(--color-background-brand-default);
+ border-color: var(--color-border-default-default);
+ border-width: 1px;
+ border-style: solid;
+ border-radius: var(--size-radius-full);
+ margin-right: var(--size-space-300);
+}
+.radio_input__1uatvdb1 {
+ width: 1.5rem;
+ height: 1.5rem;
+}
+.utility_mt100__1vyatv80 {
+ margin-top: var(--size-space-100);
+}
+.utility_mt200__1vyatv81 {
+ margin-top: var(--size-space-200);
+}
+.utility_mt300__1vyatv82 {
+ margin-top: var(--size-space-300);
+}
+.utility_mt400__1vyatv83 {
+ margin-top: var(--size-space-400);
+}",
+ ],
+ [
+ "app.js",
+ "export { default as Button } from './button/button.js';
+export { default as Checkbox } from './checkbox/checkbox.js';
+export { default as Radio } from './radio/radio.js';
+",
+ ],
+ [
+ "button/button.css.js",
+ "var btn = "button_btn__s626q60";
+
+export { btn };
+",
+ ],
+ [
+ "button/button.js",
+ "import { jsx } from 'react/jsx-runtime';
+import clsx from 'clsx';
+import { btn } from './button.css.js';
+
+function Button({
+ className,
+ children,
+ type = "button",
+ ...props
+}) {
+ return /* @__PURE__ */ jsx("button", { ...props, type, className: clsx(btn, className), children });
+}
+
+export { Button as default };
+",
+ ],
+ [
+ "checkbox/checkbox.css.js",
+ "var input = "checkbox_input__8y0ume1";
+var label = "checkbox_label__8y0ume0";
+
+export { input, label };
+",
+ ],
+ [
+ "checkbox/checkbox.js",
+ "import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
+import { useId } from 'react';
+import clsx from 'clsx';
+import { input, label } from './checkbox.css.js';
+
+function Radio({
+ children,
+ className,
+ id,
+ ...props
+}) {
+ const randomID = useId();
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
+ /* @__PURE__ */ jsx(
+ "input",
+ {
+ ...props,
+ className: input,
+ id: id ?? randomID,
+ type: "checkbox"
+ }
+ ),
+ /* @__PURE__ */ jsx("label", { className: clsx(label, className), htmlFor: id ?? randomID, children })
+ ] });
+}
+
+export { Radio as default };
+",
+ ],
+ [
+ "radio/radio.css.js",
+ "var input = "radio_input__1uatvdb1";
+var label = "radio_label__1uatvdb0";
+
+export { input, label };
+",
+ ],
+ [
+ "radio/radio.js",
+ "import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
+import { useId } from 'react';
+import clsx from 'clsx';
+import { input, label } from './radio.css.js';
+
+function Radio({
+ children,
+ className,
+ id,
+ ...props
+}) {
+ const randomID = useId();
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
+ /* @__PURE__ */ jsx(
+ "input",
+ {
+ ...props,
+ className: input,
+ id: id ?? randomID,
+ type: "radio"
+ }
+ ),
+ /* @__PURE__ */ jsx("label", { className: clsx(label, className), htmlFor: id ?? randomID, children })
+ ] });
+}
+
+export { Radio as default };
+",
+ ],
+]
+`;
diff --git a/packages/rollup-plugin/test/rollup-plugin.test.ts b/packages/rollup-plugin/test/rollup-plugin.test.ts
index 25224323c..9daefcdea 100644
--- a/packages/rollup-plugin/test/rollup-plugin.test.ts
+++ b/packages/rollup-plugin/test/rollup-plugin.test.ts
@@ -1,29 +1,51 @@
-import { rollup, type OutputOptions } from 'rollup';
+import {
+ rollup,
+ type InputPluginOption,
+ type OutputAsset,
+ type OutputChunk,
+ type OutputOptions,
+ type RollupOptions,
+} from 'rollup';
import esbuild from 'rollup-plugin-esbuild';
import json from '@rollup/plugin-json';
import path from 'path';
-import { vanillaExtractPlugin } from '..';
+import {
+ vanillaExtractPlugin,
+ type Options as VanillaExtractPluginOptions,
+} from '..';
+import { stripSideEffectImportsMatching } from '../src/lib';
-async function build(outputOptions: OutputOptions) {
+interface BuildOptions extends VanillaExtractPluginOptions {
+ rollup: RollupOptions & { output: OutputOptions };
+}
+
+async function build({
+ rollup: rollupOptions,
+ ...pluginOptions
+}: BuildOptions) {
const bundle = await rollup({
input: require.resolve('@fixtures/themed/src/index.ts'),
+ external: ['@vanilla-extract/dynamic'],
+ ...rollupOptions,
plugins: [
- vanillaExtractPlugin({
- cwd: path.dirname(require.resolve('@fixtures/themed/package.json')),
- }),
- esbuild(),
- json(),
+ ...((rollupOptions?.plugins as InputPluginOption[]) ?? [
+ vanillaExtractPlugin({
+ cwd: path.dirname(require.resolve('@fixtures/themed/package.json')),
+ ...pluginOptions,
+ }),
+ esbuild(),
+ json(),
+ ]),
],
- external: ['@vanilla-extract/dynamic'],
});
- const { output } = await bundle.generate(outputOptions);
+ const { output } = await bundle.generate(rollupOptions.output);
output.sort((a, b) => a.fileName.localeCompare(b.fileName));
return output;
}
-async function buildAndMatchSnapshot(outputOptions: OutputOptions) {
- const output = await build(outputOptions);
+async function buildAndMatchSnapshot(options: BuildOptions) {
+ const output = await build(options);
expect(
output.map((chunkOrAsset) => [
chunkOrAsset.fileName,
@@ -33,46 +55,164 @@ async function buildAndMatchSnapshot(outputOptions: OutputOptions) {
}
describe('rollup-plugin', () => {
- it('should build without preserveModules', async () => {
- // Bundle all JS outputs together
- await buildAndMatchSnapshot({
- format: 'esm',
+ describe('options', () => {
+ it('extract generates .css bundle', async () => {
+ const cwd = path.dirname(
+ require.resolve('@fixtures/react-library-example/package.json'),
+ );
+ const output = await build({
+ cwd,
+ extract: { name: 'app.css', sourcemap: true },
+ rollup: {
+ input: { app: path.join(cwd, 'src/index.ts') },
+ external: ['clsx', 'react', 'react/jsx-runtime', 'react-dom'],
+ output: {
+ assetFileNames: '[name][extname]',
+ format: 'esm',
+ preserveModules: true, // not needed for output, just makes some assertions easier
+ },
+ },
+ });
+
+ // assert essential files were made
+ const bundleAsset = output.find(
+ (file) => file.type === 'asset' && file.fileName === 'app.css',
+ );
+ expect(bundleAsset).toBeTruthy();
+ const sourcemapAsset = output.find(
+ (file) => file.type === 'asset' && file.fileName === 'app.css.map',
+ );
+ expect(sourcemapAsset).toBeTruthy();
+
+ // assert .css imports were removed
+ const jsFiles = output.filter(
+ (file) => file.type === 'chunk' && file.fileName.endsWith('.js'),
+ ) as OutputChunk[];
+ for (const jsFile of jsFiles) {
+ expect(jsFile.code).not.toMatch(/^import .*\.css['"]/m);
+ }
+
+ // assert bundle CSS reflects order from @fixtures/react-library-example/index.ts
+ const map = JSON.parse(String((sourcemapAsset as OutputAsset).source));
+ expect(map.sources).toEqual([
+ 'src/styles/reset.css.ts.vanilla.css',
+ 'src/styles/vars.css.ts.vanilla.css',
+ 'src/button/button.css.ts.vanilla.css',
+ 'src/checkbox/checkbox.css.ts.vanilla.css',
+ 'src/radio/radio.css.ts.vanilla.css',
+ 'src/styles/utility.css.ts.vanilla.css', // this always should be last
+ ]);
+
+ // assert Vanilla CSS was stripped out
+ expect(
+ output.filter((file) => file.fileName.includes('.css.ts.vanilla')),
+ ).toHaveLength(0);
+
+ // snapshot output for everything else
+ expect(
+ output
+ .filter((chunkOrAsset) => !chunkOrAsset.fileName.endsWith('.map')) // remove .msps
+ .map((chunkOrAsset) => [
+ chunkOrAsset.fileName,
+ chunkOrAsset.type === 'asset'
+ ? chunkOrAsset.source
+ : chunkOrAsset.code,
+ ]),
+ ).toMatchSnapshot();
});
});
- it('should build with preserveModules', async () => {
- // Preserve JS modules
- await buildAndMatchSnapshot({
- format: 'esm',
- preserveModules: true,
+ describe('Rollup settings', () => {
+ it('should build without preserveModules', async () => {
+ // Bundle all JS outputs together
+ await buildAndMatchSnapshot({
+ rollup: { output: { format: 'esm' } },
+ });
});
- });
- it('should build with preserveModules and assetFileNames', async () => {
- // Preserve JS modules and place assets next to JS files instead of assets directory
- await buildAndMatchSnapshot({
- format: 'esm',
- preserveModules: true,
- preserveModulesRoot: path.dirname(
- require.resolve('@fixtures/themed/src/index.ts'),
- ),
- assetFileNames({ name }) {
- return name?.replace(/^src\//, '') ?? '';
- },
+ it('should build with preserveModules', async () => {
+ // Preserve JS modules
+ await buildAndMatchSnapshot({
+ rollup: {
+ output: {
+ format: 'esm',
+ preserveModules: true,
+ },
+ },
+ });
});
- });
- it('should build with sourcemaps', async () => {
- const output = await build({
- format: 'esm',
- preserveModules: true,
- sourcemap: true,
+ it('should build with preserveModules and assetFileNames', async () => {
+ // Preserve JS modules and place assets next to JS files instead of assets directory
+ await buildAndMatchSnapshot({
+ rollup: {
+ output: {
+ format: 'esm',
+ preserveModules: true,
+ preserveModulesRoot: path.dirname(
+ require.resolve('@fixtures/themed/src/index.ts'),
+ ),
+ assetFileNames({ name }) {
+ return name?.replace(/^src\//, '') ?? '';
+ },
+ },
+ },
+ });
});
+
+ it('should build with sourcemaps', async () => {
+ const output = await build({
+ rollup: {
+ output: {
+ format: 'esm',
+ preserveModules: true,
+ sourcemap: true,
+ },
+ },
+ });
+ expect(
+ output.map((chunkOrAsset) => [
+ chunkOrAsset.fileName,
+ chunkOrAsset.type === 'asset' ? '' : chunkOrAsset.map?.mappings,
+ ]),
+ ).toMatchSnapshot();
+ });
+ });
+});
+
+describe('stripSideEffectImportsMatching', () => {
+ it('strips only specified side effects', () => {
+ // assert all specified imports are stripped
expect(
- output.map((chunkOrAsset) => [
- chunkOrAsset.fileName,
- chunkOrAsset.type === 'asset' ? '' : chunkOrAsset.map?.mappings,
- ]),
- ).toMatchSnapshot();
+ stripSideEffectImportsMatching(
+ `import React from 'react';
+import 'button.vanilla.css';
+import './foobar.js';
+
+export default function Button() {
+ return ;
+}`,
+ ['button.vanilla.css', 'checkbox.vanilla.css', 'radio.vanilla.css'],
+ ),
+ ).toBe(
+ `import React from 'react';
+import './foobar.js';
+
+export default function Button() {
+ return ;
+}`,
+ );
+ });
+
+ it('leaves code alone if no side effects specified', () => {
+ // assert empty array returns code as expected
+ const code = `import React from 'react';
+import 'button.vanilla.css';
+import './foobar.js';
+
+export default function Button() {
+ return ;
+}`;
+ expect(stripSideEffectImportsMatching(code, [])).toBe(code);
});
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 174c1ff21..21a7bf219 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -311,6 +311,28 @@ importers:
specifier: workspace:*
version: link:../../packages/webpack-plugin
+ fixtures/react-library-example:
+ dependencies:
+ '@vanilla-extract/css':
+ specifier: workspace:*
+ version: link:../../packages/css
+ clsx:
+ specifier: ^2.1.1
+ version: 2.1.1
+ react:
+ specifier: ^18.2.0
+ version: 18.2.0
+ react-dom:
+ specifier: ^18.2.0
+ version: 18.2.0(react@18.2.0)
+ devDependencies:
+ '@types/react':
+ specifier: ^18.2.55
+ version: 18.2.55
+ '@types/react-dom':
+ specifier: ^18.2.19
+ version: 18.2.19
+
fixtures/recipes:
dependencies:
'@vanilla-extract/css':
@@ -397,7 +419,7 @@ importers:
version: 6.0.10(@types/node@22.15.3)(terser@5.26.0)(tsx@4.17.0)
vite-node:
specifier: ^3.2.2
- version: 3.2.2(@types/node@22.15.3)(terser@5.26.0)(tsx@4.17.0)
+ version: 3.2.4(@types/node@22.15.3)(terser@5.26.0)(tsx@4.17.0)
packages/css:
dependencies:
@@ -560,6 +582,9 @@ importers:
'@vanilla-extract/integration':
specifier: workspace:^
version: link:../integration
+ magic-string:
+ specifier: ^0.30.17
+ version: 0.30.17
devDependencies:
'@fixtures/themed':
specifier: workspace:*
@@ -5428,6 +5453,10 @@ packages:
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
engines: {node: '>=0.8'}
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
co@4.6.0:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
@@ -11603,8 +11632,8 @@ packages:
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
- vite-node@3.2.2:
- resolution: {integrity: sha512-Xj/jovjZvDXOq2FgLXu8NsY4uHUMWtzVmMC2LkCu9HWdr9Qu1Is5sanX3Z4jOFKdohfaWDnEJWp9pRP0vVpAcA==}
+ vite-node@3.2.4:
+ resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
@@ -17252,6 +17281,8 @@ snapshots:
clone@2.1.2: {}
+ clsx@2.1.1: {}
+
co@4.6.0: {}
code-point-at@1.1.0: {}
@@ -24584,7 +24615,7 @@ snapshots:
- tsx
- yaml
- vite-node@3.2.2(@types/node@22.15.3)(terser@5.26.0)(tsx@4.17.0):
+ vite-node@3.2.4(@types/node@22.15.3)(terser@5.26.0)(tsx@4.17.0):
dependencies:
cac: 6.7.14
debug: 4.4.1
diff --git a/site/docs/integrations/rollup.md b/site/docs/integrations/rollup.md
index d412824a8..9a8fb1b57 100644
--- a/site/docs/integrations/rollup.md
+++ b/site/docs/integrations/rollup.md
@@ -90,3 +90,21 @@ Each integration will set a default value based on the configuration options pas
esbuild is used internally to compile `.css.ts` files before evaluating them to extract styles. You can pass additional options here to customize that process.
Accepts a subset of esbuild build options (`plugins`, `external`, `define`, `loader`, `tsconfig` and `conditions`). See the [build API](https://esbuild.github.io/api/#build-api) documentation.
+
+### extract
+
+Extract all generated `.css` into one bundle. This also removes side effect `import '*.css'` statements.
+
+```ts
+vanillaExtractPlugin({
+ extract: {
+ name: 'bundle.css',
+ sourcemap: false
+ }
+});
+```
+
+| Option | Type | Default | Description |
+| :------------ | :-------- | :------------- | :-------------------------------------------------------------------------------------------------------------------------------- |
+| **name** | `string` | `'bundle.css'` | Name the bundled CSS. [output.assetFilenames](https://rollupjs.org/configuration-options/#output-assetfilenames) can affect this. |
+| **sourcemap** | `boolean` | `false` | Set to `true` to also output `.css.map` file. |