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. |