diff --git a/package.json b/package.json index b302e22c2..4bfab0e26 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@vitejs/vite-plugin-react-monorepo", + "name": "@dhw/vite-plugin-react-native-web-monorepo", "private": true, "type": "module", "engines": { diff --git a/packages/plugin-react/CHANGELOG.md b/packages/plugin-react-native-web/CHANGELOG.md similarity index 100% rename from packages/plugin-react/CHANGELOG.md rename to packages/plugin-react-native-web/CHANGELOG.md diff --git a/packages/plugin-react/LICENSE b/packages/plugin-react-native-web/LICENSE similarity index 100% rename from packages/plugin-react/LICENSE rename to packages/plugin-react-native-web/LICENSE diff --git a/packages/plugin-react/README.md b/packages/plugin-react-native-web/README.md similarity index 100% rename from packages/plugin-react/README.md rename to packages/plugin-react-native-web/README.md diff --git a/packages/plugin-react/build.config.ts b/packages/plugin-react-native-web/build.config.ts similarity index 100% rename from packages/plugin-react/build.config.ts rename to packages/plugin-react-native-web/build.config.ts diff --git a/packages/plugin-react/package.json b/packages/plugin-react-native-web/package.json similarity index 79% rename from packages/plugin-react/package.json rename to packages/plugin-react-native-web/package.json index f524e4e46..8ab964630 100644 --- a/packages/plugin-react/package.json +++ b/packages/plugin-react-native-web/package.json @@ -1,11 +1,10 @@ { - "name": "@vitejs/plugin-react", + "name": "@dhw/plugin-react-native-web", "version": "4.3.4", "license": "MIT", - "author": "Evan You", + "author": "Daniel Williams", "contributors": [ - "Alec Larson", - "Arnaud Barré" + "Daniel Williams" ], "files": [ "dist" @@ -41,6 +40,10 @@ "@babel/core": "^7.26.0", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "babel-plugin-react-native-web": "^0.19.13", + "@babel/plugin-transform-flow-strip-types": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.26.3", + "@babel/plugin-syntax-export-default-from": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.14.2" }, diff --git a/packages/plugin-react/scripts/copyRefreshUtils.ts b/packages/plugin-react-native-web/scripts/copyRefreshUtils.ts similarity index 100% rename from packages/plugin-react/scripts/copyRefreshUtils.ts rename to packages/plugin-react-native-web/scripts/copyRefreshUtils.ts diff --git a/packages/plugin-react/src/babel.d.ts b/packages/plugin-react-native-web/src/babel.d.ts similarity index 100% rename from packages/plugin-react/src/babel.d.ts rename to packages/plugin-react-native-web/src/babel.d.ts diff --git a/packages/plugin-react/src/fast-refresh.ts b/packages/plugin-react-native-web/src/fast-refresh.ts similarity index 97% rename from packages/plugin-react/src/fast-refresh.ts rename to packages/plugin-react-native-web/src/fast-refresh.ts index 5423b2714..a78561395 100644 --- a/packages/plugin-react/src/fast-refresh.ts +++ b/packages/plugin-react-native-web/src/fast-refresh.ts @@ -40,7 +40,7 @@ let prevRefreshSig; if (import.meta.hot && !inWebWorker) { if (!window.__vite_plugin_react_preamble_installed__) { throw new Error( - "@vitejs/plugin-react can't detect preamble. Something is wrong. " + + "@dhw/plugin-react-native-web can't detect preamble. Something is wrong. " + "See https://github.com/vitejs/vite-plugin-react/pull/11#discussion_r430879201" ); } diff --git a/packages/plugin-react/src/index.ts b/packages/plugin-react-native-web/src/index.ts similarity index 81% rename from packages/plugin-react/src/index.ts rename to packages/plugin-react-native-web/src/index.ts index c513c4e35..23a801cd5 100644 --- a/packages/plugin-react/src/index.ts +++ b/packages/plugin-react-native-web/src/index.ts @@ -82,7 +82,7 @@ type ReactBabelHookContext = { ssr: boolean; id: string } export type ViteReactPluginApi = { /** - * Manipulate the Babel options of `@vitejs/plugin-react` + * Manipulate the Babel options of `@dhw/plugin-react-native-web` */ reactBabel?: ReactBabelHook } @@ -90,12 +90,16 @@ export type ViteReactPluginApi = { const reactCompRE = /extends\s+(?:React\.)?(?:Pure)?Component/ const refreshContentRE = /\$Refresh(?:Reg|Sig)\$\(/ const defaultIncludeRE = /\.[tj]sx?$/ +const defaultExcludeRE = /\/node_modules\/(?!react-native|@react-native)/ const tsRE = /\.tsx?$/ export default function viteReact(opts: Options = {}): PluginOption[] { // Provide default values for Rollup compat. let devBase = '/' - const filter = createFilter(opts.include ?? defaultIncludeRE, opts.exclude) + const filter = createFilter( + opts.include ?? defaultIncludeRE, + opts.exclude ?? defaultExcludeRE, + ) const jsxImportSource = opts.jsxImportSource ?? 'react' const jsxImportRuntime = `${jsxImportSource}/jsx-runtime` const jsxImportDevRuntime = `${jsxImportSource}/jsx-dev-runtime` @@ -113,24 +117,76 @@ export default function viteReact(opts: Options = {}): PluginOption[] { // - import React, {useEffect} from 'react'; const importReactRE = /\bimport\s+(?:\*\s+as\s+)?React\b/ + const extensions = [ + '.web.js', + '.web.ts', + '.web.tsx', + '.web.jsx', + '.web.mjs', + '.js', + '.mjs', + '.jsx', + '.json', + '.ts', + '.tsx', + ] + + type ViteOptions = Omit + const viteBabel: Plugin = { name: 'vite:react-babel', enforce: 'pre', - config() { + config(_userConfig, env) { + const commonOptions = { + define: { + 'global.__x': {}, + _frameTimestamp: undefined, + _WORKLET: false, + __DEV__: `${env.mode === 'development'}`, + 'process.env.NODE_ENV': JSON.stringify( + process.env.NODE_ENV || env.mode, + ), + __reanimatedLoggerConfig: `{}`, + }, + optimizeDeps: { + esbuildOptions: { + jsx: 'transform', + resolveExtensions: extensions, + loader: { + '.js': 'jsx', + }, + }, + }, + resolve: { + extensions: extensions, + alias: { + 'react-native': 'react-native-web', + }, + }, + } satisfies ViteOptions + if (opts.jsxRuntime === 'classic') { return { + ...commonOptions, esbuild: { jsx: 'transform', }, - } + } satisfies ViteOptions } else { return { + ...commonOptions, esbuild: { jsx: 'automatic', jsxImportSource: opts.jsxImportSource, }, - optimizeDeps: { esbuildOptions: { jsx: 'automatic' } }, - } + optimizeDeps: { + ...commonOptions.optimizeDeps, + esbuildOptions: { + ...commonOptions.optimizeDeps.esbuildOptions, + jsx: 'automatic', + }, + }, + } satisfies ViteOptions } }, configResolved(config) { @@ -144,7 +200,7 @@ export default function viteReact(opts: Options = {}): PluginOption[] { if ('jsxPure' in opts) { config.logger.warnOnce( - '[@vitejs/plugin-react] jsxPure was removed. You can configure esbuild.jsxSideEffects directly.', + '[@dhw/plugin-react-native-web] jsxPure was removed. You can configure esbuild.jsxSideEffects directly.', ) } @@ -164,8 +220,6 @@ export default function viteReact(opts: Options = {}): PluginOption[] { } }, async transform(code, id, options) { - if (id.includes('/node_modules/')) return - const [filepath] = id.split('?') if (!filter(filepath)) return @@ -180,7 +234,23 @@ export default function viteReact(opts: Options = {}): PluginOption[] { runPluginOverrides?.(newBabelOptions, { id, ssr }) return newBabelOptions })() - const plugins = [...babelOptions.plugins] + const plugins = [ + await loadPlugin('babel-plugin-react-native-web'), + await loadPlugin('@babel/plugin-transform-flow-strip-types'), + await loadPlugin('@babel/plugin-syntax-export-default-from'), + // [ + // // this is a fix for reanimated not working in production + // '@babel/plugin-transform-modules-commonjs', + // { + // strict: false, + // strictMode: false, // prevent "use strict" injections + // allowTopLevelThis: true, // dont rewrite global `this` -> `undefined` + // }, + // ], + + // await loadPlugin('@babel/plugin-transform-modules-commonjs'), + ...babelOptions.plugins, + ] const isJSX = filepath.endsWith('x') const useFastRefresh = @@ -278,7 +348,7 @@ export default function viteReact(opts: Options = {}): PluginOption[] { jsxImportRuntime, ] const staticBabelPlugins = - typeof opts.babel === 'object' ? opts.babel?.plugins ?? [] : [] + typeof opts.babel === 'object' ? (opts.babel?.plugins ?? []) : [] const reactCompilerPlugin = getReactCompilerPlugin(staticBabelPlugins) if (reactCompilerPlugin != null) { const reactCompilerRuntimeModule = @@ -295,7 +365,7 @@ export default function viteReact(opts: Options = {}): PluginOption[] { include: dependencies, }, resolve: { - dedupe: ['react', 'react-dom'], + dedupe: ['react', 'react-dom', 'react-native', 'react-native-web'], }, }), resolveId(id) { diff --git a/packages/plugin-react/src/refreshUtils.js b/packages/plugin-react-native-web/src/refreshUtils.js similarity index 100% rename from packages/plugin-react/src/refreshUtils.js rename to packages/plugin-react-native-web/src/refreshUtils.js diff --git a/packages/plugin-react/tsconfig.json b/packages/plugin-react-native-web/tsconfig.json similarity index 100% rename from packages/plugin-react/tsconfig.json rename to packages/plugin-react-native-web/tsconfig.json diff --git a/playground/class-components/__tests__/class-components.spec.ts b/playground/class-components/__tests__/class-components.spec.ts deleted file mode 100644 index 74875caf2..000000000 --- a/playground/class-components/__tests__/class-components.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { expect, test } from 'vitest' -import { - editFile, - isServe, - page, - untilBrowserLogAfter, - untilUpdated, -} from '~utils' - -test('should render', async () => { - expect(await page.textContent('span')).toMatch('Hello World') -}) - -if (isServe) { - test('Class component HMR', async () => { - editFile('src/App.tsx', (code) => code.replace('World', 'class components')) - await untilBrowserLogAfter( - () => page.textContent('span'), - '[vite] hot updated: /src/App.tsx', - ) - await untilUpdated(() => page.textContent('span'), 'Hello class components') - - editFile('src/utils.tsx', (code) => code.replace('Hello', 'Hi')) - await untilBrowserLogAfter( - () => page.textContent('span'), - '[vite] hot updated: /src/App.tsx', - ) - await untilUpdated(() => page.textContent('span'), 'Hi class components') - }) -} diff --git a/playground/class-components/index.html b/playground/class-components/index.html deleted file mode 100644 index f8e6bda99..000000000 --- a/playground/class-components/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + class components - - -
- - - diff --git a/playground/class-components/package.json b/playground/class-components/package.json deleted file mode 100644 index 3155ac4ce..000000000 --- a/playground/class-components/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@vitejs/test-class-components", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "dependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1" - }, - "devDependencies": { - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "workspace:*" - } -} diff --git a/playground/class-components/public/vite.svg b/playground/class-components/public/vite.svg deleted file mode 100644 index 4dcd77ad0..000000000 --- a/playground/class-components/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/playground/class-components/src/App.tsx b/playground/class-components/src/App.tsx deleted file mode 100644 index 7340ce4a8..000000000 --- a/playground/class-components/src/App.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { Component } from 'react' -import { getGetting } from './utils' - -export class App extends Component { - render() { - return {getGetting()} World - } -} diff --git a/playground/class-components/src/index.tsx b/playground/class-components/src/index.tsx deleted file mode 100644 index 607cf4b4e..000000000 --- a/playground/class-components/src/index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { App } from './App' - -createRoot(document.getElementById('root')!).render( - - - , -) diff --git a/playground/class-components/src/utils.tsx b/playground/class-components/src/utils.tsx deleted file mode 100644 index 27e61766a..000000000 --- a/playground/class-components/src/utils.tsx +++ /dev/null @@ -1 +0,0 @@ -export const getGetting = () => Hello diff --git a/playground/class-components/tsconfig.json b/playground/class-components/tsconfig.json deleted file mode 100644 index c9e7d3d89..000000000 --- a/playground/class-components/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "include": ["src"], - "compilerOptions": { - "module": "ESNext", - "lib": ["ESNext", "DOM", "DOM.Iterable"], - "target": "ESNext", - "jsx": "react-jsx", - "types": ["vite/client"], - "noEmit": true, - "isolatedModules": true, - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "useUnknownInCatchVariables": true - } -} diff --git a/playground/class-components/vite.config.ts b/playground/class-components/vite.config.ts deleted file mode 100644 index c9a13540c..000000000 --- a/playground/class-components/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' - -export default defineConfig({ - server: { port: 8908 /* Should be unique */ }, - plugins: [react()], -}) diff --git a/playground/compiler-react-18/package.json b/playground/compiler-react-18/package.json index 8ad6aa91c..b09aaccc6 100644 --- a/playground/compiler-react-18/package.json +++ b/playground/compiler-react-18/package.json @@ -16,7 +16,7 @@ "@babel/plugin-transform-react-jsx-development": "^7.25.9", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "workspace:*", + "@dhw/plugin-react-native-web": "workspace:*", "babel-plugin-react-compiler": "0.0.0-experimental-dc8bd44-20241121", "typescript": "^5.7.2" } diff --git a/playground/compiler-react-18/vite.config.ts b/playground/compiler-react-18/vite.config.ts index 8850c1ac8..28879adc4 100644 --- a/playground/compiler-react-18/vite.config.ts +++ b/playground/compiler-react-18/vite.config.ts @@ -1,5 +1,5 @@ import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import react from '@dhw/plugin-react-native-web' // https://vitejs.dev/config/ export default defineConfig(({ command }) => { diff --git a/playground/compiler/package.json b/playground/compiler/package.json index 5aca9d601..1337ff1b6 100644 --- a/playground/compiler/package.json +++ b/playground/compiler/package.json @@ -15,7 +15,7 @@ "@babel/plugin-transform-react-jsx-development": "^7.25.9", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "workspace:*", + "@dhw/plugin-react-native-web": "workspace:*", "babel-plugin-react-compiler": "0.0.0-experimental-dc8bd44-20241121", "typescript": "^5.7.2" } diff --git a/playground/compiler/vite.config.ts b/playground/compiler/vite.config.ts index 2983201e8..fdcdbcbb2 100644 --- a/playground/compiler/vite.config.ts +++ b/playground/compiler/vite.config.ts @@ -1,5 +1,5 @@ import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import react from '@dhw/plugin-react-native-web' // https://vitejs.dev/config/ export default defineConfig(({ command }) => { diff --git a/playground/mdx/__tests__/mdx.spec.ts b/playground/mdx/__tests__/mdx.spec.ts deleted file mode 100644 index ccf2aa854..000000000 --- a/playground/mdx/__tests__/mdx.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { expect, test } from 'vitest' -import { - editFile, - isServe, - page, - untilBrowserLogAfter, - untilUpdated, -} from '~utils' - -test('should render', async () => { - expect(await page.textContent('h1')).toMatch('Vite + MDX') -}) - -test('.md extension should work', async () => { - expect(await page.getByText('.md extension works.').textContent()).toEqual( - '.md extension works. This is bold text.', - ) -}) - -if (isServe) { - test('should hmr', async () => { - editFile('src/demo.mdx', (code) => code.replace('Vite + MDX', 'Updated')) - await untilBrowserLogAfter( - () => page.textContent('h1'), - '[vite] hot updated: /src/demo.mdx', - ) - await untilUpdated(() => page.textContent('h1'), 'Updated') - }) - - test('should hmr with .md extension', async () => { - await untilBrowserLogAfter( - () => - editFile('src/demo2.md', (code) => - code.replace('`.md` extension works.', '`.md` extension hmr works.'), - ), - '[vite] hot updated: /src/demo2.md', - ) - await untilUpdated( - () => page.getByText('.md extension hmr works.').textContent(), - '.md extension hmr works. This is bold text.', - ) - }) -} diff --git a/playground/mdx/index.html b/playground/mdx/index.html deleted file mode 100644 index c18a002d2..000000000 --- a/playground/mdx/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + MDX - - -
- - - diff --git a/playground/mdx/package.json b/playground/mdx/package.json deleted file mode 100644 index 96939128b..000000000 --- a/playground/mdx/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "@vitejs/test-mdx", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview" - }, - "dependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1" - }, - "devDependencies": { - "@mdx-js/rollup": "^3.1.0", - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "workspace:*" - } -} diff --git a/playground/mdx/public/vite.svg b/playground/mdx/public/vite.svg deleted file mode 100644 index e7b8dfb1b..000000000 --- a/playground/mdx/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/playground/mdx/src/demo.mdx b/playground/mdx/src/demo.mdx deleted file mode 100644 index b9935d9eb..000000000 --- a/playground/mdx/src/demo.mdx +++ /dev/null @@ -1,11 +0,0 @@ -# Vite + MDX - -Sint sit cillum pariatur eiusmod nulla pariatur ipsum. - -### Unordered List - -- Olive -- Orange - - Blood orange - - Clementine -- Papaya diff --git a/playground/mdx/src/demo2.md b/playground/mdx/src/demo2.md deleted file mode 100644 index a56493179..000000000 --- a/playground/mdx/src/demo2.md +++ /dev/null @@ -1,3 +0,0 @@ -## test md extension - -`.md` extension works. This is **bold text**. diff --git a/playground/mdx/src/main.tsx b/playground/mdx/src/main.tsx deleted file mode 100644 index 2d7788204..000000000 --- a/playground/mdx/src/main.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import Demo from './demo.mdx' -import Demo2 from './demo2.md' - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - - , -) diff --git a/playground/mdx/src/vite-env.d.ts b/playground/mdx/src/vite-env.d.ts deleted file mode 100644 index 00b6972e6..000000000 --- a/playground/mdx/src/vite-env.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// - -declare module '*.mdx' { - import { JSX } from 'react' - export default () => JSX.Element -} - -declare module '*.md' { - import { JSX } from 'react' - export default () => JSX.Element -} diff --git a/playground/mdx/tsconfig.json b/playground/mdx/tsconfig.json deleted file mode 100644 index 3d0a51a86..000000000 --- a/playground/mdx/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "useDefineForClassFields": true, - "lib": ["DOM", "DOM.Iterable", "ESNext"], - "allowJs": false, - "skipLibCheck": true, - "esModuleInterop": false, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "moduleResolution": "Node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx" - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} diff --git a/playground/mdx/tsconfig.node.json b/playground/mdx/tsconfig.node.json deleted file mode 100644 index 9d31e2aed..000000000 --- a/playground/mdx/tsconfig.node.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "module": "ESNext", - "moduleResolution": "Node", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/playground/mdx/vite.config.ts b/playground/mdx/vite.config.ts deleted file mode 100644 index bc2c2b495..000000000 --- a/playground/mdx/vite.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' -import mdx from '@mdx-js/rollup' - -// https://vitejs.dev/config/ -export default defineConfig({ - server: { port: 8901 /* Should be unique */ }, - plugins: [ - { enforce: 'pre', ...mdx() }, - react({ include: /\.(mdx|md|ts|tsx)$/ }), - ], -}) diff --git a/playground/react-classic/App.jsx b/playground/react-classic/App.jsx deleted file mode 100644 index 94326dc5f..000000000 --- a/playground/react-classic/App.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import React, { useState } from 'react' - -function App() { - const [count, setCount] = useState(0) - return ( -
-
-

