From 4c3effc8b2e8ae32b3d09b07fa58fc0022adbaca Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Mon, 30 Mar 2026 05:57:33 +0200 Subject: [PATCH 1/9] feat(vite): adopt rolldown refresh pipeline --- packages/vite/README.md | 10 +- packages/vite/index.d.ts | 4 +- packages/vite/package.json | 10 +- packages/vite/src/index.js | 126 ++++++-- pnpm-lock.yaml | 302 +------------------ test/fixture/vite-preact-compat/package.json | 9 +- test/fixture/vite-signals/package.json | 5 +- test/fixture/vite/package.json | 9 +- 8 files changed, 140 insertions(+), 335 deletions(-) diff --git a/packages/vite/README.md b/packages/vite/README.md index f8852527..ed76d9b7 100644 --- a/packages/vite/README.md +++ b/packages/vite/README.md @@ -20,11 +20,17 @@ export default { }; ``` +`@prefresh/vite` configures Vite's OXC JSX transform for Preact and composes +`@prefresh/rolldown` internally. Babel is only used as a selective fallback when +you pass `parserPlugins`. + ## Options The plugin accepts two options `include` & `exclude` which are used in the [`@rollup/pluginutils.createFilter`](https://github.com/rollup/plugins/tree/master/packages/pluginutils#createfilter) to filter out files or include them. -The plugin also accepts the addition of [`parserPlugins`](https://babeljs.io/docs/en/babel-parser#plugins) +The plugin also accepts the addition of [`parserPlugins`](https://babeljs.io/docs/en/babel-parser#plugins). +Providing `parserPlugins` opts that file transform back into the Babel-based path, +similar to how `@preact/preset-vite` only enables Babel when that path is requested. ## Best practices @@ -32,7 +38,7 @@ The plugin also accepts the addition of [`parserPlugins`](https://babeljs.io/doc We need to be able to recognise your components, this means that components should start with a capital letter and hook should start with `use` followed by a capital letter. -This allows the Babel plugin to effectively recognise these. +This allows the refresh transform to effectively recognise these. Do note that a component as seen below is not named. diff --git a/packages/vite/index.d.ts b/packages/vite/index.d.ts index 671d8fa5..79140bb0 100644 --- a/packages/vite/index.d.ts +++ b/packages/vite/index.d.ts @@ -1,5 +1,5 @@ import { FilterPattern } from '@rollup/pluginutils'; -import { Plugin } from 'vite'; +import { PluginOption } from 'vite'; interface Options { parserPlugins?: readonly string[]; @@ -7,6 +7,6 @@ interface Options { exclude?: FilterPattern; } -declare const prefreshPlugin: (options?: Options) => Plugin; +declare const prefreshPlugin: (options?: Options) => Promise; export = prefreshPlugin; diff --git a/packages/vite/package.json b/packages/vite/package.json index df61eb73..7689c79f 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -37,18 +37,20 @@ "homepage": "https://github.com/preactjs/prefresh#readme", "dependencies": { "@babel/core": "^7.22.1", - "@prefresh/babel-plugin": "^0.5.2", "@prefresh/core": "^1.5.0", + "@prefresh/babel-plugin": "^0.5.2", + "@prefresh/rolldown": "workspace:*", "@prefresh/utils": "^1.2.0", - "@rollup/pluginutils": "^4.2.1" + "@rollup/pluginutils": "^4.2.1", + "rolldown": "^1.0.0-rc.12" }, "devDependencies": { "preact": "^10.26.10", - "vite": "^5.4.21" + "vite": "^8.0.0" }, "peerDependencies": { "preact": "^10.4.0 || ^11.0.0-0", - "vite": ">=2.0.0" + "vite": "^8.0.0" }, "publishConfig": { "provenance": true diff --git a/packages/vite/src/index.js b/packages/vite/src/index.js index 69aba74a..e9c57ca9 100644 --- a/packages/vite/src/index.js +++ b/packages/vite/src/index.js @@ -2,48 +2,130 @@ const { transformSync } = require('@babel/core'); const { createFilter } = require('@rollup/pluginutils'); const prefreshBabelPlugin = require('@prefresh/babel-plugin'); +const SCRIPT_LANG_RE = /\.(c|m)?(t|j)sx?$/; + +/** @returns {Promise} */ +module.exports = async function prefreshPlugin(options = {}) { + const { default: prefreshRolldown } = await import('@prefresh/rolldown'); + const useBabel = Object.prototype.hasOwnProperty.call( + options, + 'parserPlugins' + ); + + return [ + preactOptionsPlugin(useBabel), + ...(useBabel ? [prefreshBabelTransformPlugin(options)] : []), + prefreshRolldown(), + prefreshWrapperPlugin(options), + ]; +}; + +/** @returns {import('vite').Plugin} */ +function preactOptionsPlugin(useBabel) { + return { + name: 'prefresh-preact-options', + config(config, { command }) { + const oxc = config.oxc || {}; + const jsx = oxc.jsx || {}; + + return { + oxc: { + ...oxc, + jsx: { + ...jsx, + importSource: 'preact', + refresh: !useBabel && command === 'serve', + }, + jsxRefreshInclude: oxc.jsxRefreshInclude || /\.[jt]sx$/, + }, + }; + }, + }; +} + /** @returns {import('vite').Plugin} */ -module.exports = function prefreshPlugin(options = {}) { +function prefreshBabelTransformPlugin(options = {}) { let shouldSkip = false; const filter = createFilter(options.include, options.exclude); return { - name: 'prefresh', + name: 'prefresh-babel-transform', + apply: 'serve', configResolved(config) { - shouldSkip = - config.isProduction || - config.command === 'build' || - config.server.hmr === false; + shouldSkip = config.server.hmr === false; }, - async transform(code, id, options) { + transform(code, id, transformOptions) { const ssr = - typeof options === 'boolean' - ? options - : options && options.ssr === true; + typeof transformOptions === 'boolean' + ? transformOptions + : transformOptions && transformOptions.ssr === true; if ( shouldSkip || - !/\.(c|m)?(t|j)sx?$/.test(id) || + !SCRIPT_LANG_RE.test(id) || id.includes('node_modules') || id.includes('?worker') || !filter(id) || ssr - ) + ) { return; + } const parserPlugins = [ 'jsx', 'classProperties', 'classPrivateProperties', 'classPrivateMethods', - /\.tsx?$/.test(id) && 'typescript', + /\.(c|m)?tsx?$/.test(id) && 'typescript', ...((options && options.parserPlugins) || []), ].filter(Boolean); - const result = transform(code, id, parserPlugins); - const hasReg = /\$RefreshReg\$\(/.test(result.code); - const hasSig = /\$RefreshSig\$\(/.test(result.code); + return transform(code, id, parserPlugins); + }, + }; +} + +/** @returns {import('vite').Plugin} */ +function prefreshWrapperPlugin(options = {}) { + let shouldSkip = false; + const filter = createFilter(options.include, options.exclude); + + return { + name: 'prefresh-wrapper', + apply: 'serve', + config(config) { + const include = config.optimizeDeps?.include || []; - if (!hasSig && !hasReg) return code; + return { + optimizeDeps: { + include: [ + ...new Set([...include, '@prefresh/core', '@prefresh/utils']), + ], + }, + }; + }, + configResolved(config) { + shouldSkip = config.server.hmr === false; + }, + async transform(code, id, options) { + const ssr = + typeof options === 'boolean' + ? options + : options && options.ssr === true; + if ( + shouldSkip || + !SCRIPT_LANG_RE.test(id) || + id.includes('node_modules') || + id.includes('?worker') || + !filter(id) || + ssr + ) { + return; + } + + const hasReg = /\$RefreshReg\$\(/.test(code); + const hasSig = /\$RefreshSig\$\(/.test(code); + + if (!hasSig && !hasReg) return; const prefreshCore = await this.resolve('@prefresh/core', __filename); const prefreshUtils = await this.resolve('@prefresh/utils', __filename); @@ -79,13 +161,13 @@ module.exports = function prefreshPlugin(options = {}) { if (hasSig && !hasReg) { return { - code: `${prelude}${result.code}`, - map: result.map, + code: `${prelude}${code}`, + map: null, }; } return { - code: `${prelude}${result.code} + code: `${prelude}${code} if (import.meta.hot) { self.$RefreshReg$ = prevRefreshReg; @@ -100,11 +182,11 @@ module.exports = function prefreshPlugin(options = {}) { }); } `, - map: result.map, + map: null, }; }, }; -}; +} const transform = (code, path, plugins) => transformSync(code, { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f76ba92..00dc1a2c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -112,19 +112,25 @@ importers: '@prefresh/core': specifier: ^1.5.0 version: 1.5.4(preact@10.26.10) + '@prefresh/rolldown': + specifier: workspace:* + version: link:../rolldown '@prefresh/utils': specifier: ^1.2.0 version: 1.2.1 '@rollup/pluginutils': specifier: ^4.2.1 version: 4.2.1 + rolldown: + specifier: ^1.0.0-rc.12 + version: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) devDependencies: preact: specifier: ^10.26.10 version: 10.26.10 vite: - specifier: ^5.4.21 - version: 5.4.21(@types/node@24.3.0)(lightningcss@1.32.0) + specifier: ^8.0.0 + version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.3.0) packages/web-dev-server: dependencies: @@ -420,144 +426,6 @@ packages: '@emnapi/wasi-threads@1.2.0': resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - '@eslint-community/eslint-utils@4.7.0': resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1554,6 +1422,7 @@ packages: basic-ftp@5.0.5: resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} engines: {node: '>=10.0.0'} + deprecated: Security vulnerability fixed in 5.2.0, please upgrade batch@0.6.1: resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} @@ -2210,11 +2079,6 @@ packages: es6-promise@4.2.8: resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2642,7 +2506,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} @@ -4003,10 +3867,6 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.8: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} @@ -4931,37 +4791,6 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - vite@5.4.21: - resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - vite@8.0.3: resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5585,75 +5414,6 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.21.5': - optional: true - - '@esbuild/android-arm64@0.21.5': - optional: true - - '@esbuild/android-arm@0.21.5': - optional: true - - '@esbuild/android-x64@0.21.5': - optional: true - - '@esbuild/darwin-arm64@0.21.5': - optional: true - - '@esbuild/darwin-x64@0.21.5': - optional: true - - '@esbuild/freebsd-arm64@0.21.5': - optional: true - - '@esbuild/freebsd-x64@0.21.5': - optional: true - - '@esbuild/linux-arm64@0.21.5': - optional: true - - '@esbuild/linux-arm@0.21.5': - optional: true - - '@esbuild/linux-ia32@0.21.5': - optional: true - - '@esbuild/linux-loong64@0.21.5': - optional: true - - '@esbuild/linux-mips64el@0.21.5': - optional: true - - '@esbuild/linux-ppc64@0.21.5': - optional: true - - '@esbuild/linux-riscv64@0.21.5': - optional: true - - '@esbuild/linux-s390x@0.21.5': - optional: true - - '@esbuild/linux-x64@0.21.5': - optional: true - - '@esbuild/netbsd-x64@0.21.5': - optional: true - - '@esbuild/openbsd-x64@0.21.5': - optional: true - - '@esbuild/sunos-x64@0.21.5': - optional: true - - '@esbuild/win32-arm64@0.21.5': - optional: true - - '@esbuild/win32-ia32@0.21.5': - optional: true - - '@esbuild/win32-x64@0.21.5': - optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@8.42.0)': dependencies: eslint: 8.42.0 @@ -7692,32 +7452,6 @@ snapshots: es6-promise@4.2.8: {} - esbuild@0.21.5: - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - escalade@3.2.0: {} escape-html@1.0.3: {} @@ -9898,12 +9632,6 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss@8.5.6: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.8: dependencies: nanoid: 3.3.11 @@ -11017,16 +10745,6 @@ snapshots: vary@1.1.2: {} - vite@5.4.21(@types/node@24.3.0)(lightningcss@1.32.0): - dependencies: - esbuild: 0.21.5 - postcss: 8.5.6 - rollup: 4.52.5 - optionalDependencies: - '@types/node': 24.3.0 - fsevents: 2.3.3 - lightningcss: 1.32.0 - vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.3.0): dependencies: lightningcss: 1.32.0 diff --git a/test/fixture/vite-preact-compat/package.json b/test/fixture/vite-preact-compat/package.json index 0457e1a9..4722d21d 100644 --- a/test/fixture/vite-preact-compat/package.json +++ b/test/fixture/vite-preact-compat/package.json @@ -7,13 +7,12 @@ "preact": "^10.19.2" }, "devDependencies": { - "@babel/core": "^7.22.1", - "@prefresh/babel-plugin": "file:../../packages/babel", "@prefresh/vite": "file:../../packages/vite", - "vite": "^5.0.0" + "vite": "^8.0.0" }, "resolutions": { - "@prefresh/core": "file:../../packages/core", - "@prefresh/utils": "file:../../packages/utils" + "@prefresh/rolldown": "file:../../packages/rolldown", + "@prefresh/core": "file:../../packages/core", + "@prefresh/utils": "file:../../packages/utils" } } diff --git a/test/fixture/vite-signals/package.json b/test/fixture/vite-signals/package.json index 051ff64c..bebd31ef 100644 --- a/test/fixture/vite-signals/package.json +++ b/test/fixture/vite-signals/package.json @@ -7,12 +7,11 @@ "preact": "^10.19.2" }, "devDependencies": { - "@babel/core": "^7.22.1", - "@prefresh/babel-plugin": "file:../../packages/babel", "@prefresh/vite": "file:../../packages/vite", - "vite": "^5.0.0" + "vite": "^8.0.0" }, "resolutions": { + "@prefresh/rolldown": "file:../../packages/rolldown", "@prefresh/core": "file:../../packages/core", "@prefresh/utils": "file:../../packages/utils" } diff --git a/test/fixture/vite/package.json b/test/fixture/vite/package.json index 0457e1a9..4722d21d 100644 --- a/test/fixture/vite/package.json +++ b/test/fixture/vite/package.json @@ -7,13 +7,12 @@ "preact": "^10.19.2" }, "devDependencies": { - "@babel/core": "^7.22.1", - "@prefresh/babel-plugin": "file:../../packages/babel", "@prefresh/vite": "file:../../packages/vite", - "vite": "^5.0.0" + "vite": "^8.0.0" }, "resolutions": { - "@prefresh/core": "file:../../packages/core", - "@prefresh/utils": "file:../../packages/utils" + "@prefresh/rolldown": "file:../../packages/rolldown", + "@prefresh/core": "file:../../packages/core", + "@prefresh/utils": "file:../../packages/utils" } } From 117dde9ca19d69bb53f4c410c3fd55cb4b895c97 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Mon, 30 Mar 2026 06:01:18 +0200 Subject: [PATCH 2/9] Fixes --- .changeset/slow-lamps-help.md | 5 +++++ packages/vite/src/index.js | 11 ++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 .changeset/slow-lamps-help.md diff --git a/.changeset/slow-lamps-help.md b/.changeset/slow-lamps-help.md new file mode 100644 index 00000000..a22ab66d --- /dev/null +++ b/.changeset/slow-lamps-help.md @@ -0,0 +1,5 @@ +--- +'@prefresh/vite': minor +--- + +Add true vite 8 support with native rolldown rather than babel diff --git a/packages/vite/src/index.js b/packages/vite/src/index.js index e9c57ca9..aed41b7e 100644 --- a/packages/vite/src/index.js +++ b/packages/vite/src/index.js @@ -28,17 +28,22 @@ function preactOptionsPlugin(useBabel) { const oxc = config.oxc || {}; const jsx = oxc.jsx || {}; - return { + const supportsRolldown = + this && "meta" in this && this.meta && typeof this.meta === "object" + ? "rolldownVersion" in this.meta + : false; + + return supportsRolldown ? { oxc: { ...oxc, jsx: { ...jsx, - importSource: 'preact', + importSource: oxc.jsx.importSource || 'preact', refresh: !useBabel && command === 'serve', }, jsxRefreshInclude: oxc.jsxRefreshInclude || /\.[jt]sx$/, }, - }; + } : {}; }, }; } From 894d0e99f1689e3a5d362e576e06e26034a0b85f Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Mon, 30 Mar 2026 06:05:35 +0200 Subject: [PATCH 3/9] Tweaks --- packages/vite/src/index.js | 42 ++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/packages/vite/src/index.js b/packages/vite/src/index.js index aed41b7e..0dfd293e 100644 --- a/packages/vite/src/index.js +++ b/packages/vite/src/index.js @@ -14,7 +14,7 @@ module.exports = async function prefreshPlugin(options = {}) { return [ preactOptionsPlugin(useBabel), - ...(useBabel ? [prefreshBabelTransformPlugin(options)] : []), + prefreshBabelTransformPlugin(options, useBabel), prefreshRolldown(), prefreshWrapperPlugin(options), ]; @@ -29,27 +29,29 @@ function preactOptionsPlugin(useBabel) { const jsx = oxc.jsx || {}; const supportsRolldown = - this && "meta" in this && this.meta && typeof this.meta === "object" - ? "rolldownVersion" in this.meta - : false; - - return supportsRolldown ? { - oxc: { - ...oxc, - jsx: { - ...jsx, - importSource: oxc.jsx.importSource || 'preact', - refresh: !useBabel && command === 'serve', - }, - jsxRefreshInclude: oxc.jsxRefreshInclude || /\.[jt]sx$/, - }, - } : {}; + this && 'meta' in this && this.meta && typeof this.meta === 'object' + ? 'rolldownVersion' in this.meta + : false; + + return supportsRolldown + ? { + oxc: { + ...oxc, + jsx: { + ...jsx, + importSource: oxc.jsx.importSource || 'preact', + refresh: !useBabel && command === 'serve', + }, + jsxRefreshInclude: oxc.jsxRefreshInclude || /\.[jt]sx$/, + }, + } + : {}; }, }; } /** @returns {import('vite').Plugin} */ -function prefreshBabelTransformPlugin(options = {}) { +function prefreshBabelTransformPlugin(options = {}, useBabel) { let shouldSkip = false; const filter = createFilter(options.include, options.exclude); @@ -57,7 +59,11 @@ function prefreshBabelTransformPlugin(options = {}) { name: 'prefresh-babel-transform', apply: 'serve', configResolved(config) { - shouldSkip = config.server.hmr === false; + const supportsRolldown = + this && 'meta' in this && this.meta && typeof this.meta === 'object' + ? 'rolldownVersion' in this.meta + : false; + shouldSkip = config.server.hmr === false || (!useBabel && supportsRolldown); }, transform(code, id, transformOptions) { const ssr = From 26e122a65de89dd6bf97c6d0bec1b8e872a9667c Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Tue, 31 Mar 2026 20:07:54 +0200 Subject: [PATCH 4/9] fix(vite): keep Babel fallback opt-in Only disable the Babel path when resolved Vite config exposes OXC support, and keep the broader Vite peer range while the package still supports older setups. --- packages/vite/package.json | 2 +- packages/vite/src/index.js | 49 +++++++++++++++++++++++++------------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/packages/vite/package.json b/packages/vite/package.json index 7689c79f..0997df6f 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -50,7 +50,7 @@ }, "peerDependencies": { "preact": "^10.4.0 || ^11.0.0-0", - "vite": "^8.0.0" + "vite": ">=2.0.0" }, "publishConfig": { "provenance": true diff --git a/packages/vite/src/index.js b/packages/vite/src/index.js index 0dfd293e..0167a04f 100644 --- a/packages/vite/src/index.js +++ b/packages/vite/src/index.js @@ -3,35 +3,53 @@ const { createFilter } = require('@rollup/pluginutils'); const prefreshBabelPlugin = require('@prefresh/babel-plugin'); const SCRIPT_LANG_RE = /\.(c|m)?(t|j)sx?$/; +let prefreshRolldownPromise; + +function loadPrefreshRolldown() { + prefreshRolldownPromise ||= import('@prefresh/rolldown').then( + ({ default: prefreshRolldown }) => prefreshRolldown + ); + return prefreshRolldownPromise; +} + +function hasRolldownSupport(pluginContext) { + return !!( + pluginContext && + typeof pluginContext === 'object' && + pluginContext.meta && + typeof pluginContext.meta === 'object' && + 'rolldownVersion' in pluginContext.meta + ); +} + +function hasOxcSupport(config) { + return !!(config.oxc && typeof config.oxc === 'object'); +} /** @returns {Promise} */ module.exports = async function prefreshPlugin(options = {}) { - const { default: prefreshRolldown } = await import('@prefresh/rolldown'); - const useBabel = Object.prototype.hasOwnProperty.call( + const prefreshRolldown = await loadPrefreshRolldown(); + const forceBabel = Object.prototype.hasOwnProperty.call( options, 'parserPlugins' ); return [ - preactOptionsPlugin(useBabel), - prefreshBabelTransformPlugin(options, useBabel), + preactOptionsPlugin(forceBabel), + prefreshBabelTransformPlugin(options, forceBabel), prefreshRolldown(), prefreshWrapperPlugin(options), ]; }; /** @returns {import('vite').Plugin} */ -function preactOptionsPlugin(useBabel) { +function preactOptionsPlugin(forceBabel) { return { name: 'prefresh-preact-options', config(config, { command }) { const oxc = config.oxc || {}; const jsx = oxc.jsx || {}; - - const supportsRolldown = - this && 'meta' in this && this.meta && typeof this.meta === 'object' - ? 'rolldownVersion' in this.meta - : false; + const supportsRolldown = hasRolldownSupport(this); return supportsRolldown ? { @@ -40,7 +58,7 @@ function preactOptionsPlugin(useBabel) { jsx: { ...jsx, importSource: oxc.jsx.importSource || 'preact', - refresh: !useBabel && command === 'serve', + refresh: !forceBabel && command === 'serve', }, jsxRefreshInclude: oxc.jsxRefreshInclude || /\.[jt]sx$/, }, @@ -51,7 +69,7 @@ function preactOptionsPlugin(useBabel) { } /** @returns {import('vite').Plugin} */ -function prefreshBabelTransformPlugin(options = {}, useBabel) { +function prefreshBabelTransformPlugin(options = {}, forceBabel) { let shouldSkip = false; const filter = createFilter(options.include, options.exclude); @@ -59,11 +77,8 @@ function prefreshBabelTransformPlugin(options = {}, useBabel) { name: 'prefresh-babel-transform', apply: 'serve', configResolved(config) { - const supportsRolldown = - this && 'meta' in this && this.meta && typeof this.meta === 'object' - ? 'rolldownVersion' in this.meta - : false; - shouldSkip = config.server.hmr === false || (!useBabel && supportsRolldown); + shouldSkip = + config.server.hmr === false || (!forceBabel && hasOxcSupport(config)); }, transform(code, id, transformOptions) { const ssr = From b5bde96cb8a40227726dd3608bca399b1163cfde Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Thu, 2 Apr 2026 19:07:37 +0200 Subject: [PATCH 5/9] fix(vite): avoid unnecessary Babel work in dev --- packages/vite/README.md | 2 +- packages/vite/src/index.js | 72 +++++++++++++++++++++++++++----------- 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/packages/vite/README.md b/packages/vite/README.md index ed76d9b7..22358e0b 100644 --- a/packages/vite/README.md +++ b/packages/vite/README.md @@ -20,7 +20,7 @@ export default { }; ``` -`@prefresh/vite` configures Vite's OXC JSX transform for Preact and composes +`@prefresh/vite` configures Vite's Oxc JSX transform for Preact and composes `@prefresh/rolldown` internally. Babel is only used as a selective fallback when you pass `parserPlugins`. diff --git a/packages/vite/src/index.js b/packages/vite/src/index.js index 0167a04f..655d06d2 100644 --- a/packages/vite/src/index.js +++ b/packages/vite/src/index.js @@ -1,9 +1,17 @@ -const { transformSync } = require('@babel/core'); const { createFilter } = require('@rollup/pluginutils'); -const prefreshBabelPlugin = require('@prefresh/babel-plugin'); const SCRIPT_LANG_RE = /\.(c|m)?(t|j)sx?$/; +let babel; let prefreshRolldownPromise; +let viteSupportsHookFilters; + +function loadBabel() { + babel ||= { + transformSync: require('@babel/core').transformSync, + prefreshBabelPlugin: require('@prefresh/babel-plugin'), + }; + return babel; +} function loadPrefreshRolldown() { prefreshRolldownPromise ||= import('@prefresh/rolldown').then( @@ -12,6 +20,33 @@ function loadPrefreshRolldown() { return prefreshRolldownPromise; } +function supportsHookFilters() { + if (viteSupportsHookFilters !== undefined) return viteSupportsHookFilters; + + try { + const [major, minor] = require('vite/package.json') + .version.split('.') + .map(Number); + + viteSupportsHookFilters = major > 6 || (major === 6 && minor >= 3); + } catch (error) { + viteSupportsHookFilters = false; + } + + return viteSupportsHookFilters; +} + +function withScriptHookFilter(handler) { + return supportsHookFilters() + ? { + filter: { + id: SCRIPT_LANG_RE, + }, + handler, + } + : handler; +} + function hasRolldownSupport(pluginContext) { return !!( pluginContext && @@ -22,10 +57,6 @@ function hasRolldownSupport(pluginContext) { ); } -function hasOxcSupport(config) { - return !!(config.oxc && typeof config.oxc === 'object'); -} - /** @returns {Promise} */ module.exports = async function prefreshPlugin(options = {}) { const prefreshRolldown = await loadPrefreshRolldown(); @@ -57,7 +88,7 @@ function preactOptionsPlugin(forceBabel) { ...oxc, jsx: { ...jsx, - importSource: oxc.jsx.importSource || 'preact', + importSource: jsx.importSource || 'preact', refresh: !forceBabel && command === 'serve', }, jsxRefreshInclude: oxc.jsxRefreshInclude || /\.[jt]sx$/, @@ -77,16 +108,16 @@ function prefreshBabelTransformPlugin(options = {}, forceBabel) { name: 'prefresh-babel-transform', apply: 'serve', configResolved(config) { - shouldSkip = - config.server.hmr === false || (!forceBabel && hasOxcSupport(config)); + shouldSkip = config.server.hmr === false; }, - transform(code, id, transformOptions) { + transform: withScriptHookFilter(function (code, id, transformOptions) { const ssr = typeof transformOptions === 'boolean' ? transformOptions : transformOptions && transformOptions.ssr === true; if ( shouldSkip || + (!forceBabel && hasRolldownSupport(this)) || !SCRIPT_LANG_RE.test(id) || id.includes('node_modules') || id.includes('?worker') || @@ -106,7 +137,7 @@ function prefreshBabelTransformPlugin(options = {}, forceBabel) { ].filter(Boolean); return transform(code, id, parserPlugins); - }, + }), }; } @@ -118,21 +149,17 @@ function prefreshWrapperPlugin(options = {}) { return { name: 'prefresh-wrapper', apply: 'serve', - config(config) { - const include = config.optimizeDeps?.include || []; - + config() { return { optimizeDeps: { - include: [ - ...new Set([...include, '@prefresh/core', '@prefresh/utils']), - ], + include: ['@prefresh/core', '@prefresh/utils'], }, }; }, configResolved(config) { shouldSkip = config.server.hmr === false; }, - async transform(code, id, options) { + transform: withScriptHookFilter(async function (code, id, options) { const ssr = typeof options === 'boolean' ? options @@ -210,12 +237,14 @@ function prefreshWrapperPlugin(options = {}) { `, map: null, }; - }, + }), }; } -const transform = (code, path, plugins) => - transformSync(code, { +const transform = (code, path, plugins) => { + const { transformSync, prefreshBabelPlugin } = loadBabel(); + + return transformSync(code, { plugins: [[prefreshBabelPlugin, { skipEnvCheck: true }]], parserOpts: { plugins, @@ -227,3 +256,4 @@ const transform = (code, path, plugins) => configFile: false, babelrc: false, }); +}; From 6edeb3046367fb326468cd904b0b629b106fac3a Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Thu, 2 Apr 2026 19:13:31 +0200 Subject: [PATCH 6/9] fix(vite): add rolldown to test fixtures --- test/fixture/vite-preact-compat/package.json | 1 + test/fixture/vite-signals/package.json | 1 + test/fixture/vite/package.json | 1 + 3 files changed, 3 insertions(+) diff --git a/test/fixture/vite-preact-compat/package.json b/test/fixture/vite-preact-compat/package.json index 4722d21d..a0e801d5 100644 --- a/test/fixture/vite-preact-compat/package.json +++ b/test/fixture/vite-preact-compat/package.json @@ -8,6 +8,7 @@ }, "devDependencies": { "@prefresh/vite": "file:../../packages/vite", + "rolldown": "^1.0.0-rc.12", "vite": "^8.0.0" }, "resolutions": { diff --git a/test/fixture/vite-signals/package.json b/test/fixture/vite-signals/package.json index bebd31ef..6239ece6 100644 --- a/test/fixture/vite-signals/package.json +++ b/test/fixture/vite-signals/package.json @@ -8,6 +8,7 @@ }, "devDependencies": { "@prefresh/vite": "file:../../packages/vite", + "rolldown": "^1.0.0-rc.12", "vite": "^8.0.0" }, "resolutions": { diff --git a/test/fixture/vite/package.json b/test/fixture/vite/package.json index 4722d21d..a0e801d5 100644 --- a/test/fixture/vite/package.json +++ b/test/fixture/vite/package.json @@ -8,6 +8,7 @@ }, "devDependencies": { "@prefresh/vite": "file:../../packages/vite", + "rolldown": "^1.0.0-rc.12", "vite": "^8.0.0" }, "resolutions": { From 454fd8a63872fb6dbb65a87e46e9f35a11a4be62 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Thu, 2 Apr 2026 19:18:40 +0200 Subject: [PATCH 7/9] Test --- packages/vite/src/index.js | 39 ++++++++++++++++++- .../fixture/vite-preact-compat/vite.config.js | 2 +- test/fixture/vite-signals/vite.config.js | 2 +- test/fixture/vite/vite.config.js | 2 +- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/packages/vite/src/index.js b/packages/vite/src/index.js index 655d06d2..abbad6f1 100644 --- a/packages/vite/src/index.js +++ b/packages/vite/src/index.js @@ -47,6 +47,34 @@ function withScriptHookFilter(handler) { : handler; } +function getEsbuildOptions(config) { + return config.esbuild && typeof config.esbuild === 'object' + ? config.esbuild + : {}; +} + +function getOxcOptions(config) { + return config.oxc && typeof config.oxc === 'object' ? config.oxc : {}; +} + +function stripHandledEsbuildOptions(config) { + const esbuild = getEsbuildOptions(config); + + if (!Object.keys(esbuild).length) return config.esbuild; + + const { jsx, jsxFactory, jsxFragment, jsxImportSource, jsxInject, ...rest } = + esbuild; + + return Object.keys(rest).length ? rest : false; +} + +function resolveJsxRuntime(jsx, esbuild) { + if (jsx.runtime) return jsx.runtime; + if (esbuild.jsx === 'automatic') return 'automatic'; + if (esbuild.jsx === 'preserve') return 'preserve'; + return 'classic'; +} + function hasRolldownSupport(pluginContext) { return !!( pluginContext && @@ -78,17 +106,24 @@ function preactOptionsPlugin(forceBabel) { return { name: 'prefresh-preact-options', config(config, { command }) { - const oxc = config.oxc || {}; + const oxc = getOxcOptions(config); const jsx = oxc.jsx || {}; + const esbuild = getEsbuildOptions(config); const supportsRolldown = hasRolldownSupport(this); return supportsRolldown ? { + esbuild: stripHandledEsbuildOptions(config), oxc: { ...oxc, + jsxInject: oxc.jsxInject || esbuild.jsxInject, jsx: { ...jsx, - importSource: jsx.importSource || 'preact', + runtime: resolveJsxRuntime(jsx, esbuild), + pragma: jsx.pragma || esbuild.jsxFactory || 'h', + pragmaFrag: jsx.pragmaFrag || esbuild.jsxFragment || 'Fragment', + importSource: + jsx.importSource || esbuild.jsxImportSource || 'preact', refresh: !forceBabel && command === 'serve', }, jsxRefreshInclude: oxc.jsxRefreshInclude || /\.[jt]sx$/, diff --git a/test/fixture/vite-preact-compat/vite.config.js b/test/fixture/vite-preact-compat/vite.config.js index 224615fe..430159ff 100644 --- a/test/fixture/vite-preact-compat/vite.config.js +++ b/test/fixture/vite-preact-compat/vite.config.js @@ -4,7 +4,7 @@ export default { server: { port: 3002, }, - esbuild: { + oxc: { jsxFactory: 'h', jsxFragment: 'Fragment' }, diff --git a/test/fixture/vite-signals/vite.config.js b/test/fixture/vite-signals/vite.config.js index c914d2b8..9c6f37d2 100644 --- a/test/fixture/vite-signals/vite.config.js +++ b/test/fixture/vite-signals/vite.config.js @@ -4,7 +4,7 @@ export default { server: { port: 3007 }, - esbuild: { + oxc: { jsxFactory: 'h', jsxFragment: 'Fragment' }, diff --git a/test/fixture/vite/vite.config.js b/test/fixture/vite/vite.config.js index 9af1e0ec..2c8cfe40 100644 --- a/test/fixture/vite/vite.config.js +++ b/test/fixture/vite/vite.config.js @@ -4,7 +4,7 @@ export default { server: { port: 3000, }, - esbuild: { + oxc: { jsxFactory: 'h', jsxFragment: 'Fragment' }, From 0cb11eb1e1c1b0e4201feb25b6ee6d3b5cd49dbb Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Thu, 2 Apr 2026 19:38:39 +0200 Subject: [PATCH 8/9] test(vite): cover both Oxc and Babel paths --- packages/vite/src/index.js | 85 +++++- test/constants.js | 16 +- test/fixture/vite-babel/index.html | 11 + test/fixture/vite-babel/main.jsx | 4 + test/fixture/vite-babel/package.json | 21 ++ test/fixture/vite-babel/src/app.jsx | 35 +++ test/fixture/vite-babel/src/context.jsx | 25 ++ test/fixture/vite-babel/src/effect.jsx | 10 + test/fixture/vite-babel/src/genericCtx.jsx | 28 ++ test/fixture/vite-babel/src/greeting.jsx | 24 ++ test/fixture/vite-babel/src/helpers.js | 15 + test/fixture/vite-babel/src/products.jsx | 14 + test/fixture/vite-babel/src/styles.js | 5 + test/fixture/vite-babel/src/useCounter.js | 6 + test/fixture/vite-babel/vite.config.js | 15 + .../vite-preact-compat-babel/index.html | 11 + .../fixture/vite-preact-compat-babel/main.jsx | 4 + .../vite-preact-compat-babel/package.json | 21 ++ .../vite-preact-compat-babel/src/app.jsx | 21 ++ .../vite-preact-compat-babel/src/test1.jsx | 5 + .../vite-preact-compat-babel/src/test2.jsx | 5 + .../vite-preact-compat-babel/vite.config.js | 15 + test/fixture/vite-signals-babel/index.html | 11 + test/fixture/vite-signals-babel/main.jsx | 4 + test/fixture/vite-signals-babel/package.json | 21 ++ test/fixture/vite-signals-babel/src/Input.jsx | 9 + test/fixture/vite-signals-babel/src/app.jsx | 22 ++ .../fixture/vite-signals-babel/vite.config.js | 15 + test/signals.test.js | 282 +++++++++--------- test/suspense.test.js | 224 +++++++------- 30 files changed, 718 insertions(+), 266 deletions(-) create mode 100644 test/fixture/vite-babel/index.html create mode 100644 test/fixture/vite-babel/main.jsx create mode 100644 test/fixture/vite-babel/package.json create mode 100644 test/fixture/vite-babel/src/app.jsx create mode 100644 test/fixture/vite-babel/src/context.jsx create mode 100644 test/fixture/vite-babel/src/effect.jsx create mode 100644 test/fixture/vite-babel/src/genericCtx.jsx create mode 100644 test/fixture/vite-babel/src/greeting.jsx create mode 100644 test/fixture/vite-babel/src/helpers.js create mode 100644 test/fixture/vite-babel/src/products.jsx create mode 100644 test/fixture/vite-babel/src/styles.js create mode 100644 test/fixture/vite-babel/src/useCounter.js create mode 100644 test/fixture/vite-babel/vite.config.js create mode 100644 test/fixture/vite-preact-compat-babel/index.html create mode 100644 test/fixture/vite-preact-compat-babel/main.jsx create mode 100644 test/fixture/vite-preact-compat-babel/package.json create mode 100644 test/fixture/vite-preact-compat-babel/src/app.jsx create mode 100644 test/fixture/vite-preact-compat-babel/src/test1.jsx create mode 100644 test/fixture/vite-preact-compat-babel/src/test2.jsx create mode 100644 test/fixture/vite-preact-compat-babel/vite.config.js create mode 100644 test/fixture/vite-signals-babel/index.html create mode 100644 test/fixture/vite-signals-babel/main.jsx create mode 100644 test/fixture/vite-signals-babel/package.json create mode 100644 test/fixture/vite-signals-babel/src/Input.jsx create mode 100644 test/fixture/vite-signals-babel/src/app.jsx create mode 100644 test/fixture/vite-signals-babel/vite.config.js diff --git a/packages/vite/src/index.js b/packages/vite/src/index.js index abbad6f1..25ecbcc1 100644 --- a/packages/vite/src/index.js +++ b/packages/vite/src/index.js @@ -4,6 +4,7 @@ const SCRIPT_LANG_RE = /\.(c|m)?(t|j)sx?$/; let babel; let prefreshRolldownPromise; let viteSupportsHookFilters; +let viteVersionParts; function loadBabel() { babel ||= { @@ -23,17 +24,29 @@ function loadPrefreshRolldown() { function supportsHookFilters() { if (viteSupportsHookFilters !== undefined) return viteSupportsHookFilters; + const [major, minor] = getViteVersionParts(); + viteSupportsHookFilters = major > 6 || (major === 6 && minor >= 3); + + return viteSupportsHookFilters; +} + +function getViteVersionParts() { + if (viteVersionParts) return viteVersionParts; + try { - const [major, minor] = require('vite/package.json') + viteVersionParts = require('vite/package.json') .version.split('.') .map(Number); - - viteSupportsHookFilters = major > 6 || (major === 6 && minor >= 3); } catch (error) { - viteSupportsHookFilters = false; + viteVersionParts = [0, 0, 0]; } - return viteSupportsHookFilters; + return viteVersionParts; +} + +function supportsRolldownVite() { + const [major] = getViteVersionParts(); + return major >= 8; } function withScriptHookFilter(handler) { @@ -75,6 +88,16 @@ function resolveJsxRuntime(jsx, esbuild) { return 'classic'; } +function resolvePrefreshJsxOptions(jsx, esbuild) { + return { + ...jsx, + runtime: resolveJsxRuntime(jsx, esbuild), + pragma: jsx.pragma || esbuild.jsxFactory || 'h', + pragmaFrag: jsx.pragmaFrag || esbuild.jsxFragment || 'Fragment', + importSource: jsx.importSource || esbuild.jsxImportSource || 'preact', + }; +} + function hasRolldownSupport(pluginContext) { return !!( pluginContext && @@ -87,16 +110,18 @@ function hasRolldownSupport(pluginContext) { /** @returns {Promise} */ module.exports = async function prefreshPlugin(options = {}) { - const prefreshRolldown = await loadPrefreshRolldown(); const forceBabel = Object.prototype.hasOwnProperty.call( options, 'parserPlugins' ); + const prefreshRolldown = supportsRolldownVite() + ? await loadPrefreshRolldown() + : null; return [ preactOptionsPlugin(forceBabel), prefreshBabelTransformPlugin(options, forceBabel), - prefreshRolldown(), + ...(prefreshRolldown ? [prefreshRolldown()] : []), prefreshWrapperPlugin(options), ]; }; @@ -109,24 +134,36 @@ function preactOptionsPlugin(forceBabel) { const oxc = getOxcOptions(config); const jsx = oxc.jsx || {}; const esbuild = getEsbuildOptions(config); + const prefreshJsx = resolvePrefreshJsxOptions(jsx, esbuild); + const optimizeDeps = config.optimizeDeps || {}; + const rolldownOptions = optimizeDeps.rolldownOptions || {}; + const transformOptions = rolldownOptions.transform || {}; const supportsRolldown = hasRolldownSupport(this); return supportsRolldown ? { esbuild: stripHandledEsbuildOptions(config), + optimizeDeps: { + ...optimizeDeps, + rolldownOptions: { + ...rolldownOptions, + transform: { + ...transformOptions, + jsx: { + ...prefreshJsx, + }, + }, + }, + }, oxc: { ...oxc, + include: oxc.include || SCRIPT_LANG_RE, jsxInject: oxc.jsxInject || esbuild.jsxInject, jsx: { - ...jsx, - runtime: resolveJsxRuntime(jsx, esbuild), - pragma: jsx.pragma || esbuild.jsxFactory || 'h', - pragmaFrag: jsx.pragmaFrag || esbuild.jsxFragment || 'Fragment', - importSource: - jsx.importSource || esbuild.jsxImportSource || 'preact', + ...prefreshJsx, refresh: !forceBabel && command === 'serve', }, - jsxRefreshInclude: oxc.jsxRefreshInclude || /\.[jt]sx$/, + jsxRefreshInclude: oxc.jsxRefreshInclude || SCRIPT_LANG_RE, }, } : {}; @@ -146,13 +183,13 @@ function prefreshBabelTransformPlugin(options = {}, forceBabel) { shouldSkip = config.server.hmr === false; }, transform: withScriptHookFilter(function (code, id, transformOptions) { + const useOxcRefresh = !forceBabel && hasRolldownSupport(this); const ssr = typeof transformOptions === 'boolean' ? transformOptions : transformOptions && transformOptions.ssr === true; if ( shouldSkip || - (!forceBabel && hasRolldownSupport(this)) || !SCRIPT_LANG_RE.test(id) || id.includes('node_modules') || id.includes('?worker') || @@ -171,7 +208,23 @@ function prefreshBabelTransformPlugin(options = {}, forceBabel) { ...((options && options.parserPlugins) || []), ].filter(Boolean); - return transform(code, id, parserPlugins); + if (useOxcRefresh) { + const hasReg = /\$RefreshReg\$\(/.test(code); + const hasSig = /\$RefreshSig\$\(/.test(code); + + if (hasReg || hasSig) return; + } + + const result = transform(code, id, parserPlugins); + + if (useOxcRefresh) { + const hasReg = /\$RefreshReg\$\(/.test(result.code); + const hasSig = /\$RefreshSig\$\(/.test(result.code); + + if (!hasReg && !hasSig) return; + } + + return result; }), }; } diff --git a/test/constants.js b/test/constants.js index 3e268205..6f2463bb 100644 --- a/test/constants.js +++ b/test/constants.js @@ -1,28 +1,42 @@ const path = require('path'); -exports.integrations = ['vite']; +exports.integrations = ['vite', 'vite-babel']; exports.bin = { vite: dir => path.resolve(dir, `./node_modules/vite/bin/vite.js`), + 'vite-babel': dir => path.resolve(dir, `./node_modules/vite/bin/vite.js`), 'vite-preact-compat': dir => path.resolve(dir, `./node_modules/vite/bin/vite.js`), + 'vite-preact-compat-babel': dir => + path.resolve(dir, `./node_modules/vite/bin/vite.js`), 'vite-signals': dir => path.resolve(dir, `./node_modules/vite/bin/vite.js`), + 'vite-signals-babel': dir => + path.resolve(dir, `./node_modules/vite/bin/vite.js`), }; exports.binArgs = { 'vite-preact-compat': [], + 'vite-preact-compat-babel': [], 'vite-signals': [], + 'vite-signals-babel': [], vite: [], + 'vite-babel': [], }; exports.goMessage = { vite: 'ready', + 'vite-babel': 'ready', 'vite-preact-compat': 'ready', + 'vite-preact-compat-babel': 'ready', 'vite-signals': 'ready', + 'vite-signals-babel': 'ready', }; exports.defaultPort = { vite: 3000, + 'vite-babel': 3001, 'vite-preact-compat': 3002, + 'vite-preact-compat-babel': 3003, 'vite-signals': 3007, + 'vite-signals-babel': 3008, }; diff --git a/test/fixture/vite-babel/index.html b/test/fixture/vite-babel/index.html new file mode 100644 index 00000000..0de7aa3f --- /dev/null +++ b/test/fixture/vite-babel/index.html @@ -0,0 +1,11 @@ + + + + + Vite App + + +
+ + + diff --git a/test/fixture/vite-babel/main.jsx b/test/fixture/vite-babel/main.jsx new file mode 100644 index 00000000..4b247c4b --- /dev/null +++ b/test/fixture/vite-babel/main.jsx @@ -0,0 +1,4 @@ +import { render, h } from 'preact' +import { App } from './src/app' + +render(, document.getElementById('app')) diff --git a/test/fixture/vite-babel/package.json b/test/fixture/vite-babel/package.json new file mode 100644 index 00000000..97e52675 --- /dev/null +++ b/test/fixture/vite-babel/package.json @@ -0,0 +1,21 @@ +{ + "scripts": { + "dev": "vite" + }, + "dependencies": { + "goober": "^2.0.36", + "preact": "^10.19.2" + }, + "devDependencies": { + "@babel/core": "^7.22.1", + "@prefresh/babel-plugin": "file:../../packages/babel", + "@prefresh/vite": "file:../../packages/vite", + "rolldown": "^1.0.0-rc.12", + "vite": "^5.0.0" + }, + "resolutions": { + "@prefresh/rolldown": "file:../../packages/rolldown", + "@prefresh/core": "file:../../packages/core", + "@prefresh/utils": "file:../../packages/utils" + } +} diff --git a/test/fixture/vite-babel/src/app.jsx b/test/fixture/vite-babel/src/app.jsx new file mode 100644 index 00000000..ef3981c5 --- /dev/null +++ b/test/fixture/vite-babel/src/app.jsx @@ -0,0 +1,35 @@ +import { h } from 'preact'; +import { useCounter } from './useCounter'; +import { StoreProvider } from './context'; +import { Products } from './products'; +import { Greeting } from './greeting'; +import { Effect } from './effect'; +import { GenericContext } from './genericCtx'; +import { setup } from 'goober'; +import { Style } from './styles'; + +setup(h); + +function Test() { + const [count, increment] = useCounter(); + return ( +
+

Count: {count}

+ +
+ ) +} + +export function App(props) { + return ( + + ) +} diff --git a/test/fixture/vite-babel/src/context.jsx b/test/fixture/vite-babel/src/context.jsx new file mode 100644 index 00000000..4ca0a6ab --- /dev/null +++ b/test/fixture/vite-babel/src/context.jsx @@ -0,0 +1,25 @@ +import { createContext, h } from 'preact'; +import { useState } from 'preact/hooks'; + +export const StoreContext = createContext(); + +export const StoreProvider = ({ children }) => { + const [items, setItems] = useState([]); + + const addItem = (id) => { + if (!items.includes(id)) setItems([...items, id]); + } + + const removeItem = (id) => { + if (items.includes(id)) setItems(items.filter(itemId => itemId !== id)); + } + + return ( + +
    + {items.map(id =>
  • {id}
  • )} +
+ {children} +
+ ) +} diff --git a/test/fixture/vite-babel/src/effect.jsx b/test/fixture/vite-babel/src/effect.jsx new file mode 100644 index 00000000..a9d966be --- /dev/null +++ b/test/fixture/vite-babel/src/effect.jsx @@ -0,0 +1,10 @@ +import { h } from 'preact'; +import { useEffect, useState } from 'preact/hooks'; + +export const Effect = () => { + const [state, setState] = useState('hello world'); + + useEffect(() => { setState('hello world'); }, []); + + return

{state}

+} diff --git a/test/fixture/vite-babel/src/genericCtx.jsx b/test/fixture/vite-babel/src/genericCtx.jsx new file mode 100644 index 00000000..87333491 --- /dev/null +++ b/test/fixture/vite-babel/src/genericCtx.jsx @@ -0,0 +1,28 @@ +import { h } from 'preact'; +import { createContextWithoutDefault } from './helpers'; + +const [ContextA, useContextA] = + createContextWithoutDefault('Context A Error'); +const [ContextB, useContextB] = + createContextWithoutDefault('Context B Error'); + +export function GenericContext() { + return ( + + + + ); +} + +const Test = () => { + let contextB; + try { + // Expect this to throw. + contextB = useContextB(); + } catch (e) { + return

Correct behavior: {e.message}

; + } + + // Instead it has the value of "Context A"! + return

{contextB}

; +}; diff --git a/test/fixture/vite-babel/src/greeting.jsx b/test/fixture/vite-babel/src/greeting.jsx new file mode 100644 index 00000000..58ddccf7 --- /dev/null +++ b/test/fixture/vite-babel/src/greeting.jsx @@ -0,0 +1,24 @@ +import { Component, h } from 'preact' + +export class Greeting extends Component { + constructor(props) { + super(props); + this.state = { greeting: 'hi' }; + this.setGreeting = this.setGreeting.bind(this); + } + + setGreeting() { + this.setState({ greeting: 'bye' }); + } + + render() { + return ( +
+

I'm a class component

+

{this.state.greeting}

+ +
+ ) + } + +} diff --git a/test/fixture/vite-babel/src/helpers.js b/test/fixture/vite-babel/src/helpers.js new file mode 100644 index 00000000..d7559188 --- /dev/null +++ b/test/fixture/vite-babel/src/helpers.js @@ -0,0 +1,15 @@ +import { createContext } from 'preact'; +import { useContext } from 'preact/hooks'; + +export const createContextWithoutDefault = (errorMessage) => { + const emptyContext = Symbol(); + const context = createContext(emptyContext); + const useCtx = () => { + const ctx = useContext(context); + if ((ctx) === emptyContext) { + throw new Error(errorMessage); + } + return ctx; + }; + return [context, useCtx]; +}; diff --git a/test/fixture/vite-babel/src/products.jsx b/test/fixture/vite-babel/src/products.jsx new file mode 100644 index 00000000..71c30bed --- /dev/null +++ b/test/fixture/vite-babel/src/products.jsx @@ -0,0 +1,14 @@ +import { h } from 'preact'; +import { useContext } from 'preact/hooks'; +import { StoreContext } from './context'; + +const products = ['apple', 'peach'] + +export const Products = () => { + const { addItem, removeItem, items } = useContext(StoreContext); + return products.map(id => ( +
items.includes(id) ? removeItem(id) : addItem(id)}> + {id} +
+ )) +} diff --git a/test/fixture/vite-babel/src/styles.js b/test/fixture/vite-babel/src/styles.js new file mode 100644 index 00000000..424afca3 --- /dev/null +++ b/test/fixture/vite-babel/src/styles.js @@ -0,0 +1,5 @@ +import { styled } from 'goober'; + +export const Style = styled('div')` + background-color: #000; +`; diff --git a/test/fixture/vite-babel/src/useCounter.js b/test/fixture/vite-babel/src/useCounter.js new file mode 100644 index 00000000..8ccc2dc4 --- /dev/null +++ b/test/fixture/vite-babel/src/useCounter.js @@ -0,0 +1,6 @@ +import { useState } from 'preact/hooks'; + +export const useCounter = () => { + const [state, setState] = useState(0); + return [state, () => setState(state + 1)]; +}; diff --git a/test/fixture/vite-babel/vite.config.js b/test/fixture/vite-babel/vite.config.js new file mode 100644 index 00000000..80d80997 --- /dev/null +++ b/test/fixture/vite-babel/vite.config.js @@ -0,0 +1,15 @@ +import prefresh from '@prefresh/vite'; + +export default { + server: { + port: 3001, + }, + esbuild: { + jsxFactory: 'h', + jsxFragment: 'Fragment' + }, + optimizeDeps: { + include: ['preact/hooks'] + }, + plugins: [prefresh()] +}; diff --git a/test/fixture/vite-preact-compat-babel/index.html b/test/fixture/vite-preact-compat-babel/index.html new file mode 100644 index 00000000..0de7aa3f --- /dev/null +++ b/test/fixture/vite-preact-compat-babel/index.html @@ -0,0 +1,11 @@ + + + + + Vite App + + +
+ + + diff --git a/test/fixture/vite-preact-compat-babel/main.jsx b/test/fixture/vite-preact-compat-babel/main.jsx new file mode 100644 index 00000000..47fd939e --- /dev/null +++ b/test/fixture/vite-preact-compat-babel/main.jsx @@ -0,0 +1,4 @@ +import { render, createElement as h } from 'preact/compat' +import { App } from './src/app' + +render(, document.getElementById('app')) diff --git a/test/fixture/vite-preact-compat-babel/package.json b/test/fixture/vite-preact-compat-babel/package.json new file mode 100644 index 00000000..97e52675 --- /dev/null +++ b/test/fixture/vite-preact-compat-babel/package.json @@ -0,0 +1,21 @@ +{ + "scripts": { + "dev": "vite" + }, + "dependencies": { + "goober": "^2.0.36", + "preact": "^10.19.2" + }, + "devDependencies": { + "@babel/core": "^7.22.1", + "@prefresh/babel-plugin": "file:../../packages/babel", + "@prefresh/vite": "file:../../packages/vite", + "rolldown": "^1.0.0-rc.12", + "vite": "^5.0.0" + }, + "resolutions": { + "@prefresh/rolldown": "file:../../packages/rolldown", + "@prefresh/core": "file:../../packages/core", + "@prefresh/utils": "file:../../packages/utils" + } +} diff --git a/test/fixture/vite-preact-compat-babel/src/app.jsx b/test/fixture/vite-preact-compat-babel/src/app.jsx new file mode 100644 index 00000000..18248673 --- /dev/null +++ b/test/fixture/vite-preact-compat-babel/src/app.jsx @@ -0,0 +1,21 @@ +import { lazy, Suspense, useState, createElement as h, Fragment } from 'preact/compat'; + +const Test1Lazy = lazy(() => import('./test1.jsx')); +const Test2Lazy = lazy(() => import('./test2.jsx')); + +export function App() { + const [show, setShow] = useState(true); + + const toggle = () => { + setShow(!show); + }; + + return ( + <> + Loading}> + {show ? : } + + + + ); +} diff --git a/test/fixture/vite-preact-compat-babel/src/test1.jsx b/test/fixture/vite-preact-compat-babel/src/test1.jsx new file mode 100644 index 00000000..2a840576 --- /dev/null +++ b/test/fixture/vite-preact-compat-babel/src/test1.jsx @@ -0,0 +1,5 @@ +import { createElement as h } from 'preact/compat'; + +export default function Test1() { + return

Test 1

; +} diff --git a/test/fixture/vite-preact-compat-babel/src/test2.jsx b/test/fixture/vite-preact-compat-babel/src/test2.jsx new file mode 100644 index 00000000..9861d0f8 --- /dev/null +++ b/test/fixture/vite-preact-compat-babel/src/test2.jsx @@ -0,0 +1,5 @@ +import { createElement as h } from 'preact/compat'; + +export default function Test2() { + return

Test 2

; +} diff --git a/test/fixture/vite-preact-compat-babel/vite.config.js b/test/fixture/vite-preact-compat-babel/vite.config.js new file mode 100644 index 00000000..b6554d88 --- /dev/null +++ b/test/fixture/vite-preact-compat-babel/vite.config.js @@ -0,0 +1,15 @@ +import prefresh from '@prefresh/vite'; + +export default { + server: { + port: 3003, + }, + esbuild: { + jsxFactory: 'h', + jsxFragment: 'Fragment' + }, + optimizeDeps: { + include: ['preact/hooks', 'preact/compat', 'preact'] + }, + plugins: [prefresh()] +}; diff --git a/test/fixture/vite-signals-babel/index.html b/test/fixture/vite-signals-babel/index.html new file mode 100644 index 00000000..0de7aa3f --- /dev/null +++ b/test/fixture/vite-signals-babel/index.html @@ -0,0 +1,11 @@ + + + + + Vite App + + +
+ + + diff --git a/test/fixture/vite-signals-babel/main.jsx b/test/fixture/vite-signals-babel/main.jsx new file mode 100644 index 00000000..4b247c4b --- /dev/null +++ b/test/fixture/vite-signals-babel/main.jsx @@ -0,0 +1,4 @@ +import { render, h } from 'preact' +import { App } from './src/app' + +render(, document.getElementById('app')) diff --git a/test/fixture/vite-signals-babel/package.json b/test/fixture/vite-signals-babel/package.json new file mode 100644 index 00000000..927031cb --- /dev/null +++ b/test/fixture/vite-signals-babel/package.json @@ -0,0 +1,21 @@ +{ + "scripts": { + "dev": "vite" + }, + "dependencies": { + "@preact/signals": "^2.0.0", + "preact": "^10.19.2" + }, + "devDependencies": { + "@babel/core": "^7.22.1", + "@prefresh/babel-plugin": "file:../../packages/babel", + "@prefresh/vite": "file:../../packages/vite", + "rolldown": "^1.0.0-rc.12", + "vite": "^5.0.0" + }, + "resolutions": { + "@prefresh/rolldown": "file:../../packages/rolldown", + "@prefresh/core": "file:../../packages/core", + "@prefresh/utils": "file:../../packages/utils" + } +} diff --git a/test/fixture/vite-signals-babel/src/Input.jsx b/test/fixture/vite-signals-babel/src/Input.jsx new file mode 100644 index 00000000..b3ed6a03 --- /dev/null +++ b/test/fixture/vite-signals-babel/src/Input.jsx @@ -0,0 +1,9 @@ +import { h } from 'preact'; +import { signal } from '@preact/signals'; + +const inputValue = signal('foo') +export function Input() { + return ( + { inputValue.value = e.currentTarget.value }} /> + ) +} diff --git a/test/fixture/vite-signals-babel/src/app.jsx b/test/fixture/vite-signals-babel/src/app.jsx new file mode 100644 index 00000000..62a882df --- /dev/null +++ b/test/fixture/vite-signals-babel/src/app.jsx @@ -0,0 +1,22 @@ +import { h } from 'preact'; +import { signal } from '@preact/signals'; +import { Input } from './Input.jsx' + +const count = signal(0) +function Counter() { + return ( +
+

Count: {count}

+ +
+ ) +} + +export function App() { + return ( +
+ + +
+ ) +} diff --git a/test/fixture/vite-signals-babel/vite.config.js b/test/fixture/vite-signals-babel/vite.config.js new file mode 100644 index 00000000..8aeaab8a --- /dev/null +++ b/test/fixture/vite-signals-babel/vite.config.js @@ -0,0 +1,15 @@ +import prefresh from '@prefresh/vite'; + +export default { + server: { + port: 3008 + }, + esbuild: { + jsxFactory: 'h', + jsxFragment: 'Fragment' + }, + optimizeDeps: { + include: ['preact/hooks'] + }, + plugins: [prefresh()] +}; diff --git a/test/signals.test.js b/test/signals.test.js index fd672273..6c066667 100644 --- a/test/signals.test.js +++ b/test/signals.test.js @@ -13,162 +13,166 @@ const { bin, binArgs, goMessage, defaultPort } = require('./constants'); const TIMEOUT = 1000; describe('Signals', () => { - const integration = 'vite-signals'; - let devServer, browser, page, serverConsoleListener; - - const browserConsoleListener = msg => { - console.log('[BROWSER LOG]: ', msg); - }; - - jest.setTimeout(100000); - - afterAll(async () => { - if (process.env.DEBUG) - page.removeListener('console', browserConsoleListener); - - if (browser) await browser.close(); - if (devServer) { - devServer.kill('SIGTERM', { - forceKillAfterTimeout: 0, + ['vite-signals', 'vite-signals-babel'].forEach(integration => { + let devServer, browser, page, serverConsoleListener; + + const browserConsoleListener = msg => { + console.log('[BROWSER LOG]: ', msg); + }; + + jest.setTimeout(100000); + + describe(integration, () => { + afterAll(async () => { + if (process.env.DEBUG) + page.removeListener('console', browserConsoleListener); + + if (browser) await browser.close(); + if (devServer) { + devServer.kill('SIGTERM', { + forceKillAfterTimeout: 0, + }); + } + + try { + await fs.remove(getTempDir(integration)); + } catch (e) {} }); - } - try { - await fs.remove(getTempDir(integration)); - } catch (e) {} - }); + beforeAll(async () => { + await timeout(2000); + try { + await fs.remove(getTempDir(integration)); + } catch (e) {} + + await fs.copy(getFixtureDir(integration), getTempDir(integration), { + filter: file => !/dist|node_modules/.test(file), + }); + + await execa('yarn', { cwd: getTempDir(integration) }); + + browser = await puppeteer.launch({ + headless: 'new', + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }); + page = await browser.newPage(); + + devServer = execa( + bin[integration](getTempDir(integration)), + binArgs[integration], + { + cwd: getTempDir(integration), + } + ); + + await new Promise(resolve => { + devServer.stdout.on( + 'data', + (serverConsoleListener = data => { + if (process.env.DEBUG) + console.log('[SERVER LOG]: ', data.toString()); + if (data.toString().match(goMessage[integration])) { + resolve(); + } + }) + ); + + devServer.stderr.on( + 'data', + (serverConsoleListener = data => { + console.log('[ERROR SERVER LOG]: ', data.toString()); + }) + ); + }); + + page = await browser.newPage(); + if (process.env.DEBUG) page.on('console', browserConsoleListener); + + await page.goto('http://localhost:' + defaultPort[integration]); + }); - beforeAll(async () => { - await timeout(2000); - try { - await fs.remove(getTempDir(integration)); - } catch (e) {} + const getEl = async selectorOrEl => { + return typeof selectorOrEl === 'string' + ? await page.$(selectorOrEl) + : selectorOrEl; + }; + + const getText = async selectorOrEl => { + const el = await getEl(selectorOrEl); + return el ? el.evaluate(el => el.textContent) : null; + }; + + const getInputValue = async selectorOrEl => { + const el = await getEl(selectorOrEl); + return el ? el.evaluate(el => el.value) : null; + }; + + async function updateFile(file, replacer) { + const compPath = path.join(getTempDir(integration), file); + const content = await fs.readFile(compPath, 'utf-8'); + await fs.writeFile(compPath, replacer(content)); + } - await fs.copy(getFixtureDir(integration), getTempDir(integration), { - filter: file => !/dist|node_modules/.test(file), - }); + test('Can increment after HMR', async () => { + const increment = await page.$('.increment'); + const countValue = await page.$('.value'); + await expectByPolling(() => getText(countValue), 'Count: 0'); + await increment.click(); + await expectByPolling(() => getText(countValue), 'Count: 1'); - await execa('yarn', { cwd: getTempDir(integration) }); + await updateFile('src/app.jsx', content => + content.replace('Count:', 'count:') + ); - browser = await puppeteer.launch({ - headless: 'new', - args: ['--no-sandbox', '--disable-setuid-sandbox'], - }); - page = await browser.newPage(); + await timeout(TIMEOUT); + await expectByPolling(() => getText(countValue), 'count: 0'); - devServer = execa( - bin[integration](getTempDir(integration)), - binArgs[integration], - { - cwd: getTempDir(integration), - } - ); - - await new Promise(resolve => { - devServer.stdout.on( - 'data', - (serverConsoleListener = data => { - if (process.env.DEBUG) console.log('[SERVER LOG]: ', data.toString()); - if (data.toString().match(goMessage[integration])) { - resolve(); - } - }) - ); - - devServer.stderr.on( - 'data', - (serverConsoleListener = data => { - console.log('[ERROR SERVER LOG]: ', data.toString()); - }) - ); - }); + await increment.click(); + await increment.click(); + await increment.click(); - page = await browser.newPage(); - if (process.env.DEBUG) page.on('console', browserConsoleListener); + await expectByPolling(() => getText(countValue), 'count: 3'); - await page.goto('http://localhost:' + defaultPort[integration]); - }); + await updateFile('src/app.jsx', content => + content.replace('count:', 'Count:') + ); + await timeout(TIMEOUT); + await expectByPolling(() => getText(countValue), 'Count: 0'); - const getEl = async selectorOrEl => { - return typeof selectorOrEl === 'string' - ? await page.$(selectorOrEl) - : selectorOrEl; - }; - - const getText = async selectorOrEl => { - const el = await getEl(selectorOrEl); - return el ? el.evaluate(el => el.textContent) : null; - }; - - const getInputValue = async selectorOrEl => { - const el = await getEl(selectorOrEl); - return el ? el.evaluate(el => el.value) : null; - }; - - async function updateFile(file, replacer) { - const compPath = path.join(getTempDir(integration), file); - const content = await fs.readFile(compPath, 'utf-8'); - await fs.writeFile(compPath, replacer(content)); - } - - test('Can increment after HMR', async () => { - const increment = await page.$('.increment'); - const countValue = await page.$('.value'); - await expectByPolling(() => getText(countValue), 'Count: 0'); - await increment.click(); - await expectByPolling(() => getText(countValue), 'Count: 1'); - - await updateFile('src/app.jsx', content => - content.replace('Count:', 'count:') - ); - - await timeout(TIMEOUT); - await expectByPolling(() => getText(countValue), 'count: 0'); - - await increment.click(); - await increment.click(); - await increment.click(); - - await expectByPolling(() => getText(countValue), 'count: 3'); - - await updateFile('src/app.jsx', content => - content.replace('count:', 'Count:') - ); - await timeout(TIMEOUT); - await expectByPolling(() => getText(countValue), 'Count: 0'); - - await increment.click(); - await expectByPolling(() => getText(countValue), 'Count: 1'); - }); + await increment.click(); + await expectByPolling(() => getText(countValue), 'Count: 1'); + }); - test('Reacts to adjusting the initial value', async () => { - const countValue = await page.$('.value'); + test('Reacts to adjusting the initial value', async () => { + const countValue = await page.$('.value'); - await updateFile('src/app.jsx', content => - content.replace('signal(0)', 'signal(10)') - ); + await updateFile('src/app.jsx', content => + content.replace('signal(0)', 'signal(10)') + ); - await timeout(TIMEOUT); - await expectByPolling(() => getText(countValue), 'Count: 10'); - }); + await timeout(TIMEOUT); + await expectByPolling(() => getText(countValue), 'Count: 10'); + }); - test('Can change input values', async () => { - const input = await page.$('.input'); - await expectByPolling(() => getInputValue(input), 'foo'); + test('Can change input values', async () => { + const input = await page.$('.input'); + await expectByPolling(() => getInputValue(input), 'foo'); - await page.focus('.input'); - await page.keyboard.type('foooo'); - await expectByPolling(() => getInputValue(input), 'foooo'); + await page.focus('.input'); + await page.keyboard.type('foooo'); + await expectByPolling(() => getInputValue(input), 'foooo'); - await updateFile('src/Input.jsx', content => - content.replace("signal('foo')", "signal('bar')") - ); + await updateFile('src/Input.jsx', content => + content.replace("signal('foo')", "signal('bar')") + ); - await timeout(TIMEOUT); - //await expectByPolling(() => getInputValue(input), 'bar'); + await timeout(TIMEOUT); + //await expectByPolling(() => getInputValue(input), 'bar'); - await page.focus('.input'); - await page.keyboard.type('barfoo'); - await expectByPolling(() => getInputValue(input), 'barfoo'); + await page.focus('.input'); + await page.keyboard.type('barfoo'); + await expectByPolling(() => getInputValue(input), 'barfoo'); + }); + }); }); }); diff --git a/test/suspense.test.js b/test/suspense.test.js index 40aa86ea..7818b1b0 100644 --- a/test/suspense.test.js +++ b/test/suspense.test.js @@ -13,127 +13,131 @@ const { bin, binArgs, goMessage, defaultPort } = require('./constants'); const TIMEOUT = 1000; describe('Suspense', () => { - const integration = 'vite-preact-compat'; - let devServer, browser, page, serverConsoleListener; - - const browserConsoleListener = msg => { - console.log('[BROWSER LOG]: ', msg); - }; - - jest.setTimeout(100000); - - afterAll(async () => { - if (process.env.DEBUG) - page.removeListener('console', browserConsoleListener); - - if (browser) await browser.close(); - if (devServer) { - devServer.kill('SIGTERM', { - forceKillAfterTimeout: 0, + ['vite-preact-compat', 'vite-preact-compat-babel'].forEach(integration => { + let devServer, browser, page, serverConsoleListener; + + const browserConsoleListener = msg => { + console.log('[BROWSER LOG]: ', msg); + }; + + jest.setTimeout(100000); + + describe(integration, () => { + afterAll(async () => { + if (process.env.DEBUG) + page.removeListener('console', browserConsoleListener); + + if (browser) await browser.close(); + if (devServer) { + devServer.kill('SIGTERM', { + forceKillAfterTimeout: 0, + }); + } + + try { + await fs.remove(getTempDir(integration)); + } catch (e) {} }); - } - - try { - await fs.remove(getTempDir(integration)); - } catch (e) {} - }); - - beforeAll(async () => { - await timeout(2000); - try { - await fs.remove(getTempDir(integration)); - } catch (e) {} - - await fs.copy(getFixtureDir(integration), getTempDir(integration), { - filter: file => !/dist|node_modules/.test(file), - }); - - await execa('yarn', { cwd: getTempDir(integration) }); - - browser = await puppeteer.launch({ - headless: 'new', - args: ['--no-sandbox', '--disable-setuid-sandbox'], - }); - page = await browser.newPage(); - devServer = execa( - bin[integration](getTempDir(integration)), - binArgs[integration], - { - cwd: getTempDir(integration), - } - ); - - await new Promise(resolve => { - devServer.stdout.on( - 'data', - (serverConsoleListener = data => { - if (process.env.DEBUG) console.log('[SERVER LOG]: ', data.toString()); - if (data.toString().match(goMessage[integration])) { - resolve(); + beforeAll(async () => { + await timeout(2000); + try { + await fs.remove(getTempDir(integration)); + } catch (e) {} + + await fs.copy(getFixtureDir(integration), getTempDir(integration), { + filter: file => !/dist|node_modules/.test(file), + }); + + await execa('yarn', { cwd: getTempDir(integration) }); + + browser = await puppeteer.launch({ + headless: 'new', + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }); + page = await browser.newPage(); + + devServer = execa( + bin[integration](getTempDir(integration)), + binArgs[integration], + { + cwd: getTempDir(integration), } - }) - ); - - devServer.stderr.on( - 'data', - (serverConsoleListener = data => { - console.log('[ERROR SERVER LOG]: ', data.toString()); - }) - ); - }); - - page = await browser.newPage(); - if (process.env.DEBUG) page.on('console', browserConsoleListener); - - await page.goto('http://localhost:' + defaultPort[integration]); - }); - - const getEl = async selectorOrEl => { - return typeof selectorOrEl === 'string' - ? await page.$(selectorOrEl) - : selectorOrEl; - }; - - const getText = async selectorOrEl => { - const el = await getEl(selectorOrEl); - return el ? el.evaluate(el => el.textContent) : null; - }; + ); + + await new Promise(resolve => { + devServer.stdout.on( + 'data', + (serverConsoleListener = data => { + if (process.env.DEBUG) + console.log('[SERVER LOG]: ', data.toString()); + if (data.toString().match(goMessage[integration])) { + resolve(); + } + }) + ); + + devServer.stderr.on( + 'data', + (serverConsoleListener = data => { + console.log('[ERROR SERVER LOG]: ', data.toString()); + }) + ); + }); + + page = await browser.newPage(); + if (process.env.DEBUG) page.on('console', browserConsoleListener); + + await page.goto('http://localhost:' + defaultPort[integration]); + }); - async function updateFile(file, replacer) { - const compPath = path.join(getTempDir(integration), file); - const content = await fs.readFile(compPath, 'utf-8'); - await fs.writeFile(compPath, replacer(content)); - } + const getEl = async selectorOrEl => { + return typeof selectorOrEl === 'string' + ? await page.$(selectorOrEl) + : selectorOrEl; + }; + + const getText = async selectorOrEl => { + const el = await getEl(selectorOrEl); + return el ? el.evaluate(el => el.textContent) : null; + }; + + async function updateFile(file, replacer) { + const compPath = path.join(getTempDir(integration), file); + const content = await fs.readFile(compPath, 'utf-8'); + await fs.writeFile(compPath, replacer(content)); + } - test('Can make change to lazy loaded components', async () => { - const button = await page.$('.toggle'); - let text1 = await page.$('.test-1'); - await expectByPolling(() => getText(text1), 'Test 1'); + test('Can make change to lazy loaded components', async () => { + const button = await page.$('.toggle'); + let text1 = await page.$('.test-1'); + await expectByPolling(() => getText(text1), 'Test 1'); - await updateFile('src/test1.jsx', content => - content.replace('Test 1', 'Test 1!!!') - ); + await updateFile('src/test1.jsx', content => + content.replace('Test 1', 'Test 1!!!') + ); - await timeout(TIMEOUT); - await expectByPolling(() => getText(text1), 'Test 1!!!'); + await timeout(TIMEOUT); + await expectByPolling(() => getText(text1), 'Test 1!!!'); - await button.click(); - await timeout(TIMEOUT); + await button.click(); + await timeout(TIMEOUT); - const text2 = await page.$('.test-2'); - await expectByPolling(() => getText(text2), 'Test 2'); + const text2 = await page.$('.test-2'); + await expectByPolling(() => getText(text2), 'Test 2'); - await button.click(); - await timeout(TIMEOUT); + await button.click(); + await timeout(TIMEOUT); - text1 = await page.$('.test-1'); - await expectByPolling(() => getText(text1), 'Test 1!!!'); + text1 = await page.$('.test-1'); + await expectByPolling(() => getText(text1), 'Test 1!!!'); - await updateFile('src/test1.jsx', content => - content.replace('Test 1!!!', 'Test 1!') - ); - await timeout(TIMEOUT); - await expectByPolling(() => getText(text1), 'Test 1!'); + await updateFile('src/test1.jsx', content => + content.replace('Test 1!!!', 'Test 1!') + ); + await timeout(TIMEOUT); + await expectByPolling(() => getText(text1), 'Test 1!'); + }); + }); }); }); From bd22f930e2e44a576784c4109108d61b2ff783f9 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Mon, 6 Apr 2026 07:13:19 +0200 Subject: [PATCH 9/9] Bump major, just to be safe --- .changeset/slow-lamps-help.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/slow-lamps-help.md b/.changeset/slow-lamps-help.md index a22ab66d..a4799b30 100644 --- a/.changeset/slow-lamps-help.md +++ b/.changeset/slow-lamps-help.md @@ -1,5 +1,5 @@ --- -'@prefresh/vite': minor +'@prefresh/vite': major --- Add true vite 8 support with native rolldown rather than babel