diff --git a/apps/vite-example/vite.config.js b/apps/vite-example/vite.config.js new file mode 100644 index 000000000..1447cf4fe --- /dev/null +++ b/apps/vite-example/vite.config.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * + */ +import { defineConfig } from 'vite'; +import stylexPlugin from '@stylexjs/vite-plugin'; +import path from 'path'; + +export default defineConfig({ + plugins: [stylexPlugin()], + build: { + outDir: path.join(__dirname, '.build'), + }, + optimizeDeps: { + include: ['@stylexjs/stylex'], + }, +}); diff --git a/package-lock.json b/package-lock.json index 0a9de2db7..6136039ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31849,6 +31849,7 @@ "@stylexjs/babel-plugin": "0.3.0" }, "devDependencies": { + "@stylexjs/stylex": "0.3.0", "vite": "^5.0.6" } }, @@ -36824,6 +36825,7 @@ "@babel/plugin-syntax-jsx": "^7.14.5", "@babel/plugin-syntax-typescript": "^7.14.5", "@stylexjs/babel-plugin": "0.3.0", + "@stylexjs/stylex": "0.3.0", "vite": "^5.0.6" } }, diff --git a/packages/vite-plugin/__tests__/__fixtures__/.babelrc.json b/packages/vite-plugin/__tests__/__fixtures__/.babelrc.json new file mode 100644 index 000000000..9dde4c33d --- /dev/null +++ b/packages/vite-plugin/__tests__/__fixtures__/.babelrc.json @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "ie": 11 + } + } + ] + ] +} diff --git a/packages/vite-plugin/__tests__/__fixtures__/index.js b/packages/vite-plugin/__tests__/__fixtures__/index.js new file mode 100644 index 000000000..0a6d95cd7 --- /dev/null +++ b/packages/vite-plugin/__tests__/__fixtures__/index.js @@ -0,0 +1,42 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * + */ + +// index.js + +'use strict'; + +import stylex from 'stylex'; +import otherStyles from './otherStyles'; +import npmStyles from './npmStyles'; + +const fadeAnimation = stylex.keyframes({ + '0%': { + opacity: 0.25, + }, + '100%': { + opacity: 1, + }, +}); + +const styles = stylex.create({ + foo: { + animationName: fadeAnimation, + display: 'flex', + marginStart: 10, + marginBlockStart: 99, + height: 500, + ':hover': { + background: 'red', + }, + }, +}); + +export default function App() { + return stylex(otherStyles.bar, styles.foo, npmStyles.baz); +} diff --git a/packages/vite-plugin/__tests__/__fixtures__/npmStyles.js b/packages/vite-plugin/__tests__/__fixtures__/npmStyles.js new file mode 100644 index 000000000..2772303fe --- /dev/null +++ b/packages/vite-plugin/__tests__/__fixtures__/npmStyles.js @@ -0,0 +1,24 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * + */ + +// npmStyles.js + +'use strict'; + +import stylex from 'stylex'; + +const styles = stylex.create({ + baz: { + display: 'inline', + height: 500, + width: '50%', + }, +}); + +export default styles; diff --git a/packages/vite-plugin/__tests__/__fixtures__/otherStyles.js b/packages/vite-plugin/__tests__/__fixtures__/otherStyles.js new file mode 100644 index 000000000..d196fcac4 --- /dev/null +++ b/packages/vite-plugin/__tests__/__fixtures__/otherStyles.js @@ -0,0 +1,23 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * + */ + +// otherStyles.js + +'use strict'; + +import stylex from 'stylex'; + +const styles = stylex.create({ + bar: { + display: 'block', + width: '100%', + }, +}); + +export default styles; diff --git a/packages/vite-plugin/__tests__/index-test.js b/packages/vite-plugin/__tests__/index-test.js new file mode 100644 index 000000000..d94be3582 --- /dev/null +++ b/packages/vite-plugin/__tests__/index-test.js @@ -0,0 +1,197 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * + */ + +const path = require('path'); +const stylexPlugin = require('../src/index'); + +describe('vite-stylex-plugin', () => { + async function runStylex(options) { + const vite = await import('vite'); + const bundle = await vite.build({ + build: { + lib: { + entry: path.resolve(__dirname, '__fixtures__/index.js'), + formats: ['es'], + fileName: 'bundle', + }, + cssMinify: false, + minify: false, + rollupOptions: { + external: ['stylex'], + }, + outDir: '__builds__', + write: false, + }, + plugins: [stylexPlugin(options)], + }); + // Generate output specific code in-memory + // You can call this function multiple times on the same bundle object + let css, js; + const { output } = bundle[0]; + for (const chunkOrAsset of output) { + if (chunkOrAsset.fileName === 'stylex.css') { + css = chunkOrAsset.source; + } else if (chunkOrAsset.fileName === 'bundle.mjs') { + js = chunkOrAsset.code; + } + } + return { css, js }; + } + + it('extracts CSS and removes stylex.inject calls', async () => { + const { css, js } = await runStylex({ fileName: 'stylex.css' }); + + expect(css).toMatchInlineSnapshot(` + " + @layer priority1, priority2, priority3, priority4; + @layer priority1{ + @keyframes xgnty7z-B{0%{opacity:.25;}100%{opacity:1;}} + } + @layer priority2{ + .x1oz5o6v:hover{background:red} + } + @layer priority3{ + .xeuoslp{animation-name:xgnty7z-B} + .x1lliihq{display:block} + .x78zum5{display:flex} + .xt0psk2{display:inline} + .x1hm9lzh{margin-inline-start:10px} + } + @layer priority4{ + .x1egiwwb{height:500px} + .xlrshdv{margin-top:99px} + .xh8yej3{width:100%} + .x3hqpx7{width:50%} + }" + `); + + expect(js).toMatchInlineSnapshot(` + "import stylex from "stylex"; + const styles$2 = { + bar: { + display: "x1lliihq", + width: "xh8yej3", + $$css: true + } + }; + const styles$1 = { + baz: { + display: "xt0psk2", + height: "x1egiwwb", + width: "x3hqpx7", + $$css: true + } + }; + const styles = { + foo: { + animationName: "xeuoslp", + display: "x78zum5", + marginInlineStart: "x1hm9lzh", + marginLeft: null, + marginRight: null, + marginTop: "xlrshdv", + height: "x1egiwwb", + ":hover_background": "x1oz5o6v", + ":hover_backgroundAttachment": null, + ":hover_backgroundClip": null, + ":hover_backgroundColor": null, + ":hover_backgroundImage": null, + ":hover_backgroundOrigin": null, + ":hover_backgroundPosition": null, + ":hover_backgroundPositionX": null, + ":hover_backgroundPositionY": null, + ":hover_backgroundRepeat": null, + ":hover_backgroundSize": null, + $$css: true + } + }; + function App() { + return stylex(styles$2.bar, styles.foo, styles$1.baz); + } + export { + App as default + }; + " + `); + }); + + describe('when in dev mode', () => { + it('preserves stylex.inject calls and does not extract CSS', async () => { + const { css, js } = await runStylex({ + dev: true, + }); + + expect(css).toBeUndefined(); + + expect(js).toMatchInlineSnapshot(` + "import stylex from "stylex"; + stylex.inject(".x1lliihq{display:block}", 3e3); + stylex.inject(".xh8yej3{width:100%}", 4e3); + const styles$2 = { + bar: { + "otherStyles__styles.bar": "otherStyles__styles.bar", + display: "x1lliihq", + width: "xh8yej3", + $$css: true + } + }; + stylex.inject(".xt0psk2{display:inline}", 3e3); + stylex.inject(".x1egiwwb{height:500px}", 4e3); + stylex.inject(".x3hqpx7{width:50%}", 4e3); + const styles$1 = { + baz: { + "npmStyles__styles.baz": "npmStyles__styles.baz", + display: "xt0psk2", + height: "x1egiwwb", + width: "x3hqpx7", + $$css: true + } + }; + stylex.inject("@keyframes xgnty7z-B{0%{opacity:.25;}100%{opacity:1;}}", 1); + stylex.inject(".xeuoslp{animation-name:xgnty7z-B}", 3e3); + stylex.inject(".x78zum5{display:flex}", 3e3); + stylex.inject(".x1hm9lzh{margin-inline-start:10px}", 3e3); + stylex.inject(".xlrshdv{margin-top:99px}", 4e3); + stylex.inject(".x1egiwwb{height:500px}", 4e3); + stylex.inject(".x1oz5o6v:hover{background:red}", 1130); + const styles = { + foo: { + "index__styles.foo": "index__styles.foo", + animationName: "xeuoslp", + display: "x78zum5", + marginInlineStart: "x1hm9lzh", + marginLeft: null, + marginRight: null, + marginTop: "xlrshdv", + height: "x1egiwwb", + ":hover_background": "x1oz5o6v", + ":hover_backgroundAttachment": null, + ":hover_backgroundClip": null, + ":hover_backgroundColor": null, + ":hover_backgroundImage": null, + ":hover_backgroundOrigin": null, + ":hover_backgroundPosition": null, + ":hover_backgroundPositionX": null, + ":hover_backgroundPositionY": null, + ":hover_backgroundRepeat": null, + ":hover_backgroundSize": null, + $$css: true + } + }; + function App() { + return stylex(styles$2.bar, styles.foo, styles$1.baz); + } + export { + App as default + }; + " + `); + }); + }); +}); diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index fbed6c8cb..0a4619152 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -5,7 +5,17 @@ "main": "src/index.js", "repository": "https://www.github.com/facebook/stylex", "license": "MIT", - "scripts": {}, + "scripts": { + "test": "node --experimental-vm-modules ../../node_modules/jest/bin/jest.js" + }, + "jest": { + "testPathIgnorePatterns": [ + "/node_modules/", + "__builds__", + "/__fixtures__/" + ], + "testEnvironment": "node" + }, "dependencies": { "@babel/core": "^7.15.5", "@babel/plugin-syntax-flow": "^7.18.6", @@ -14,6 +24,7 @@ "@stylexjs/babel-plugin": "0.3.0" }, "devDependencies": { - "vite": "^5.0.6" + "vite": "^5.0.6", + "@stylexjs/stylex": "0.3.0" } } \ No newline at end of file diff --git a/packages/vite-plugin/src/index.js b/packages/vite-plugin/src/index.js index f97969301..e05148d4f 100644 --- a/packages/vite-plugin/src/index.js +++ b/packages/vite-plugin/src/index.js @@ -32,6 +32,7 @@ module.exports = function stylexPlugin({ let baseDir = '/'; return { name: 'vite-plugin-stylex', + enforce: 'post', buildStart() { stylexRules = {}; }, @@ -84,13 +85,13 @@ module.exports = function stylexPlugin({ fileName, ); // vite's css post plugin handle (&|?)inline.css file and export them as js. - const res = await cssPostPlugin.transform(css, 'a&inline.css'); - res.code = res.code - .replace('export default ', '') - .substring(1, res.code.length - 1); + // const res = await cssPostPlugin.transform(css, 'a&inline.css'); + // res.code = res.code.replace('export default ', ''); + // res.code = res.code.substring(1, res.code.length - 1); + this.emitFile({ fileName, - source: res.code, + source: css, type: 'asset', }); } else { @@ -103,19 +104,21 @@ module.exports = function stylexPlugin({ } }, transformIndexHtml(html) { - return { - html, - tags: [ - { - tag: 'link', - injectTo: 'head', - attrs: { - href: path.join(baseDir, fileName), - rel: 'stylesheet', + if (!dev) { + return { + html, + tags: [ + { + tag: 'link', + injectTo: 'head', + attrs: { + href: path.join(baseDir, fileName), + rel: 'stylesheet', + }, }, - }, - ], - }; + ], + }; + } }, }; };