Hello Vite + React

-

- -

-

- Edit App.jsx and save to test HMR updates. -

- - Learn React - -
-
- ) -} - -export default App diff --git a/playground/react-classic/__tests__/react.spec.ts b/playground/react-classic/__tests__/react.spec.ts deleted file mode 100644 index 33fe7dac8..000000000 --- a/playground/react-classic/__tests__/react.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { expect, test } from 'vitest' -import { editFile, isServe, page, untilUpdated } from '~utils' - -test('should render', async () => { - expect(await page.textContent('h1')).toMatch('Hello Vite + React') -}) - -test('should update', async () => { - expect(await page.textContent('button')).toMatch('count is: 0') - await page.click('button') - expect(await page.textContent('button')).toMatch('count is: 1') -}) - -test.runIf(isServe)('should hmr', async () => { - editFile('App.jsx', (code) => code.replace('Vite + React', 'Updated')) - await untilUpdated(() => page.textContent('h1'), 'Hello Updated') - // preserve state - expect(await page.textContent('button')).toMatch('count is: 1') -}) - -test.runIf(isServe)( - 'should have annotated jsx with file location metadata', - async () => { - const meta = await page.evaluate(() => { - const button = document.querySelector('button') - const key = Object.keys(button).find( - (key) => key.indexOf('__reactFiber') === 0, - ) - return button[key]._debugSource - }) - // If the evaluate call doesn't crash, and the returned metadata has - // the expected fields, we're good. - expect(Object.keys(meta).sort()).toEqual([ - 'columnNumber', - 'fileName', - 'lineNumber', - ]) - }, -) diff --git a/playground/react-classic/index.html b/playground/react-classic/index.html deleted file mode 100644 index 7417c442d..000000000 --- a/playground/react-classic/index.html +++ /dev/null @@ -1,10 +0,0 @@ -
- diff --git a/playground/react-classic/package.json b/playground/react-classic/package.json deleted file mode 100644 index 1fe88e13a..000000000 --- a/playground/react-classic/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@vitejs/test-react-classic", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "debug": "node --inspect-brk vite", - "preview": "vite preview" - }, - "dependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1" - }, - "devDependencies": { - "@vitejs/plugin-react": "workspace:*" - }, - "babel": { - "presets": [ - "@babel/preset-env" - ] - } -} diff --git a/playground/react-classic/vite.config.ts b/playground/react-classic/vite.config.ts deleted file mode 100644 index 530e397d8..000000000 --- a/playground/react-classic/vite.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import react from '@vitejs/plugin-react' -import type { UserConfig } from 'vite' - -const config: UserConfig = { - server: { port: 8903 /* Should be unique */ }, - plugins: [ - react({ - jsxRuntime: 'classic', - }), - ], - build: { - // to make tests faster - minify: false, - }, -} - -export default config diff --git a/playground/react-emotion/App.jsx b/playground/react-emotion/App.jsx deleted file mode 100644 index b1d3f58e4..000000000 --- a/playground/react-emotion/App.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useState } from 'react' -import _Switch from 'react-switch' -import { Counter, StyledCode } from './Counter' -const Switch = _Switch.default || _Switch - -function FragmentTest() { - const [checked, setChecked] = useState(false) - return ( - <> - -

- -

- - ) -} - -function App() { - return ( -
-
-

Hello Vite + React + @emotion/react

- -

- Edit App.jsx and save to test HMR updates. -

- - Learn React - -
-
- ) -} - -export default App diff --git a/playground/react-emotion/Counter.jsx b/playground/react-emotion/Counter.jsx deleted file mode 100644 index 132b60f14..000000000 --- a/playground/react-emotion/Counter.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import styled from '@emotion/styled' -import { css } from '@emotion/react' -import { useState } from 'react' - -// Ensure HMR of styled component alongside other components -export const StyledCode = styled.code` - color: #646cff; -` - -export function Counter() { - const [count, setCount] = useState(0) - - return ( - - ) -} diff --git a/playground/react-emotion/__tests__/react.spec.ts b/playground/react-emotion/__tests__/react.spec.ts deleted file mode 100644 index dc98fb37b..000000000 --- a/playground/react-emotion/__tests__/react.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { expect, test } from 'vitest' -import { editFile, getColor, isServe, page, untilUpdated } from '~utils' - -test('should render', async () => { - expect(await page.textContent('h1')).toMatch( - 'Hello Vite + React + @emotion/react', - ) -}) - -test('should update', async () => { - expect(await page.textContent('button')).toMatch('count is: 0') - await page.click('button') - expect(await page.textContent('button')).toMatch('count is: 1') -}) - -test.runIf(isServe)('should hmr', async () => { - editFile('App.jsx', (code) => - code.replace('Vite + React + @emotion/react', 'Updated'), - ) - await untilUpdated(() => page.textContent('h1'), 'Hello Updated') - - editFile('Counter.jsx', (code) => - code.replace('color: #646cff;', 'color: #d26ac2;'), - ) - - await untilUpdated(() => getColor('code'), '#d26ac2') - - // preserve state - expect(await page.textContent('button')).toMatch('count is: 1') -}) - -test('should update button style', async () => { - function getButtonBorderStyle() { - return page.evaluate(() => { - return window.getComputedStyle(document.querySelector('button')).border - }) - } - - await page.evaluate(() => { - return document.querySelector('button').style - }) - - expect(await getButtonBorderStyle()).toMatch('2px solid rgb(0, 0, 0)') - - if (isServe) { - editFile('Counter.jsx', (code) => - code.replace('border: 2px solid #000', 'border: 4px solid red'), - ) - - await untilUpdated(getButtonBorderStyle, '4px solid rgb(255, 0, 0)') - - // preserve state - expect(await page.textContent('button')).toMatch('count is: 1') - } -}) diff --git a/playground/react-emotion/index.html b/playground/react-emotion/index.html deleted file mode 100644 index 7417c442d..000000000 --- a/playground/react-emotion/index.html +++ /dev/null @@ -1,10 +0,0 @@ -
- diff --git a/playground/react-emotion/vite.config.ts b/playground/react-emotion/vite.config.ts deleted file mode 100644 index 3c0aa96b7..000000000 --- a/playground/react-emotion/vite.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import react from '@vitejs/plugin-react' -import { defineConfig } from 'vite' - -export default defineConfig({ - server: { port: 8904 /* Should be unique */ }, - plugins: [ - react({ - jsxImportSource: '@emotion/react', - babel: { - plugins: ['@emotion/babel-plugin'], - }, - }), - ], - clearScreen: false, - build: { - // to make tests faster - minify: false, - }, -}) diff --git a/playground/react-env/App.jsx b/playground/react-env/App.jsx deleted file mode 100644 index 216b2989b..000000000 --- a/playground/react-env/App.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import { useState } from 'react' - -function App() { - const [count, setCount] = useState(0) - return ( -
-
-

Hello Vite + React

-
-
- ) -} - -export default App diff --git a/playground/react-env/__tests__/react.spec.ts b/playground/react-env/__tests__/react.spec.ts deleted file mode 100644 index 12686328d..000000000 --- a/playground/react-env/__tests__/react.spec.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { expect, test } from 'vitest' -import { page } from '~utils' - -test('should work', async () => { - expect(await page.textContent('h1')).toMatch('Hello Vite + React') -}) diff --git a/playground/react-env/package.json b/playground/react-env/package.json deleted file mode 100644 index 9eeddfd0e..000000000 --- a/playground/react-env/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@vitejs/test-react-env", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "debug": "node --inspect-brk vite", - "preview": "vite preview" - }, - "dependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1" - }, - "devDependencies": { - "@vitejs/plugin-react": "workspace:*" - } -} diff --git a/playground/react-env/vite.config.ts b/playground/react-env/vite.config.ts deleted file mode 100644 index 4f7c0081f..000000000 --- a/playground/react-env/vite.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import react from '@vitejs/plugin-react' -import type { UserConfig } from 'vite' - -// Overriding the NODE_ENV set by vitest -process.env.NODE_ENV = '' - -const config: UserConfig = { - server: { port: 8905 /* Should be unique */ }, - plugins: [react()], - mode: 'staging', - build: { - // to make tests faster - minify: false, - }, -} - -export default config diff --git a/playground/react-native/App.jsx b/playground/react-native/App.jsx new file mode 100644 index 000000000..9479a09ad --- /dev/null +++ b/playground/react-native/App.jsx @@ -0,0 +1,70 @@ +import { useState } from 'react' +import Button from 'jsx-entry' +import { Text, TouchableOpacity, View } from 'react-native' +import Dummy from './components/Dummy?qs-should-not-break-plugin-react' +import Parent from './hmr/parent' +import { JsxImportRuntime } from './hmr/jsx-import-runtime' +import { CountProvider } from './context/CountProvider' +import { ContextButton } from './context/ContextButton' +import { TestImportAttributes } from './import-attributes/test' +import { PressableWithOverlay } from './components/PressableWithOverlay' + +function App() { + const [count, setCount] = useState(0) + return ( + + + + Hello Vite + React + + + setCount((count) => count + 1)} + > + count is: {count} + + + + +

+ Edit App.jsx and save to test HMR updates. +

+ + Learn React + +
+ + + + + + +
+ ) +} + +function AppWithProviders() { + return ( + + + + ) +} + +export default AppWithProviders diff --git a/playground/react-native/__tests__/react.spec.ts b/playground/react-native/__tests__/react.spec.ts new file mode 100644 index 000000000..3c17d963b --- /dev/null +++ b/playground/react-native/__tests__/react.spec.ts @@ -0,0 +1,145 @@ +import { expect, test } from 'vitest' +import { + editFile, + isBuild, + isServe, + page, + untilBrowserLogAfter, + untilUpdated, +} from '~utils' + +test('should render', async () => { + expect(await page.textContent('h1')).toMatch('Hello Vite + React') +}) + +test('should update', async () => { + expect(await page.textContent('#state-button')).toMatch('count is: 0') + await page.click('#state-button') + expect(await page.textContent('#state-button')).toMatch('count is: 1') +}) + +test.runIf(isServe)('should hmr', async () => { + editFile('App.jsx', (code) => + code.replace('Vite + React', 'Vite + React Updated'), + ) + await untilUpdated(() => page.textContent('h1'), 'Hello Vite + React Updated') + // preserve state + expect(await page.textContent('#state-button')).toMatch('count is: 1') + + editFile('App.jsx', (code) => + code.replace('Vite + React Updated', 'Vite + React'), + ) + await untilUpdated(() => page.textContent('h1'), 'Hello Vite + React') +}) + +test.runIf(isServe)('should not invalidate when code is invalid', async () => { + editFile('App.jsx', (code) => + code.replace('
', '
'), + ) + + await untilUpdated( + () => page.textContent('vite-error-overlay .message-body'), + 'Unexpected token', + ) + // if import.meta.invalidate happened, the old page won't be shown because the page is reloaded + expect(await page.textContent('h1')).toMatch('Hello Vite + React') + + editFile('App.jsx', (code) => + code.replace('
', '
'), + ) +}) + +test.runIf(isServe)( + 'should have annotated jsx with file location metadata', + async () => { + const meta = await page.evaluate(() => { + const button = document.querySelector('#state-button') + const key = Object.keys(button).find( + (key) => key.indexOf('__reactFiber') === 0, + ) + return button[key]._debugSource + }) + // If the evaluate call doesn't crash, and the returned metadata has + // the expected fields, we're good. + expect(Object.keys(meta).sort()).toEqual([ + 'columnNumber', + 'fileName', + 'lineNumber', + ]) + }, +) + +test('import attributes', async () => { + expect(await page.textContent('.import-attributes')).toBe('ok') +}) + +if (!isBuild) { + // #9869 + test('should only hmr files with exported react components', async () => { + await untilBrowserLogAfter( + () => + editFile('hmr/no-exported-comp.jsx', (code) => + code.replace('An Object', 'Updated'), + ), + [ + '[vite] invalidate /hmr/no-exported-comp.jsx: Could not Fast Refresh ("Foo" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports', + '[vite] hot updated: /hmr/no-exported-comp.jsx', + '[vite] hot updated: /hmr/parent.jsx', + 'Parent rendered', + ], + ) + await untilUpdated(() => page.textContent('#parent'), 'Updated') + }) + + // #3301 + test('should hmr react context', async () => { + expect(await page.textContent('#context-button')).toMatch( + 'context-based count is: 0', + ) + await page.click('#context-button') + expect(await page.textContent('#context-button')).toMatch( + 'context-based count is: 1', + ) + + await untilBrowserLogAfter( + () => + editFile('context/CountProvider.jsx', (code) => + code.replace('context provider', 'context provider updated'), + ), + [ + '[vite] invalidate /context/CountProvider.jsx: Could not Fast Refresh ("CountContext" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports', + '[vite] hot updated: /context/CountProvider.jsx', + '[vite] hot updated: /App.jsx', + '[vite] hot updated: /context/ContextButton.jsx', + 'Parent rendered', + ], + ) + await untilUpdated( + () => page.textContent('#context-provider'), + 'context provider updated', + ) + }) + + test('should hmr files with "react/jsx-runtime"', async () => { + expect(await page.textContent('#state-button')).toMatch('count is: 0') + await page.click('#state-button') + expect(await page.textContent('#state-button')).toMatch('count is: 1') + + await untilBrowserLogAfter( + () => + editFile('hmr/jsx-import-runtime.js', (code) => + code.replace( + 'JSX import runtime works', + 'JSX import runtime updated', + ), + ), + ['[vite] hot updated: /hmr/jsx-import-runtime.js'], + ) + await untilUpdated( + () => page.textContent('#jsx-import-runtime'), + 'JSX import runtime updated', + ) + + expect(await page.textContent('#state-button')).toMatch('count is: 1') + }) +} diff --git a/playground/react-native/components/Dummy.jsx b/playground/react-native/components/Dummy.jsx new file mode 100644 index 000000000..27ec3c21d --- /dev/null +++ b/playground/react-native/components/Dummy.jsx @@ -0,0 +1,3 @@ +export default function Dummy() { + return <> +} diff --git a/playground/react-native/components/PressableWithOverlay.tsx b/playground/react-native/components/PressableWithOverlay.tsx new file mode 100644 index 000000000..4e1ef1b3d --- /dev/null +++ b/playground/react-native/components/PressableWithOverlay.tsx @@ -0,0 +1,82 @@ +import type { + GestureResponderEvent, + PressableProps, + StyleProp, + ViewStyle, +} from 'react-native' +import { Platform, Pressable as RNPressable, StyleSheet } from 'react-native' +import Animated, { + useSharedValue, + withDelay, + withSpring, +} from 'react-native-reanimated' + +export interface ButtonProps + extends Omit { + children?: React.ReactNode | React.ReactNode[] + style: StyleProp +} + +const Pressable = Animated.createAnimatedComponent(RNPressable) + +export const PressableWithOverlay = ({ + children, + onPress, + onPressIn, + onPressOut, + style, + ...props +}: ButtonProps) => { + const opacity = useSharedValue(0) + + const pressIn = (e: GestureResponderEvent) => { + onPressIn?.(e) + + opacity.value = withSpring(0.25) + } + + const pressOut = (e: GestureResponderEvent) => { + onPressOut?.(e) + + if (opacity.value !== 0.25) { + // for some reason with delay doesn't work on web + if (Platform.OS === 'web') { + setTimeout(() => { + opacity.value = withSpring(0) + }, 250) + } else { + opacity.value = withDelay(250, withSpring(0)) + } + } else { + opacity.value = withSpring(0) + } + } + + return ( + + {children} + + + + ) +} diff --git a/playground/react-native/context/ContextButton.jsx b/playground/react-native/context/ContextButton.jsx new file mode 100644 index 000000000..92c6d0bd2 --- /dev/null +++ b/playground/react-native/context/ContextButton.jsx @@ -0,0 +1,11 @@ +import { useContext } from 'react' +import { CountContext } from './CountProvider' + +export function ContextButton() { + const { count, setCount } = useContext(CountContext) + return ( + + ) +} diff --git a/playground/react-native/context/CountProvider.jsx b/playground/react-native/context/CountProvider.jsx new file mode 100644 index 000000000..223ad25f0 --- /dev/null +++ b/playground/react-native/context/CountProvider.jsx @@ -0,0 +1,12 @@ +import { createContext, useState } from 'react' +export const CountContext = createContext() + +export const CountProvider = ({ children }) => { + const [count, setCount] = useState(0) + return ( + + {children} +
context provider
+
+ ) +} diff --git a/playground/react-native/hmr/jsx-import-runtime.js b/playground/react-native/hmr/jsx-import-runtime.js new file mode 100644 index 000000000..27f1529dd --- /dev/null +++ b/playground/react-native/hmr/jsx-import-runtime.js @@ -0,0 +1,8 @@ +import * as JsxRuntime from 'react/jsx-runtime' + +export function JsxImportRuntime() { + return JsxRuntime.jsx('p', { + id: 'jsx-import-runtime', + children: 'JSX import runtime works', + }) +} diff --git a/playground/react-native/hmr/no-exported-comp.jsx b/playground/react-native/hmr/no-exported-comp.jsx new file mode 100644 index 000000000..6954fc9c7 --- /dev/null +++ b/playground/react-native/hmr/no-exported-comp.jsx @@ -0,0 +1,7 @@ +// This un-exported react component should not cause this file to be treated +// as an HMR boundary +const Unused = () => An unused react component + +export const Foo = { + is: 'An Object', +} diff --git a/playground/react-native/hmr/parent.jsx b/playground/react-native/hmr/parent.jsx new file mode 100644 index 000000000..ff8698281 --- /dev/null +++ b/playground/react-native/hmr/parent.jsx @@ -0,0 +1,7 @@ +import { Foo } from './no-exported-comp' + +export default function Parent() { + console.log('Parent rendered') + + return
{Foo.is}
+} diff --git a/playground/react-native/import-attributes/data.json b/playground/react-native/import-attributes/data.json new file mode 100644 index 000000000..5e7d61912 --- /dev/null +++ b/playground/react-native/import-attributes/data.json @@ -0,0 +1,3 @@ +{ + "message": "ok" +} diff --git a/playground/react-native/import-attributes/test.jsx b/playground/react-native/import-attributes/test.jsx new file mode 100644 index 000000000..f6ff81575 --- /dev/null +++ b/playground/react-native/import-attributes/test.jsx @@ -0,0 +1,9 @@ +import data from './data.json' with { type: 'json' } + +export function TestImportAttributes() { + return ( +
+ import-attirbutes: {data.message} +
+ ) +} diff --git a/playground/react-env/index.html b/playground/react-native/index.html similarity index 92% rename from playground/react-env/index.html rename to playground/react-native/index.html index 7417c442d..711c035f1 100644 --- a/playground/react-env/index.html +++ b/playground/react-native/index.html @@ -1,5 +1,6 @@