diff --git a/.changeset/small-peaches-wave.md b/.changeset/small-peaches-wave.md new file mode 100644 index 000000000..b9c1ae56e --- /dev/null +++ b/.changeset/small-peaches-wave.md @@ -0,0 +1,5 @@ +--- +"@preact/signals-react-transform-rolldown": minor +--- + +Add a native Rolldown plugin for the React Signals transform so Rolldown and Vite Rolldown builds can automatically subscribe React components and hooks to signal reads. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ef92f5ca1..cb2831640 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -53,4 +53,4 @@ jobs: working-directory: packages/react run: | pnpm i react@16 react-dom@16 react-router-dom@5 - pnpm -w exec cross-env COVERAGE=true MINIFY=true vitest run packages/react + pnpm -w exec cross-env COVERAGE=true MINIFY=true vitest run ./packages/react/ diff --git a/package.json b/package.json index f94f34ae8..cbff0f8cf 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "scripts": { "prebuild": "shx rm -rf packages/*/dist/", "build": "run-p build:* && pnpm _build:devtools-ui", - "_build": "microbundle --raw --globals @preact/signals-core=preactSignalsCore,preact/hooks=preactHooks,@preact/signals-react/runtime=reactSignalsRuntime", + "_build": "microbundle --raw --globals @preact/signals-core=preactSignalsCore,preact/hooks=preactHooks,@preact/signals-react/runtime=reactSignalsRuntime,rolldown/utils=rolldownUtils", "build:core": "pnpm _build --cwd packages/core && pnpm postbuild:core", "build:debug": "pnpm _build --cwd packages/debug && pnpm postbuild:debug", "build:devtools-adapter": "pnpm _build --cwd packages/devtools-adapter && pnpm postbuild:devtools-adapter", @@ -16,6 +16,7 @@ "build:react-utils": "pnpm _build --cwd packages/react/utils && pnpm postbuild:react-utils", "build:react-runtime": "pnpm _build --cwd packages/react/runtime && pnpm postbuild:react-runtime", "build:react-transform": "pnpm _build --no-compress --cwd packages/react-transform", + "build:react-transform-rolldown": "pnpm _build --no-compress --cwd packages/react-transform-rolldown --external \"rolldown,rolldown/utils,rolldown-string\"", "build:preact-transform": "pnpm _build --no-compress --cwd packages/preact-transform", "postbuild:core": "shx mv -f packages/core/dist/index.d.ts packages/core/dist/signals-core.d.ts", "postbuild:debug": "shx mv -f packages/debug/dist/debug/src/index.d.ts packages/debug/dist/signals-debug.d.ts", diff --git a/packages/react-transform-rolldown/CHANGELOG.md b/packages/react-transform-rolldown/CHANGELOG.md new file mode 100644 index 000000000..b108b521f --- /dev/null +++ b/packages/react-transform-rolldown/CHANGELOG.md @@ -0,0 +1 @@ +# @preact/signals-react-transform-rolldown diff --git a/packages/react-transform-rolldown/README.md b/packages/react-transform-rolldown/README.md new file mode 100644 index 000000000..6336988d3 --- /dev/null +++ b/packages/react-transform-rolldown/README.md @@ -0,0 +1,56 @@ +# Signals React Transform Rolldown Plugin + +> A Rolldown plugin to transform React components so they automatically subscribe to Preact Signals. + +This package applies the React Signals transform during Rolldown builds with Rolldown's native magic string pipeline, so React components and hooks can subscribe to signal reads without wiring Babel up manually. + +## Installation + +```sh +npm i --save-dev @preact/signals-react-transform-rolldown +npm i react @preact/signals-react +``` + +## Usage + +```ts +import reactSignalsTransform from "@preact/signals-react-transform-rolldown"; + +export default { + plugins: [ + reactSignalsTransform({ + mode: "auto", + }), + ], +}; +``` + +## Options + +This plugin forwards the same options as `@preact/signals-react-transform`: + +- `mode` +- `importSource` +- `detectTransformedJSX` +- `experimental` + +Example: + +```ts +reactSignalsTransform({ + detectTransformedJSX: true, + experimental: { + debug: true, + }, +}); +``` + +## Notes + +- Run it before other JSX transforms. +- The generated code imports `useSignals` from `@preact/signals-react/runtime` by default. +- When your code is already compiled to `react/jsx-runtime` or `React.createElement`, enable `detectTransformedJSX`. + +## License + +`MIT`, see the [LICENSE](../../LICENSE) file. diff --git a/packages/react-transform-rolldown/package.json b/packages/react-transform-rolldown/package.json new file mode 100644 index 000000000..255cb98d7 --- /dev/null +++ b/packages/react-transform-rolldown/package.json @@ -0,0 +1,78 @@ +{ + "name": "@preact/signals-react-transform-rolldown", + "version": "0.0.0", + "license": "MIT", + "description": "Rolldown plugin for the React Signals transform", + "keywords": [ + "plugin", + "react", + "rolldown", + "rolldown-plugin", + "signals" + ], + "authors": [ + "The Preact Authors (https://github.com/preactjs/signals/contributors)" + ], + "repository": { + "type": "git", + "url": "https://github.com/preactjs/signals", + "directory": "packages/react-transform-rolldown" + }, + "bugs": "https://github.com/preactjs/signals/issues", + "homepage": "https://preactjs.com", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + }, + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "source": "src/index.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "files": [ + "dist", + "src", + "CHANGELOG.md", + "LICENSE", + "README.md" + ], + "scripts": { + "build": "microbundle --raw --no-compress --external \"rolldown,rolldown/utils,rolldown-string\" --globals \"rolldown/utils=rolldownUtils\"", + "test": "cd ../.. && pnpm vitest run ./packages/react-transform-rolldown/test/node/*.test.ts", + "prepublishOnly": "cd ../.. && pnpm build:react-transform-rolldown" + }, + "dependencies": { + "rolldown-string": "^0.3.0" + }, + "peerDependencies": { + "@preact/signals-react": "^3.9.1", + "react": "^16.14.0 || 17.x || 18.x || 19.x", + "rolldown": "^1.0.0-rc.9", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + }, + "devDependencies": { + "@preact/signals-core": "workspace:*", + "@preact/signals-react": "workspace:^3.9.1", + "@types/react": "18.0.18", + "@types/react-dom": "18.0.6", + "prettier": "^3.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "rolldown": "^1.0.0-rc.9" + }, + "publishConfig": { + "access": "public", + "provenance": true + } +} diff --git a/packages/react-transform-rolldown/src/index.ts b/packages/react-transform-rolldown/src/index.ts new file mode 100644 index 000000000..527b6bcce --- /dev/null +++ b/packages/react-transform-rolldown/src/index.ts @@ -0,0 +1,1117 @@ +/* oxlint-disable */ +// @ts-nocheck + +import { withMagicString } from "rolldown-string"; +import type { Plugin } from "rolldown"; +import { parseSync } from "rolldown/utils"; +import type { ESTree } from "rolldown/utils"; +import type { ReactSignalsTransformPluginOptions } from "./types.ts"; + +export type { ReactSignalsTransformPluginOptions } from "./types.ts"; + +const optOutCommentIdentifier = /(^|\s)@no(Use|Track)Signals(\s|$)/; +const optInCommentIdentifier = /(^|\s)@(use|track)Signals(\s|$)/; +const defaultImportSource = "@preact/signals-react/runtime"; +const defaultHookIdentifier = "_useSignals"; +const effectIdentifier = "_effect"; + +const UNMANAGED = "0"; +const MANAGED_COMPONENT = "1"; +const MANAGED_HOOK = "2"; + +const signalCallNames = new Set([ + "signal", + "computed", + "useSignal", + "useComputed", +]); + +const jsxPackages = { + "react/jsx-runtime": ["jsx", "jsxs"], + "react/jsx-dev-runtime": ["jsxDEV"], + react: ["createElement"], +}; + +type FunctionLike = + | ESTree.FunctionDeclaration + | ESTree.FunctionExpression + | ESTree.ArrowFunctionExpression; + +interface FunctionInfo { + node: FunctionLike; + name: string | null; + containsJSX: boolean; + maybeUsesSignal: boolean; +} + +function basename(filename: string | undefined): string | undefined { + return filename?.split(/[\\/]/).pop(); +} + +function looksLikeJSX(code: string): boolean { + return /<>|<\/[A-Za-z]|<[A-Za-z]/.test(code); +} + +function getParseOptions(id: string, code: string) { + const cleanId = id.replace(/\?.*$/, ""); + const isCommonJS = + /(^|\W)require\s*\(|(^|\W)module\.exports\b|(^|\W)exports\./.test(code); + + let lang: "js" | "jsx" | "ts" | "tsx" = "js"; + if (cleanId.endsWith(".tsx")) { + lang = "tsx"; + } else if ( + cleanId.endsWith(".ts") || + cleanId.endsWith(".mts") || + cleanId.endsWith(".cts") + ) { + lang = looksLikeJSX(code) ? "tsx" : "ts"; + } else if (cleanId.endsWith(".jsx")) { + lang = "jsx"; + } else if (looksLikeJSX(code)) { + lang = "jsx"; + } + + return { + isCommonJS, + lang, + sourceType: isCommonJS ? "commonjs" : "module", + }; +} + +function isNode(value: unknown): value is ESTree.Node { + return ( + value != null && + typeof value === "object" && + typeof Reflect.get(value, "type") === "string" && + typeof Reflect.get(value, "start") === "number" && + typeof Reflect.get(value, "end") === "number" + ); +} + +function walkNode( + node: ESTree.Node, + parent: ESTree.Node | null, + visit: (node: ESTree.Node, parent: ESTree.Node | null) => void +): void { + visit(node, parent); + + for (const value of Object.values(node)) { + if (Array.isArray(value)) { + for (const item of value) { + if (isNode(item)) { + walkNode(item, node, visit); + } + } + } else if (isNode(value)) { + walkNode(value, node, visit); + } + } +} + +function isFunctionLike(node: ESTree.Node): node is FunctionLike { + return ( + node.type === "FunctionDeclaration" || + node.type === "FunctionExpression" || + node.type === "ArrowFunctionExpression" + ); +} + +function getObjectPropertyKey(node: ESTree.Property): string | null { + if (node.key.type === "Identifier") { + return node.key.name; + } + + if (node.key.type === "Literal" && typeof node.key.value === "string") { + return node.key.value; + } + + return null; +} + +function getFunctionNodeName(node: FunctionLike): string | null { + if ( + (node.type === "FunctionDeclaration" || + node.type === "FunctionExpression") && + node.id != null + ) { + return node.id.name; + } + + return null; +} + +function getAssignmentTargetName( + node: ESTree.AssignmentExpression +): string | null { + if (node.left.type === "Identifier") { + return node.left.name; + } + + if (node.left.type !== "MemberExpression") { + return null; + } + + const property = node.left.property; + if (!node.left.computed && property.type === "Identifier") { + return property.name; + } + + if (property.type === "Literal" && typeof property.value === "string") { + return property.value; + } + + return null; +} + +function getFunctionNameFromParent( + node: ESTree.Node | null | undefined, + parentMap: Map, + filename: string | undefined +): string | null { + if (node == null) { + return null; + } + + if (node.type === "VariableDeclarator" && node.id.type === "Identifier") { + return node.id.name; + } + + if (node.type === "AssignmentExpression") { + return getAssignmentTargetName(node); + } + + if (node.type === "Property") { + return getObjectPropertyKey(node); + } + + if (node.type === "ExportDefaultDeclaration") { + return basename(filename) ?? null; + } + + if (node.type === "CallExpression") { + return getFunctionNameFromParent(parentMap.get(node), parentMap, filename); + } + + if (node.type === "ParenthesizedExpression") { + return getFunctionNameFromParent(parentMap.get(node), parentMap, filename); + } + + return null; +} + +function getFunctionName( + node: FunctionLike, + parentMap: Map, + filename: string | undefined +): string | null { + return ( + getFunctionNodeName(node) ?? + getFunctionNameFromParent(parentMap.get(node), parentMap, filename) + ); +} + +function isComponentName(name: string | null): boolean { + return name != null && /^[A-Z]/.test(name); +} + +function isCustomHookName(name: string | null): boolean { + return name != null && /^use[A-Z]/.test(name); +} + +function isHookCallbackFunction( + node: FunctionLike, + parentMap: Map +): boolean { + const parent = parentMap.get(node); + return ( + parent?.type === "CallExpression" && + parent.callee.type === "Identifier" && + isCustomHookName(parent.callee.name) + ); +} + +function findParentComponentOrHook( + node: ESTree.Node, + parentMap: Map, + functionInfoMap: Map +): FunctionInfo | null { + let current = parentMap.get(node); + + while (current != null) { + if (isFunctionLike(current)) { + const info = functionInfoMap.get(current); + if (info == null) { + return null; + } + + if (isComponentName(info.name) || isCustomHookName(info.name)) { + return info; + } + + if (isHookCallbackFunction(current, parentMap)) { + return null; + } + } + + current = parentMap.get(current); + } + + return null; +} + +function hasLeadingComment( + node: ESTree.Node, + comments: ESTree.Comment[], + code: string, + matcher: RegExp +): boolean { + return comments.some(comment => { + if (comment.end > node.start) { + return false; + } + + const between = code.slice(comment.end, node.start); + return /^\s*$/.test(between) && matcher.test(comment.value); + }); +} + +function isOptedIntoSignalTracking( + node: ESTree.Node | null | undefined, + comments: ESTree.Comment[], + code: string, + parentMap: Map +): boolean { + if (node == null) { + return false; + } + + switch (node.type) { + case "ArrowFunctionExpression": + case "FunctionExpression": + case "FunctionDeclaration": + case "ObjectExpression": + case "VariableDeclarator": + case "VariableDeclaration": + case "AssignmentExpression": + case "CallExpression": + case "ParenthesizedExpression": + return ( + hasLeadingComment(node, comments, code, optInCommentIdentifier) || + isOptedIntoSignalTracking( + parentMap.get(node), + comments, + code, + parentMap + ) + ); + + case "ExportDefaultDeclaration": + case "ExportNamedDeclaration": + case "Property": + case "ExpressionStatement": + return hasLeadingComment(node, comments, code, optInCommentIdentifier); + + default: + return false; + } +} + +function isOptedOutOfSignalTracking( + node: ESTree.Node | null | undefined, + comments: ESTree.Comment[], + code: string, + parentMap: Map +): boolean { + if (node == null) { + return false; + } + + switch (node.type) { + case "ArrowFunctionExpression": + case "FunctionExpression": + case "FunctionDeclaration": + case "ObjectExpression": + case "VariableDeclarator": + case "VariableDeclaration": + case "AssignmentExpression": + case "CallExpression": + case "ParenthesizedExpression": + return ( + hasLeadingComment(node, comments, code, optOutCommentIdentifier) || + isOptedOutOfSignalTracking( + parentMap.get(node), + comments, + code, + parentMap + ) + ); + + case "ExportDefaultDeclaration": + case "ExportNamedDeclaration": + case "Property": + case "ExpressionStatement": + return hasLeadingComment(node, comments, code, optOutCommentIdentifier); + + default: + return false; + } +} + +function shouldTransform( + info: FunctionInfo, + options: ReactSignalsTransformPluginOptions, + comments: ESTree.Comment[], + code: string, + parentMap: Map +): boolean { + const isComponentFunction = info.containsJSX && isComponentName(info.name); + + if (isOptedOutOfSignalTracking(info.node, comments, code, parentMap)) { + return false; + } + + if (isOptedIntoSignalTracking(info.node, comments, code, parentMap)) { + return true; + } + + if (options.mode === "all") { + return isComponentFunction; + } + + if (options.mode == null || options.mode === "auto") { + return ( + info.maybeUsesSignal && + (isComponentFunction || isCustomHookName(info.name)) + ); + } + + return false; +} + +function isValueMemberExpression(node: ESTree.MemberExpression): boolean { + if (!node.computed && node.property.type === "Identifier") { + return node.property.name === "value"; + } + + return node.property.type === "Literal" && node.property.value === "value"; +} + +function hasValuePropertyInPattern(node: ESTree.ObjectPattern): boolean { + return node.properties.some(property => { + if (property.type !== "Property") { + return false; + } + + return property.key.type === "Identifier" && property.key.name === "value"; + }); +} + +function isRequireCall( + node: ESTree.Node | null | undefined, + source: string +): boolean { + return ( + node?.type === "CallExpression" && + node.callee.type === "Identifier" && + node.callee.name === "require" && + node.arguments[0]?.type === "Literal" && + node.arguments[0].value === source + ); +} + +function collectJSXAlternativeImports(program: ESTree.Program) { + const identifiers = new Set(); + const objects = new Map(); + + for (const statement of program.body) { + if (statement.type === "ImportDeclaration") { + const jsxMethods = jsxPackages[statement.source.value]; + if (jsxMethods == null) { + continue; + } + + for (const specifier of statement.specifiers) { + if (specifier.type === "ImportSpecifier") { + const importedName = + specifier.imported.type === "Identifier" + ? specifier.imported.name + : specifier.imported.value; + + if (jsxMethods.includes(importedName)) { + identifiers.add(specifier.local.name); + } + } else if ( + specifier.type === "ImportDefaultSpecifier" || + specifier.type === "ImportNamespaceSpecifier" + ) { + objects.set(specifier.local.name, jsxMethods); + } + } + + continue; + } + + if (statement.type !== "VariableDeclaration") { + continue; + } + + for (const declarator of statement.declarations) { + if ( + !isRequireCall(declarator.init, "react") && + !isRequireCall(declarator.init, "react/jsx-runtime") && + !isRequireCall(declarator.init, "react/jsx-dev-runtime") + ) { + continue; + } + + const source = declarator.init.arguments[0].value; + const jsxMethods = jsxPackages[source]; + if (jsxMethods == null) { + continue; + } + + if (declarator.id.type === "Identifier") { + objects.set(declarator.id.name, jsxMethods); + } else if (declarator.id.type === "ObjectPattern") { + for (const property of declarator.id.properties) { + if (property.type !== "Property") { + continue; + } + + const importedName = getObjectPropertyKey(property); + if (!jsxMethods.includes(importedName ?? "")) { + continue; + } + + if (property.value.type === "Identifier") { + identifiers.add(property.value.name); + } + } + } + } + } + + return { identifiers, objects }; +} + +function isJSXAlternativeCall( + node: ESTree.CallExpression, + jsxIdentifiers: Set, + jsxObjects: Map +): boolean { + const callee = node.callee; + + if (callee.type === "Identifier") { + return jsxIdentifiers.has(callee.name); + } + + if ( + callee.type !== "MemberExpression" || + callee.object.type !== "Identifier" + ) { + return false; + } + + const allowedMethods = jsxObjects.get(callee.object.name); + if (allowedMethods == null) { + return false; + } + + if (!callee.computed && callee.property.type === "Identifier") { + return allowedMethods.includes(callee.property.name); + } + + return callee.property.type === "Literal" && + typeof callee.property.value === "string" + ? allowedMethods.includes(callee.property.value) + : false; +} + +function isSignalCall(node: ESTree.CallExpression): boolean { + return ( + node.callee.type === "Identifier" && signalCallNames.has(node.callee.name) + ); +} + +function getVariableNameFromDeclarator( + node: ESTree.Node, + parentMap: Map +): string | null { + let current = node; + + while (current != null) { + if ( + current.type === "VariableDeclarator" && + current.id.type === "Identifier" + ) { + return current.id.name; + } + + current = parentMap.get(current); + } + + return null; +} + +function hasNameInOptions(node: ESTree.CallExpression): boolean { + if (node.arguments.length < 2) { + return false; + } + + const optionsArgument = node.arguments[1]; + if (optionsArgument.type !== "ObjectExpression") { + return false; + } + + return optionsArgument.properties.some(property => { + if (property.type !== "Property") { + return false; + } + + if (property.key.type === "Identifier") { + return property.key.name === "name"; + } + + return property.key.type === "Literal" && property.key.value === "name"; + }); +} + +function createLineLookup(code: string) { + const lineStarts = [0]; + for (let index = 0; index < code.length; index++) { + if (code[index] === "\n") { + lineStarts.push(index + 1); + } + } + + return (offset: number): number => { + let low = 0; + let high = lineStarts.length - 1; + + while (low <= high) { + const middle = Math.floor((low + high) / 2); + const start = lineStarts[middle]; + const next = lineStarts[middle + 1] ?? Number.POSITIVE_INFINITY; + + if (offset < start) { + high = middle - 1; + } else if (offset >= next) { + low = middle + 1; + } else { + return middle + 1; + } + } + + return lineStarts.length; + }; +} + +function hasTrailingComma(code: string, end: number): boolean { + let index = end - 2; + while (index >= 0 && /\s/.test(code[index])) { + index--; + } + return code[index] === ","; +} + +function createSignalNameLiteral( + variableName: string, + filename: string | undefined, + lineOf: (offset: number) => number, + offset: number +): string { + if (filename == null) { + return JSON.stringify(variableName); + } + + const file = basename(filename); + if (file == null) { + return JSON.stringify(variableName); + } + + return JSON.stringify(`${variableName} (${file}:${lineOf(offset)})`); +} + +function injectSignalName( + s: Parameters>[1] | any, + code: string, + node: ESTree.CallExpression, + variableName: string, + filename: string | undefined, + lineOf: (offset: number) => number +): void { + const nameLiteral = createSignalNameLiteral( + variableName, + filename, + lineOf, + node.start + ); + const objectLiteral = `{\n name: ${nameLiteral}\n}`; + + if (node.arguments.length === 0) { + s.appendLeft(node.end - 1, `undefined, ${objectLiteral}`); + return; + } + + if (node.arguments.length === 1) { + s.appendLeft(node.end - 1, `, ${objectLiteral}`); + return; + } + + const optionsArgument = node.arguments[1]; + if (optionsArgument.type === "ObjectExpression") { + if (optionsArgument.properties.length === 0) { + s.appendLeft(optionsArgument.end - 1, `name: ${nameLiteral}`); + return; + } + + const separator = hasTrailingComma(code, optionsArgument.end) ? " " : ", "; + s.appendLeft(optionsArgument.end - 1, `${separator}name: ${nameLiteral}`); + return; + } + + s.update(optionsArgument.start, optionsArgument.end, objectLiteral); +} + +function extractBindingNames( + pattern: ESTree.ParamPattern | ESTree.BindingPattern, + names: string[] +) { + switch (pattern.type) { + case "Identifier": + names.push(pattern.name); + break; + + case "ArrayPattern": + for (const element of pattern.elements) { + if (element != null) { + extractBindingNames(element, names); + } + } + break; + + case "ObjectPattern": + for (const property of pattern.properties) { + if (property.type === "RestElement") { + extractBindingNames(property.argument, names); + } else { + extractBindingNames(property.value, names); + } + } + break; + + case "AssignmentPattern": + extractBindingNames(pattern.left, names); + break; + + case "RestElement": + extractBindingNames(pattern.argument, names); + break; + } +} + +function collectTopLevelBindings(program: ESTree.Program): Set { + const names = new Set(); + + for (const statement of program.body) { + if (statement.type === "ImportDeclaration") { + for (const specifier of statement.specifiers) { + names.add(specifier.local.name); + } + continue; + } + + if (statement.type === "VariableDeclaration") { + for (const declaration of statement.declarations) { + const declarationNames: string[] = []; + extractBindingNames(declaration.id, declarationNames); + for (const name of declarationNames) { + names.add(name); + } + } + continue; + } + + if ( + (statement.type === "FunctionDeclaration" || + statement.type === "ClassDeclaration") && + statement.id + ) { + names.add(statement.id.name); + } + } + + return names; +} + +function createUniqueIdentifier( + topLevelBindings: Set, + base: string +): string { + if (!topLevelBindings.has(base)) { + return base; + } + + let index = 2; + while (topLevelBindings.has(`${base}${index}`)) { + index++; + } + return `${base}${index}`; +} + +function findExistingHookBinding( + program: ESTree.Program, + importSource: string +): string | null { + for (const statement of program.body) { + if ( + statement.type === "ImportDeclaration" && + statement.source.value === importSource + ) { + for (const specifier of statement.specifiers) { + if (specifier.type !== "ImportSpecifier") { + continue; + } + + const importedName = + specifier.imported.type === "Identifier" + ? specifier.imported.name + : specifier.imported.value; + + if (importedName === "useSignals") { + return specifier.local.name; + } + } + } + + if (statement.type !== "VariableDeclaration") { + continue; + } + + for (const declaration of statement.declarations) { + if ( + declaration.id.type === "Identifier" && + declaration.init?.type === "MemberExpression" && + declaration.init.object.type === "CallExpression" && + isRequireCall(declaration.init.object, importSource) + ) { + const property = declaration.init.property; + if ( + !declaration.init.computed && + property.type === "Identifier" && + property.name === "useSignals" + ) { + return declaration.id.name; + } + } + + if ( + declaration.id.type !== "ObjectPattern" || + !isRequireCall(declaration.init, importSource) + ) { + continue; + } + + for (const property of declaration.id.properties) { + if (property.type !== "Property") { + continue; + } + + if (getObjectPropertyKey(property) !== "useSignals") { + continue; + } + + if (property.value.type === "Identifier") { + return property.value.name; + } + } + } + } + + return null; +} + +function addHookImport( + s: Parameters>[1] | any, + program: ESTree.Program, + importSource: string, + hookIdentifier: string, + isCommonJS: boolean +): void { + if (isCommonJS) { + s.prepend( + `var ${hookIdentifier} = require(${JSON.stringify(importSource)}).useSignals\n` + ); + return; + } + + const importLine = `import { useSignals as ${hookIdentifier} } from ${JSON.stringify(importSource)};\n`; + let lastImport: ESTree.ImportDeclaration | null = null; + + for (const statement of program.body) { + if (statement.type === "ImportDeclaration") { + lastImport = statement; + } + } + + if (lastImport != null) { + s.appendLeft(lastImport.end, `\n${importLine}`); + } else { + const leadingWhitespace = s.original.slice(0, program.start); + if (program.start > 0 && /^\s*$/.test(leadingWhitespace)) { + s.update(0, program.start, importLine); + } else { + s.prepend(importLine); + } + } +} + +function createUseSignalsCall( + hookIdentifier: string, + usage: string | null, + options: ReactSignalsTransformPluginOptions, + functionName: string | null +): string { + const args: string[] = []; + + if (usage != null) { + args.push(usage); + } else if (options.experimental?.debug && functionName) { + args.push("undefined"); + } + + if (options.experimental?.debug && functionName) { + args.push(JSON.stringify(functionName)); + } + + return `${hookIdentifier}(${args.join(", ")})`; +} + +function transformFunction( + s: Parameters>[1] | any, + info: FunctionInfo, + hookIdentifier: string, + options: ReactSignalsTransformPluginOptions +): void { + const isHook = isCustomHookName(info.name); + const isComponent = isComponentName(info.name); + const hookUsage = options.experimental?.noTryFinally + ? UNMANAGED + : isHook + ? MANAGED_HOOK + : isComponent + ? MANAGED_COMPONENT + : UNMANAGED; + + const body = info.node.body; + + if (hookUsage === UNMANAGED) { + const hookCall = createUseSignalsCall( + hookIdentifier, + null, + options, + info.name + ); + + if (body.type === "BlockStatement") { + s.appendLeft(body.start + 1, `\n${hookCall};`); + return; + } + + s.appendLeft(body.start, `{${hookCall};\nreturn `); + s.appendLeft(body.end, "\n}"); + return; + } + + const hookCall = createUseSignalsCall( + hookIdentifier, + hookUsage, + options, + info.name + ); + if (body.type === "BlockStatement") { + s.appendLeft( + body.start + 1, + `\nvar ${effectIdentifier} = ${hookCall};\ntry {` + ); + s.appendLeft(body.end - 1, `\n} finally {\n${effectIdentifier}.f();\n}`); + return; + } + + s.appendLeft( + body.start, + `{var ${effectIdentifier} = ${hookCall};\ntry {\nreturn ` + ); + s.appendLeft(body.end, `;\n} finally {\n${effectIdentifier}.f();\n}\n}`); +} + +export default function reactSignalsTransform( + options: ReactSignalsTransformPluginOptions = {} +): Plugin { + return { + name: "@preact/signals-react-transform-rolldown", + // @ts-expect-error Vite-specific property + enforce: "pre", + transform: { + filter: { + id: /\.[cm]?[jt]sx?(?:$|\?)/, + }, + handler: withMagicString(function (s, id) { + const parseOptions = getParseOptions(id, s.original); + const parsed = parseSync(id, s.original, { + lang: parseOptions.lang, + sourceType: parseOptions.sourceType, + }); + const program = parsed.program; + const comments = parsed.comments ?? []; + const parentMap = new Map(); + const functionInfoMap = new Map(); + const signalCallsToName: Array<{ + node: ESTree.CallExpression; + variableName: string; + }> = []; + const lineOf = createLineLookup(s.original); + + walkNode(program, null, (node, parent) => { + parentMap.set(node, parent); + + if (isFunctionLike(node)) { + functionInfoMap.set(node, { + node, + name: null, + containsJSX: false, + maybeUsesSignal: false, + }); + } + }); + + for (const info of functionInfoMap.values()) { + info.name = getFunctionName(info.node, parentMap, id); + } + + const jsxAlternatives = options.detectTransformedJSX + ? collectJSXAlternativeImports(program) + : null; + + walkNode(program, null, node => { + if (node.type === "CallExpression") { + if ( + jsxAlternatives != null && + isJSXAlternativeCall( + node, + jsxAlternatives.identifiers, + jsxAlternatives.objects + ) + ) { + const info = findParentComponentOrHook( + node, + parentMap, + functionInfoMap + ); + if (info != null) { + info.containsJSX = true; + } + } + + if ( + options.experimental?.debug && + isSignalCall(node) && + !hasNameInOptions(node) + ) { + const variableName = getVariableNameFromDeclarator( + node, + parentMap + ); + if (variableName != null) { + signalCallsToName.push({ node, variableName }); + } + } + } + + if ( + node.type === "MemberExpression" && + isValueMemberExpression(node) + ) { + const info = findParentComponentOrHook( + node, + parentMap, + functionInfoMap + ); + if (info != null) { + info.maybeUsesSignal = true; + } + } + + if ( + node.type === "ObjectPattern" && + hasValuePropertyInPattern(node) + ) { + const info = findParentComponentOrHook( + node, + parentMap, + functionInfoMap + ); + if (info != null) { + info.maybeUsesSignal = true; + } + } + + if (node.type === "JSXElement" || node.type === "JSXFragment") { + const info = findParentComponentOrHook( + node, + parentMap, + functionInfoMap + ); + if (info != null) { + info.containsJSX = true; + } + } + }); + + const functionsToTransform = Array.from( + functionInfoMap.values() + ).filter(info => + shouldTransform(info, options, comments, s.original, parentMap) + ); + + if ( + functionsToTransform.length === 0 && + signalCallsToName.length === 0 + ) { + return; + } + + for (const { node, variableName } of signalCallsToName) { + injectSignalName(s, s.original, node, variableName, id, lineOf); + } + + let hookIdentifier = findExistingHookBinding( + program, + options.importSource ?? defaultImportSource + ); + if (functionsToTransform.length > 0 && hookIdentifier == null) { + hookIdentifier = createUniqueIdentifier( + collectTopLevelBindings(program), + defaultHookIdentifier + ); + addHookImport( + s, + program, + options.importSource ?? defaultImportSource, + hookIdentifier, + parseOptions.isCommonJS + ); + } + + if (hookIdentifier == null) { + return; + } + + for (const info of functionsToTransform) { + transformFunction(s, info, hookIdentifier, options); + } + }), + }, + }; +} diff --git a/packages/react-transform-rolldown/src/types.ts b/packages/react-transform-rolldown/src/types.ts new file mode 100644 index 000000000..603fda635 --- /dev/null +++ b/packages/react-transform-rolldown/src/types.ts @@ -0,0 +1,9 @@ +export interface ReactSignalsTransformPluginOptions { + mode?: "auto" | "manual" | "all"; + importSource?: string; + detectTransformedJSX?: boolean; + experimental?: { + debug?: boolean; + noTryFinally?: boolean; + }; +} diff --git a/packages/react-transform-rolldown/test/node/helpers.ts b/packages/react-transform-rolldown/test/node/helpers.ts new file mode 100644 index 000000000..83a952e7f --- /dev/null +++ b/packages/react-transform-rolldown/test/node/helpers.ts @@ -0,0 +1,1407 @@ +/* oxlint-disable */ +// @ts-nocheck + +/** + * This file generates test cases for the transform. It generates a bunch of + * different components and then generates the source code for them. The + * generated source code is then used as the input for the transform. The test + * can then assert whether the transform should transform the code into the + * expected output or leave it untouched. + * + * Many of the language constructs generated here are to test the logic that + * finds the component name. For example, the transform should be able to find + * the component name even if the component is wrapped in a memo or forwardRef + * call. So we generate a bunch of components wrapped in those calls. + * + * We also generate constructs to test where users may place the comment to opt + * in or out of tracking signals. For example, the comment may be placed on the + * function declaration, the variable declaration, or the export statement. + * + * Some common abbreviations you may see in this file: + * - Comp: component + * - Exp: expression + * - Decl: declaration + * - Var: variable + * - Obj: object + * - Prop: property + */ + +// TODO: consider separating into a codeGenerators.ts file and a caseGenerators.ts file + +/** + * Interface representing the input and transformed output. A test may choose + * to use the transformed output or ignore it if the test is asserting the + * plugin does nothing + */ +interface InputOutput { + input: string; + transformed: string; +} + +export type CommentKind = "opt-in" | "opt-out" | undefined; +type VariableKind = "var" | "let" | "const"; +type ParamsConfig = 0 | 1 | 2 | 3 | undefined; + +type HookUsage = "" | "0" | "1" | "2"; +interface ComponentConfig { + name?: string | undefined; + body: string; + params?: ParamsConfig; + comment?: CommentKind; + usage?: HookUsage; +} + +interface FuncDeclComponent extends ComponentConfig { + type: "FuncDeclComp"; + name: string; +} + +interface FuncDeclHook { + type: "FuncDeclHook"; + name: string; + body: string; + comment?: CommentKind; + usage?: HookUsage; +} + +interface FuncExpComponent extends ComponentConfig { + type: "FuncExpComp"; +} + +interface FuncExpHook { + type: "FuncExpHook"; + name?: string; + body: string; + usage?: HookUsage; +} + +interface ArrowFuncComponent extends ComponentConfig { + type: "ArrowComp"; + return: "statement" | "expression"; + name?: undefined; +} + +interface ArrowFuncHook { + type: "ArrowFuncHook"; + return: "statement" | "expression"; + body: string; + usage?: HookUsage; +} + +interface ObjMethodComponent extends ComponentConfig { + type: "ObjectMethodComp"; + name: string; +} + +// TOOD: Add object method hook tests +// interface ObjMethodHook { +// type: "ObjectMethodHook"; +// name: string; +// body: string; +// } + +interface CallExp { + type: "CallExp"; + name: string; + args: Array; +} + +interface Variable { + type: "Variable"; + name: string; + body: InputOutput; + kind?: VariableKind; + comment?: CommentKind; + inlineComment?: CommentKind; +} + +interface Assignment { + type: "Assignment"; + name: string; + body: InputOutput; + kind?: VariableKind; + comment?: CommentKind; +} + +interface MemberExpAssign { + type: "MemberExpAssign"; + property: string; + body: InputOutput; + comment?: CommentKind; +} + +interface ObjectProperty { + type: "ObjectProperty"; + name: string; + body: InputOutput; + comment?: CommentKind; +} + +interface ExportDefault { + type: "ExportDefault"; + body: InputOutput; + comment?: CommentKind; +} + +interface ExportNamed { + type: "ExportNamed"; + body: InputOutput; + comment?: CommentKind; +} + +interface NodeTypes { + FuncDeclComp: FuncDeclComponent; + FuncDeclHook: FuncDeclHook; + FuncExpComp: FuncExpComponent; + FuncExpHook: FuncExpHook; + ArrowComp: ArrowFuncComponent; + ObjectMethodComp: ObjMethodComponent; + ArrowFuncHook: ArrowFuncHook; + CallExp: CallExp; + ExportDefault: ExportDefault; + ExportNamed: ExportNamed; + Variable: Variable; + Assignment: Assignment; + MemberExpAssign: MemberExpAssign; + ObjectProperty: ObjectProperty; +} + +type Node = NodeTypes[keyof NodeTypes]; +type ComponentNode = NodeTypes[ + | "FuncDeclComp" + | "FuncExpComp" + | "ArrowComp" + | "ObjectMethodComp"]; + +type HookNode = NodeTypes["FuncDeclHook" | "FuncExpHook" | "ArrowFuncHook"]; + +type Generators = { + [key in keyof NodeTypes]: (config: NodeTypes[key]) => InputOutput; +}; + +function transformComponent(config: ComponentNode): string { + const { type, body } = config; + const addReturn = type === "ArrowComp" && config.return === "expression"; + + if (config.usage === "" || config.usage === "0") { + return `_useSignals(${config.usage ?? ""}); + ${addReturn ? "return " : ""}${body}`; + } else { + return `var _effect = _useSignals(${config.usage ?? "1"}); + try { + ${addReturn ? "return " : ""}${body} + } finally { + _effect.f(); + }`; + } +} + +function transformHook(config: HookNode): string { + const { type, body } = config; + const addReturn = type === "ArrowFuncHook" && config.return === "expression"; + + if (config.usage === "" || config.usage === "0") { + return `_useSignals(${config.usage ?? ""}); + ${addReturn ? "return " : ""}${body}`; + } else { + return `var _effect = _useSignals(${config.usage ?? "2"}); + try { + ${addReturn ? "return " : ""}${body} + } finally { + _effect.f(); + }`; + } +} + +function generateParams(count?: ParamsConfig): string { + if (count == null || count === 0) return ""; + if (count === 1) return "props"; + if (count === 2) return "props, ref"; + return Array.from({ length: count }, (_, i) => `arg${i}`).join(", "); +} + +function generateComment(comment?: CommentKind): string { + if (comment === "opt-out") return "/* @noUseSignals */\n"; + if (comment === "opt-in") return "/* @useSignals */\n"; + return ""; +} + +const codeGenerators: Generators = { + FuncDeclComp(config) { + const params = generateParams(config.params); + const inputBody = config.body; + const outputBody = transformComponent(config); + let comment = generateComment(config.comment); + return { + input: `${comment}function ${config.name}(${params}) {\n${inputBody}\n}`, + transformed: `${comment}function ${config.name}(${params}) {\n${outputBody}\n}`, + }; + }, + FuncDeclHook(config) { + const inputBody = config.body; + const outputBody = transformHook(config); + let comment = generateComment(config.comment); + return { + input: `${comment}function ${config.name}() {\n${inputBody}\n}`, + transformed: `${comment}function ${config.name}() {\n${outputBody}\n}`, + }; + }, + FuncExpComp(config) { + const name = config.name ?? ""; + const params = generateParams(config.params); + const inputBody = config.body; + const outputBody = transformComponent(config); + return { + input: `(function ${name}(${params}) {\n${inputBody}\n})`, + transformed: `(function ${name}(${params}) {\n${outputBody}\n})`, + }; + }, + FuncExpHook(config) { + const name = config.name ?? ""; + const inputBody = config.body; + const outputBody = transformHook(config); + return { + input: `(function ${name}() {\n${inputBody}\n})`, + transformed: `(function ${name}() {\n${outputBody}\n})`, + }; + }, + ArrowComp(config) { + const params = generateParams(config.params); + const isExpBody = config.return === "expression"; + const inputBody = isExpBody ? config.body : `{\n${config.body}\n}`; + const outputBody = transformComponent(config); + return { + input: `(${params}) => ${inputBody}`, + transformed: `(${params}) => {\n${outputBody}\n}`, + }; + }, + ArrowFuncHook(config) { + const isExpBody = config.return === "expression"; + const inputBody = isExpBody ? config.body : `{\n${config.body}\n}`; + const outputBody = transformHook(config); + return { + input: `() => ${inputBody}`, + transformed: `() => {\n${outputBody}\n}`, + }; + }, + ObjectMethodComp(config) { + const params = generateParams(config.params); + const inputBody = config.body; + const outputBody = transformComponent(config); + const comment = generateComment(config.comment); + return { + input: `var o = {\n${comment}${config.name}(${params}) {\n${inputBody}\n}\n};`, + transformed: `var o = {\n${comment}${config.name}(${params}) {\n${outputBody}\n}\n};`, + }; + }, + CallExp(config) { + return { + input: `${config.name}(${config.args.map(arg => arg.input).join(", ")})`, + transformed: `${config.name}(${config.args.map(arg => arg.transformed).join(", ")})`, + }; + }, + Variable(config) { + const kind = config.kind ?? "const"; + const comment = generateComment(config.comment); + const inlineComment = generateComment(config.inlineComment)?.trim(); + return { + input: `${comment}${kind} ${config.name} = ${inlineComment}${config.body.input}`, + transformed: `${comment}${kind} ${config.name} = ${inlineComment}${config.body.transformed}`, + }; + }, + Assignment(config) { + const kind = config.kind ?? "let"; + const comment = generateComment(config.comment); + return { + input: `${kind} ${config.name};\n ${comment}${config.name} = ${config.body.input}`, + transformed: `${kind} ${config.name};\n ${comment}${config.name} = ${config.body.transformed}`, + }; + }, + MemberExpAssign(config) { + const comment = generateComment(config.comment); + const isComputed = config.property.startsWith("["); + const property = isComputed ? config.property : `.${config.property}`; + return { + input: `${comment}obj.prop1${property} = ${config.body.input}`, + transformed: `${comment}obj.prop1${property} = ${config.body.transformed}`, + }; + }, + ObjectProperty(config) { + const comment = generateComment(config.comment); + return { + input: `var o = {\n ${comment}${config.name}: ${config.body.input} \n}`, + transformed: `var o = {\n ${comment}${config.name}: ${config.body.transformed} \n}`, + }; + }, + ExportDefault(config) { + const comment = generateComment(config.comment); + return { + input: `${comment}export default ${config.body.input}`, + transformed: `${comment}export default ${config.body.transformed}`, + }; + }, + ExportNamed(config) { + const comment = generateComment(config.comment); + return { + input: `${comment}export ${config.body.input}`, + transformed: `${comment}export ${config.body.transformed}`, + }; + }, +}; + +function generateCode(config: Node): InputOutput { + return codeGenerators[config.type](config as any); +} + +export interface GeneratedCode extends InputOutput { + name: string; +} + +interface CodeConfig { + /** Whether to output source code that auto should transform */ + auto: boolean; + /** What kind of opt-in or opt-out to include if any */ + comment?: CommentKind; + /** Name of the generated code (useful for test case titles) */ + name?: string; + /** Number of parameters the component function should have */ + params?: ParamsConfig; +} + +interface ComponentCodeConfig extends CodeConfig { + properInlineName?: boolean; + usage?: HookUsage; +} + +interface HookCodeConfig extends CodeConfig { + usage?: HookUsage; +} + +interface VariableCodeConfig extends CodeConfig { + inlineComment?: CommentKind; +} + +const codeTitle = (...parts: Array) => + parts.filter(Boolean).join(" "); + +function expressionComponents(config: ComponentCodeConfig): GeneratedCode[] { + const { name: baseName, params, usage } = config; + + let components: GeneratedCode[]; + if (config.auto) { + components = [ + { + name: codeTitle(baseName, "as function without inline name"), + ...generateCode({ + type: "FuncExpComp", + body: "return
{signal.value}
", + params, + usage, + }), + }, + { + name: codeTitle(baseName, "as arrow function with statement body"), + ...generateCode({ + type: "ArrowComp", + return: "statement", + body: "return
{signal.value}
", + params, + usage, + }), + }, + { + name: codeTitle(baseName, "as arrow function with expression body"), + ...generateCode({ + type: "ArrowComp", + return: "expression", + body: "
{signal.value}
", + params, + usage, + }), + }, + ]; + } else { + components = [ + { + name: codeTitle(baseName, "as function with no JSX"), + ...generateCode({ + type: "FuncExpComp", + body: "return signal.value", + params, + usage, + }), + }, + { + name: codeTitle(baseName, "as function with no signals"), + ...generateCode({ + type: "FuncExpComp", + body: "return
Hello World
", + params, + usage, + }), + }, + { + name: codeTitle(baseName, "as arrow function with no JSX"), + ...generateCode({ + type: "ArrowComp", + return: "expression", + body: "signal.value", + params, + usage, + }), + }, + { + name: codeTitle(baseName, "as arrow function with no signals"), + ...generateCode({ + type: "ArrowComp", + return: "expression", + body: "
Hello World
", + params, + usage, + }), + }, + ]; + } + + if ( + (config.properInlineName != null && !config.properInlineName) || + !config.auto + ) { + components.push({ + name: codeTitle(baseName, "as function with bad inline name"), + ...generateCode({ + type: "FuncExpComp", + name: "app", + body: "return
{signal.value}
", + params, + usage: "", + }), + }); + } else { + components.push({ + name: codeTitle(baseName, "as function with proper inline name"), + ...generateCode({ + type: "FuncExpComp", + name: "App", + body: "return
{signal.value}
", + params, + usage, + }), + }); + } + + return components; +} + +function withCallExpWrappers(config: ComponentCodeConfig): GeneratedCode[] { + const codeCases: GeneratedCode[] = []; + + // Simulate a component wrapped memo + const memoedComponents = expressionComponents({ ...config, params: 1 }); + for (let component of memoedComponents) { + codeCases.push({ + name: component.name + " wrapped in memo", + ...generateCode({ + type: "CallExp", + name: "memo", + args: [component], + }), + }); + } + + // Simulate a component wrapped in forwardRef + const forwardRefComponents = expressionComponents({ ...config, params: 2 }); + for (let component of forwardRefComponents) { + codeCases.push({ + name: component.name + " wrapped in forwardRef", + ...generateCode({ + type: "CallExp", + name: "forwardRef", + args: [component], + }), + }); + } + + // Simulate components wrapped in both memo and forwardRef + for (let component of forwardRefComponents) { + codeCases.push({ + name: component.name + " wrapped in memo and forwardRef", + ...generateCode({ + type: "CallExp", + name: "memo", + args: [ + generateCode({ + type: "CallExp", + name: "forwardRef", + args: [component], + }), + ], + }), + }); + } + + return codeCases; +} + +export function declarationComp(config: CodeConfig): GeneratedCode[] { + const { name: baseName, params, comment } = config; + if (config.auto) { + return [ + { + name: codeTitle(baseName, "with proper name, jsx, and signal usage"), + ...generateCode({ + type: "FuncDeclComp", + name: "App", + body: "return <>{signal.value}", + params, + comment, + }), + }, + ]; + } else { + return [ + { + name: codeTitle(baseName, "with bad name"), + ...generateCode({ + type: "FuncDeclComp", + name: "app", + body: "return
{signal.value}
", + params, + comment, + usage: "", + }), + }, + { + name: codeTitle(baseName, "with no JSX"), + ...generateCode({ + type: "FuncDeclComp", + name: "App", + body: "return signal.value", + params, + comment, + }), + }, + { + name: codeTitle(baseName, "with no signals"), + ...generateCode({ + type: "FuncDeclComp", + name: "App", + body: "return
Hello World
", + params, + comment, + }), + }, + ]; + } +} + +export function objMethodComp(config: CodeConfig): GeneratedCode[] { + const { name: baseName, params, comment } = config; + if (config.auto) { + return [ + { + name: codeTitle(baseName, "with proper name, jsx, and signal usage"), + ...generateCode({ + type: "ObjectMethodComp", + name: "App", + body: "return <>{signal.value}", + params, + comment, + }), + }, + { + name: codeTitle( + baseName, + "with computed literal name, jsx, and signal usage" + ), + ...generateCode({ + type: "ObjectMethodComp", + name: "['App']", + body: "return <>{signal.value}", + params, + comment, + }), + }, + ]; + } else { + return [ + { + name: codeTitle(baseName, "with bad name"), + ...generateCode({ + type: "ObjectMethodComp", + name: "app", + body: "return
{signal.value}
", + params, + comment, + usage: "", + }), + }, + { + name: codeTitle(baseName, "with dynamic name"), + ...generateCode({ + type: "ObjectMethodComp", + name: "['App' + '1']", + body: "return
{signal.value}
", + params, + comment, + usage: "", + }), + }, + { + name: codeTitle(baseName, "with no JSX"), + ...generateCode({ + type: "ObjectMethodComp", + name: "App", + body: "return signal.value", + params, + comment, + }), + }, + { + name: codeTitle(baseName, "with no signals"), + ...generateCode({ + type: "ObjectMethodComp", + name: "App", + body: "return
Hello World
", + params, + comment, + }), + }, + ]; + } +} + +export function variableComp(config: VariableCodeConfig): GeneratedCode[] { + const { name: baseName, comment, inlineComment } = config; + const codeCases: GeneratedCode[] = []; + + const components = expressionComponents(config); + for (const c of components) { + codeCases.push({ + name: codeTitle(c.name), + ...generateCode({ + type: "Variable", + name: "VarComp", + body: c, + comment, + inlineComment, + }), + }); + } + + if (!config.auto) { + codeCases.push({ + name: codeTitle(baseName, `as function with bad variable name`), + ...generateCode({ + type: "Variable", + name: "render", + comment, + inlineComment, + body: generateCode({ + type: "FuncExpComp", + body: "return
{signal.value}
", + usage: "", + }), + }), + }); + + codeCases.push({ + name: codeTitle(baseName, `as arrow function with bad variable name`), + ...generateCode({ + type: "Variable", + name: "render", + comment, + inlineComment, + body: generateCode({ + type: "ArrowComp", + return: "expression", + body: "
{signal.value}
", + usage: "", + }), + }), + }); + } + + // With HoC wrappers, we are testing the logic to find the component name. So + // only generate tests where the function body is correct ("auto" is true) and + // the name is either correct or bad. + const hocComponents = withCallExpWrappers({ + ...config, + auto: true, + usage: config.auto ? "1" : "", + properInlineName: config.auto, + }); + const suffix = config.auto ? "" : "with bad variable name"; + for (const c of hocComponents) { + codeCases.push({ + name: codeTitle(c.name, suffix), + ...generateCode({ + type: "Variable", + name: config.auto ? "VarComp" : "render", + body: c, + comment, + inlineComment, + }), + }); + } + + return codeCases; +} + +export function assignmentComp(config: CodeConfig): GeneratedCode[] { + const { name: baseName, comment } = config; + const codeCases: GeneratedCode[] = []; + + const components = expressionComponents(config); + for (const c of components) { + codeCases.push({ + name: codeTitle(c.name), + ...generateCode({ + type: "Assignment", + name: "AssignComp", + body: c, + comment, + }), + }); + } + + if (!config.auto) { + codeCases.push({ + name: codeTitle(baseName, "function component with bad variable name"), + ...generateCode({ + type: "Assignment", + name: "render", + comment, + body: generateCode({ + type: "FuncExpComp", + body: "return
{signal.value}
", + usage: "", + }), + }), + }); + + codeCases.push({ + name: codeTitle(baseName, "arrow function with bad variable name"), + ...generateCode({ + type: "Assignment", + name: "render", + comment, + body: generateCode({ + type: "ArrowComp", + return: "expression", + body: "
{signal.value}
", + usage: "", + }), + }), + }); + } + + // With HoC wrappers, we are testing the logic to find the component name. So + // only generate tests where the function body is correct ("auto" is true) and + // the name is either correct or bad. + const hocComponents = withCallExpWrappers({ + ...config, + auto: true, + usage: config.auto ? "1" : "", + properInlineName: config.auto, + }); + const suffix = config.auto ? "" : "with bad variable name"; + for (const c of hocComponents) { + codeCases.push({ + name: codeTitle(c.name, suffix), + ...generateCode({ + type: "Assignment", + name: config.auto ? "AssignComp" : "render", + body: c, + comment, + }), + }); + } + + return codeCases; +} + +export function objAssignComp(config: CodeConfig): GeneratedCode[] { + const { name: baseName, comment } = config; + const codeCases: GeneratedCode[] = []; + + const components = expressionComponents(config); + for (const c of components) { + codeCases.push({ + name: codeTitle(c.name), + ...generateCode({ + type: "MemberExpAssign", + property: "Comp", + body: c, + comment, + }), + }); + } + + if (!config.auto) { + codeCases.push({ + name: codeTitle(baseName, "function component with bad property name"), + ...generateCode({ + type: "MemberExpAssign", + property: "render", + comment, + body: generateCode({ + type: "FuncExpComp", + body: "return
{signal.value}
", + usage: "", + }), + }), + }); + + codeCases.push({ + name: codeTitle(baseName, "arrow function with bad property name"), + ...generateCode({ + type: "MemberExpAssign", + property: "render", + comment, + body: generateCode({ + type: "ArrowComp", + return: "expression", + body: "
{signal.value}
", + usage: "", + }), + }), + }); + + codeCases.push({ + name: codeTitle( + baseName, + "function component with bad computed property name" + ), + ...generateCode({ + type: "MemberExpAssign", + property: "['render']", + body: generateCode({ + type: "FuncExpComp", + body: "return
{signal.value}
", + usage: "", + }), + comment, + }), + }); + + codeCases.push({ + name: codeTitle( + baseName, + "function component with dynamic computed property name" + ), + ...generateCode({ + type: "MemberExpAssign", + property: "['Comp' + '1']", + body: generateCode({ + type: "FuncExpComp", + body: "return
{signal.value}
", + usage: "", + }), + comment, + }), + }); + } else { + codeCases.push({ + name: codeTitle( + baseName, + "function component with computed property name" + ), + ...generateCode({ + type: "MemberExpAssign", + property: "['Comp']", + body: generateCode({ + type: "FuncExpComp", + body: "return
{signal.value}
", + }), + comment, + }), + }); + } + + // With HoC wrappers, we are testing the logic to find the component name. So + // only generate tests where the function body is correct ("auto" is true) and + // the name is either correct or bad. + const hocComponents = withCallExpWrappers({ + ...config, + auto: true, + usage: config.auto ? "1" : "", + properInlineName: config.auto, + }); + const suffix = config.auto ? "" : "with bad variable name"; + for (const c of hocComponents) { + codeCases.push({ + name: codeTitle(c.name, suffix), + ...generateCode({ + type: "MemberExpAssign", + property: config.auto ? "Comp" : "render", + body: c, + comment, + }), + }); + } + + return codeCases; +} + +export function objectPropertyComp(config: CodeConfig): GeneratedCode[] { + const { name: baseName, comment } = config; + const codeCases: GeneratedCode[] = []; + + const components = expressionComponents(config); + for (const c of components) { + codeCases.push({ + name: c.name, + ...generateCode({ + type: "ObjectProperty", + name: "ObjComp", + body: c, + comment, + }), + }); + } + + if (!config.auto) { + codeCases.push({ + name: codeTitle(baseName, "function component with bad property name"), + ...generateCode({ + type: "ObjectProperty", + name: "render_prop", + comment, + body: generateCode({ + type: "FuncExpComp", + body: "return
{signal.value}
", + usage: "", + }), + }), + }); + + codeCases.push({ + name: codeTitle(baseName, "arrow function with bad property name"), + ...generateCode({ + type: "ObjectProperty", + name: "render_prop", + comment, + body: generateCode({ + type: "ArrowComp", + return: "expression", + body: "
{signal.value}
", + usage: "", + }), + }), + }); + + codeCases.push({ + name: codeTitle( + baseName, + "function component with bad computed property name" + ), + ...generateCode({ + type: "ObjectProperty", + name: "['render']", + comment, + body: generateCode({ + type: "FuncExpComp", + body: "return
{signal.value}
", + usage: "", + }), + }), + }); + + codeCases.push({ + name: codeTitle( + baseName, + "function component with dynamic computed property name" + ), + ...generateCode({ + type: "ObjectProperty", + name: "['Comp' + '1']", + comment, + body: generateCode({ + type: "FuncExpComp", + body: "return
{signal.value}
", + usage: "", + }), + }), + }); + } else { + codeCases.push({ + name: codeTitle( + baseName, + "function component with computed property name" + ), + ...generateCode({ + type: "ObjectProperty", + name: "['Comp']", + comment, + body: generateCode({ + type: "FuncExpComp", + body: "return
{signal.value}
", + }), + }), + }); + } + + // With HoC wrappers, we are testing the logic to find the component name. So + // only generate tests where the function body is correct ("auto" is true) and + // the name is either correct or bad. + const hocComponents = withCallExpWrappers({ + ...config, + auto: true, + usage: config.auto ? "1" : "", + properInlineName: config.auto, + }); + const suffix = config.auto ? "" : "with bad property name"; + for (const c of hocComponents) { + codeCases.push({ + name: codeTitle(c.name, suffix), + ...generateCode({ + type: "ObjectProperty", + name: config.auto ? "ObjComp" : "render_prop", + body: c, + comment, + }), + }); + } + + return codeCases; +} + +export function exportDefaultComp(config: CodeConfig): GeneratedCode[] { + const { comment } = config; + const codeCases: GeneratedCode[] = []; + + const usage = config.auto ? "1" : ""; + const components = [ + ...declarationComp({ ...config, comment: undefined }), + ...expressionComponents({ ...config, usage }), + ...withCallExpWrappers({ ...config, usage }), + ]; + + for (const c of components) { + codeCases.push({ + name: c.name + " exported as default", + ...generateCode({ + type: "ExportDefault", + body: c, + comment, + }), + }); + } + + return codeCases; +} + +export function exportNamedComp(config: CodeConfig): GeneratedCode[] { + const { comment } = config; + const codeCases: GeneratedCode[] = []; + + // `declarationComp` will put the comment on the function declaration, but in + // this case we want to put it on the export statement. + const funcComponents = declarationComp({ ...config, comment: undefined }); + for (const c of funcComponents) { + codeCases.push({ + name: `function declaration ${c.name}`, + ...generateCode({ + type: "ExportNamed", + body: c, + comment, + }), + }); + } + + // `variableComp` will put the comment on the function declaration, but in + // this case we want to put it on the export statement. + const varComponents = variableComp({ ...config, comment: undefined }); + for (const c of varComponents) { + const name = c.name.replace(" variable ", " exported "); + codeCases.push({ + name: `variable ${name}`, + ...generateCode({ + type: "ExportNamed", + body: c, + comment, + }), + }); + } + + return codeCases; +} + +function expressionHooks(config: HookCodeConfig): GeneratedCode[] { + const { name, usage } = config; + if (config.auto) { + return [ + { + name: codeTitle(name, "as function without inline name"), + ...generateCode({ + type: "FuncExpHook", + body: "return signal.value", + usage, + }), + }, + { + name: codeTitle(name, "as function with proper inline name"), + ...generateCode({ + type: "FuncExpHook", + name: "useCustomHook", + body: "return signal.value", + usage: "2", + }), + }, + { + name: codeTitle(name, "as arrow function with with statement body"), + ...generateCode({ + type: "ArrowFuncHook", + return: "statement", + body: "return signal.value", + usage, + }), + }, + { + name: codeTitle(name, "as arrow function with with expression body"), + ...generateCode({ + type: "ArrowFuncHook", + return: "expression", + body: "signal.value", + usage, + }), + }, + ]; + } else { + return [ + { + name: codeTitle(name, "as function with bad inline name"), + ...generateCode({ + type: "FuncExpHook", + name: "usecustomHook", + body: "return signal.value", + usage: "", + }), + }, + { + name: codeTitle(name, "as function with no signals"), + ...generateCode({ + type: "FuncExpHook", + body: "return useState(0)", + usage, + }), + }, + { + name: codeTitle(name, "as arrow function with no signals"), + ...generateCode({ + type: "ArrowFuncHook", + return: "expression", + body: "useState(0)", + usage, + }), + }, + ]; + } +} + +export function declarationHooks(config: HookCodeConfig): GeneratedCode[] { + const { name, comment, usage } = config; + if (config.auto) { + return [ + { + name: codeTitle(name, "with proper name and signal usage"), + ...generateCode({ + type: "FuncDeclHook", + name: "useCustomHook", + comment, + body: "return signal.value", + usage: "2", + }), + }, + ]; + } else { + return [ + { + name: codeTitle(name, "with bad name"), + ...generateCode({ + type: "FuncDeclHook", + name: "usecustomHook", + comment, + body: "return signal.value", + usage: "", + }), + }, + { + name: codeTitle(name, "with no signals"), + ...generateCode({ + type: "FuncDeclHook", + name: "useCustomHook", + comment, + body: "return useState(0)", + usage, + }), + }, + ]; + } +} + +export function variableHooks(config: VariableCodeConfig): GeneratedCode[] { + const { name, comment, inlineComment } = config; + const codeCases: GeneratedCode[] = []; + + const hooks = expressionHooks(config); + for (const h of hooks) { + codeCases.push({ + name: codeTitle(h.name), + ...generateCode({ + type: "Variable", + name: "useCustomHook", + comment, + inlineComment, + body: h, + }), + }); + } + + if (!config.auto) { + codeCases.push({ + name: codeTitle(name, "as function with bad variable name"), + ...generateCode({ + type: "Variable", + name: "usecustomHook", + comment, + inlineComment, + body: generateCode({ + type: "FuncExpHook", + body: "return signal.value", + usage: "", + }), + }), + }); + + codeCases.push({ + name: codeTitle(name, "as arrow function with bad variable name"), + ...generateCode({ + type: "Variable", + name: "usecustomHook", + comment, + inlineComment, + body: generateCode({ + type: "ArrowFuncHook", + return: "expression", + body: "signal.value", + usage: "", + }), + }), + }); + } + + return codeCases; +} + +export function exportDefaultHooks(config: CodeConfig): GeneratedCode[] { + const { comment } = config; + const codeCases: GeneratedCode[] = []; + + const usage = config.auto ? "2" : ""; + const components = [ + ...declarationHooks({ ...config, comment: undefined }), + ...expressionHooks({ ...config, usage }), + ]; + + for (const c of components) { + codeCases.push({ + name: c.name + " exported as default", + ...generateCode({ + type: "ExportDefault", + body: c, + comment, + }), + }); + } + + return codeCases; +} + +export function exportNamedHooks(config: CodeConfig): GeneratedCode[] { + const { comment } = config; + const codeCases: GeneratedCode[] = []; + + // `declarationHooks` will put the comment on the function declaration, but in + // this case we want to put it on the export statement. + const funcHooks = declarationHooks({ ...config, comment: undefined }); + for (const c of funcHooks) { + codeCases.push({ + name: `function declaration ${c.name}`, + ...generateCode({ + type: "ExportNamed", + body: c, + comment, + }), + }); + } + + // `variableHooks` will put the comment on the function declaration, but in + // this case we want to put it on the export statement. + const varHooks = variableHooks({ ...config, comment: undefined }); + for (const c of varHooks) { + const name = c.name.replace(" variable ", " exported "); + codeCases.push({ + name: `variable ${name}`, + ...generateCode({ + type: "ExportNamed", + body: c, + comment, + }), + }); + } + + return codeCases; +} + +// Command to use to debug the generated code +// ../../../../node_modules/.bin/tsc --target es2020 --module es2020 --moduleResolution node --esModuleInterop --outDir . helpers.ts; mv helpers.js helpers.mjs; node helpers.mjs +/* eslint-disable no-console */ +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars +async function debug() { + // @ts-ignore + const prettier = await import("prettier"); + const format = (code: string) => prettier.format(code, { parser: "babel" }); + console.log("generating..."); + console.time("generated"); + const codeCases: GeneratedCode[] = [ + // ...declarationComponents({ name: "transforms a", auto: true }), + // ...declarationComponents({ name: "does not transform a", auto: false }), + // + // ...expressionComponents({ name: "transforms a", auto: true }), + // ...expressionComponents({ name: "does not transform a", auto: false }), + // + // ...withCallExpWrappers({ name: "transforms a", auto: true }), + // ...withCallExpWrappers({ name: "does not transform a", auto: false }), + // + ...variableComp({ name: "transforms a", auto: true }), + ...variableComp({ name: "does not transform a", auto: false }), + + ...assignmentComp({ name: "transforms a", auto: true }), + ...assignmentComp({ name: "does not transform a", auto: false }), + + ...objectPropertyComp({ name: "transforms a", auto: true }), + ...objectPropertyComp({ name: "does not transform a", auto: false }), + + ...exportDefaultComp({ name: "transforms a", auto: true }), + ...exportDefaultComp({ name: "does not transform a", auto: false }), + + ...exportNamedComp({ name: "transforms a", auto: true }), + ...exportNamedComp({ name: "does not transform a", auto: false }), + ]; + console.timeEnd("generated"); + + for (const code of codeCases) { + console.log("=".repeat(80)); + console.log(code.name); + console.log("input:"); + console.log(await format(code.input)); + console.log("transformed:"); + console.log(await format(code.transformed)); + console.log(); + } +} + +// debug(); diff --git a/packages/react-transform-rolldown/test/node/runtime.test.ts b/packages/react-transform-rolldown/test/node/runtime.test.ts new file mode 100644 index 000000000..3c1e9c598 --- /dev/null +++ b/packages/react-transform-rolldown/test/node/runtime.test.ts @@ -0,0 +1,581 @@ +/* oxlint-disable */ +// @ts-nocheck +// @vitest-environment jsdom + +import * as signalsCore from "@preact/signals-core"; +import { batch, signal } from "@preact/signals-core"; +import * as signalsRuntime from "@preact/signals-react/runtime"; +import * as React from "react"; +import * as jsxDevRuntime from "react/jsx-dev-runtime"; +import * as jsxRuntime from "react/jsx-runtime"; +import { createRequire } from "node:module"; +import { runInNewContext } from "node:vm"; +import { createRoot } from "react-dom/client"; +import { act } from "react-dom/test-utils"; +import { rolldown } from "rolldown"; +import { transform as rolldownTransform } from "rolldown/utils"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import reactSignalsTransform, { + type ReactSignalsTransformPluginOptions, +} from "../../src/index.ts"; + +const customSource = "useSignals-custom-source"; +const nodeRequire = createRequire(import.meta.url); +const disposeSymbol = Symbol.for("Symbol.dispose"); +const disposableSignalsRuntime = { + ...signalsRuntime, + useSignals(...args: unknown[]) { + const value = signalsRuntime.useSignals(...args); + if ( + value != null && + typeof value === "object" && + !(disposeSymbol in value) && + typeof value.f === "function" + ) { + return { + ...value, + [Symbol.dispose]() { + value.f(); + }, + [disposeSymbol]() { + value.f(); + }, + }; + } + + return value; + }, +}; +const modules: Record = { + "@preact/signals-core": signalsCore, + "@preact/signals-react/runtime": disposableSignalsRuntime, + react: React, + "react/jsx-dev-runtime": jsxDevRuntime, + "react/jsx-runtime": jsxRuntime, + [customSource]: disposableSignalsRuntime, +}; + +function testRequire(name: string): unknown { + if (name in modules) { + return modules[name]; + } + + return nodeRequire(name); +} + +async function createComponent( + code: string, + options: ReactSignalsTransformPluginOptions = {}, + filename = "virtual:entry.tsx" +): Promise> { + let sourceCode = code; + if (/\busing\s+/.test(sourceCode)) { + const transformed = await rolldownTransform(filename, sourceCode, { + jsx: "preserve", + lang: filename.endsWith(".tsx") + ? "tsx" + : filename.endsWith(".ts") + ? "ts" + : "jsx", + sourceType: "module", + target: "es2022", + }); + sourceCode = transformed.code; + } + + const build = await rolldown({ + input: filename, + plugins: [ + { + name: "virtual", + resolveId(id) { + if (id === filename) return id; + return { id, external: true }; + }, + load(id) { + if (id === filename) return sourceCode; + }, + }, + reactSignalsTransform(options), + ], + }); + + const { output } = await build.generate({ format: "cjs" }); + await build.close(); + + const generatedCode = output[0].code; + + const exports: Record = {}; + const module = { exports }; + runInNewContext(generatedCode, { + clearTimeout, + console, + exports, + globalThis, + module, + process, + require: testRequire, + setTimeout, + }); + return module.exports; +} + +describe("react signals transform runtime", () => { + let scratch: HTMLDivElement; + let root: ReturnType; + + async function render(element: React.ReactElement) { + await act(async () => { + root.render(element); + }); + } + + beforeEach(() => { + globalThis.IS_REACT_ACT_ENVIRONMENT = true; + scratch = document.createElement("div"); + document.body.appendChild(scratch); + root = createRoot(scratch); + }); + + afterEach(async () => { + await act(async () => { + root.unmount(); + }); + scratch.remove(); + }); + + it("should rerender components when using signals as text", async () => { + const { App } = await createComponent(` + export function App({ name }) { + return
Hello {name}
; + } + `); + + const name = signal("John"); + await render(React.createElement(App as any, { name })); + expect(scratch.innerHTML).toBe("
Hello John
"); + + await act(async () => { + name.value = "Jane"; + }); + expect(scratch.innerHTML).toBe("
Hello Jane
"); + }); + + it("should rerender components when signals they use change", async () => { + const { App } = await createComponent(` + export function App({ name }) { + return
Hello {name.value}
; + } + `); + + const name = signal("John"); + await render(React.createElement(App as any, { name })); + expect(scratch.innerHTML).toBe("
Hello John
"); + + await act(async () => { + name.value = "Jane"; + }); + expect(scratch.innerHTML).toBe("
Hello Jane
"); + }); + + it("should rerender components with custom hooks that use signals", async () => { + const { App, name } = await createComponent(` + import { signal } from '@preact/signals-core'; + + export const name = signal('John'); + function useName() { + return name.value; + } + + export function App() { + const name = useName(); + return
Hello {name}
; + } + `); + + await render(React.createElement(App as any)); + expect(scratch.innerHTML).toBe("
Hello John
"); + + await act(async () => { + name.value = "Jane"; + }); + expect(scratch.innerHTML).toBe("
Hello Jane
"); + }); + + it("should rerender components with multiple custom hooks that use signals", async () => { + const { App, name, greeting } = await createComponent(` + import { signal } from '@preact/signals-core'; + + export const greeting = signal('Hello'); + function useGreeting() { + return greeting.value; + } + + export const name = signal('John'); + function useName() { + return name.value; + } + + export function App() { + const greeting = useGreeting(); + const name = useName(); + return
{greeting} {name}
; + } + `); + + await render(React.createElement(App as any)); + expect(scratch.innerHTML).toBe("
Hello John
"); + + await act(async () => { + greeting.value = "Hi"; + }); + expect(scratch.innerHTML).toBe("
Hi John
"); + + await act(async () => { + name.value = "Jane"; + }); + expect(scratch.innerHTML).toBe("
Hi Jane
"); + + await act(async () => { + batch(() => { + greeting.value = "Hello"; + name.value = "John"; + }); + }); + expect(scratch.innerHTML).toBe("
Hello John
"); + }); + + it("should rerender components that use signals with multiple custom hooks that use signals", async () => { + const { App, name, greeting, punctuation } = await createComponent(` + import { signal } from '@preact/signals-core'; + + export const greeting = signal('Hello'); + function useGreeting() { + return greeting.value; + } + + export const name = signal('John'); + function useName() { + return name.value; + } + + export const punctuation = signal('!'); + export function App() { + const greeting = useGreeting(); + const name = useName(); + return
{greeting} {name}{punctuation.value}
; + } + `); + + await render(React.createElement(App as any)); + expect(scratch.innerHTML).toBe("
Hello John!
"); + + await act(async () => { + greeting.value = "Hi"; + }); + expect(scratch.innerHTML).toBe("
Hi John!
"); + + await act(async () => { + name.value = "Jane"; + }); + expect(scratch.innerHTML).toBe("
Hi Jane!
"); + + await act(async () => { + punctuation.value = "?"; + }); + expect(scratch.innerHTML).toBe("
Hi Jane?
"); + + await act(async () => { + batch(() => { + greeting.value = "Hello"; + name.value = "John"; + punctuation.value = "!"; + }); + }); + expect(scratch.innerHTML).toBe("
Hello John!
"); + }); + + it("should rerender components wrapped in memo", async () => { + const { MemoApp, name } = await createComponent(` + import { signal } from '@preact/signals-core'; + import { memo } from 'react'; + + export const name = signal('John'); + + function App({ name }) { + return
Hello {name.value}
; + } + + export const MemoApp = memo(App); + `); + + await render(React.createElement(MemoApp as any, { name })); + expect(scratch.innerHTML).toBe("
Hello John
"); + + await act(async () => { + name.value = "Jane"; + }); + expect(scratch.innerHTML).toBe("
Hello Jane
"); + }); + + it("should rerender components wrapped in memo inline", async () => { + const { MemoApp, name } = await createComponent(` + import { signal } from '@preact/signals-core'; + import { memo } from 'react'; + + export const name = signal('John'); + + export const MemoApp = memo(({ name }) => { + return
Hello {name.value}
; + }); + `); + + await render(React.createElement(MemoApp as any, { name })); + expect(scratch.innerHTML).toBe("
Hello John
"); + + await act(async () => { + name.value = "Jane"; + }); + expect(scratch.innerHTML).toBe("
Hello Jane
"); + }); + + it("should rerender components wrapped in forwardRef", async () => { + const { ForwardRefApp, name } = await createComponent(` + import { signal } from '@preact/signals-core'; + import { forwardRef } from 'react'; + + export const name = signal('John'); + + function App({ name }, ref) { + return
Hello {name.value}
; + } + + export const ForwardRefApp = forwardRef(App); + `); + + const ref = React.createRef(); + await render(React.createElement(ForwardRefApp as any, { name, ref })); + expect(scratch.innerHTML).toBe("
Hello John
"); + expect(ref.current).toBe(scratch.firstChild); + + await act(async () => { + name.value = "Jane"; + }); + expect(scratch.innerHTML).toBe("
Hello Jane
"); + expect(ref.current).toBe(scratch.firstChild); + }); + + it("should rerender components wrapped in forwardRef inline", async () => { + const { ForwardRefApp, name } = await createComponent(` + import { signal } from '@preact/signals-core'; + import { forwardRef } from 'react'; + + export const name = signal('John'); + + export const ForwardRefApp = forwardRef(({ name }, ref) => { + return
Hello {name.value}
; + }); + `); + + const ref = React.createRef(); + await render(React.createElement(ForwardRefApp as any, { name, ref })); + expect(scratch.innerHTML).toBe("
Hello John
"); + expect(ref.current).toBe(scratch.firstChild); + + await act(async () => { + name.value = "Jane"; + }); + expect(scratch.innerHTML).toBe("
Hello Jane
"); + expect(ref.current).toBe(scratch.firstChild); + }); + + it("should rerender components wrapped in forwardRef with memo", async () => { + const { MemoForwardRefApp, name } = await createComponent(` + import { signal } from '@preact/signals-core'; + import { memo, forwardRef } from 'react'; + + export const name = signal('John'); + + export const MemoForwardRefApp = memo(forwardRef(({ name }, ref) => { + return
Hello {name.value}
; + })); + `); + + const ref = React.createRef(); + await render(React.createElement(MemoForwardRefApp as any, { name, ref })); + expect(scratch.innerHTML).toBe("
Hello John
"); + expect(ref.current).toBe(scratch.firstChild); + + await act(async () => { + name.value = "Jane"; + }); + expect(scratch.innerHTML).toBe("
Hello Jane
"); + expect(ref.current).toBe(scratch.firstChild); + }); + + it("should rerender registry-style declared components", async () => { + const { App, name, lang } = await createComponent(` + import { signal } from '@preact/signals-core'; + import { memo } from 'react'; + + const Greeting = { + English: memo(({ name }) =>
Hello {name.value}
), + ['Espanol']: memo(({ name }) =>
Hola {name.value}
), + }; + + export const name = signal('John'); + export const lang = signal('English'); + + export function App() { + const Component = Greeting[lang.value]; + return ; + } + `); + + await render(React.createElement(App as any)); + expect(scratch.innerHTML).toBe("
Hello John
"); + + await act(async () => { + name.value = "Jane"; + }); + expect(scratch.innerHTML).toBe("
Hello Jane
"); + + await act(async () => { + lang.value = "Espanol"; + }); + expect(scratch.innerHTML).toBe("
Hola Jane
"); + }); + + it("should transform components authored inside a test body", async () => { + const { name, App } = await createComponent(` + import { signal } from '@preact/signals-core'; + + export const name = signal('John'); + export let App; + + const it = (name, fn) => fn(); + + it('should work', () => { + App = () => { + return
Hello {name.value}
; + }; + }); + `); + + await render(React.createElement(App as any)); + expect(scratch.innerHTML).toBe("
Hello John
"); + + await act(async () => { + name.value = "Jane"; + }); + expect(scratch.innerHTML).toBe("
Hello Jane
"); + }); + + it("should work when an ambiguous function is manually transformed and used as a hook", async () => { + const { App, greeting, name } = await createComponent(` + import { signal } from '@preact/signals-core'; + + export const greeting = signal('Hello'); + export const name = signal('John'); + + /** @useSignals */ + function usename() { + return name.value; + } + + export function App() { + const name = usename(); + return
{greeting.value} {name}
; + } + `); + + await render(React.createElement(App as any)); + expect(scratch.innerHTML).toBe("
Hello John
"); + + await act(async () => { + greeting.value = "Hi"; + }); + expect(scratch.innerHTML).toBe("
Hi John
"); + + await act(async () => { + name.value = "Jane"; + }); + expect(scratch.innerHTML).toBe("
Hi Jane
"); + + await act(async () => { + batch(() => { + greeting.value = "Hello"; + name.value = "John"; + }); + }); + expect(scratch.innerHTML).toBe("
Hello John
"); + }); + + it("loads useSignals from a custom source", async () => { + const { App } = await createComponent( + ` + export function App({ name }) { + return
Hello {name.value}
; + } + `, + { importSource: customSource } + ); + + const name = signal("John"); + await render(React.createElement(App as any, { name })); + expect(scratch.innerHTML).toBe("
Hello John
"); + + await act(async () => { + name.value = "Jane"; + }); + expect(scratch.innerHTML).toBe("
Hello Jane
"); + }); + + it("works with the using keyword", async () => { + const { App } = await createComponent( + ` + import { useSignals } from '@preact/signals-react/runtime'; + + export function App({ name }) { + using _ = useSignals(); + return
Hello {name.value}
; + } + `, + { mode: "manual" } + ); + + const name = signal("John"); + await render(React.createElement(App as any, { name })); + expect(scratch.innerHTML).toBe("
Hello John
"); + + await act(async () => { + name.value = "Jane"; + }); + expect(scratch.innerHTML).toBe("
Hello Jane
"); + }); + + it("should transform components created by Array.map that use signals", async () => { + const { App } = await createComponent(` + export function App({ name }) { + const greetings = ['Hello', 'Goodbye']; + + const children = greetings.map((greeting) =>
{greeting} {name.value}
); + + return
{children}
; + } + `); + + const name = signal("John"); + await render(React.createElement(App as any, { name })); + expect(scratch.innerHTML).toBe( + "
Hello John
Goodbye John
" + ); + + await act(async () => { + name.value = "Jane"; + }); + expect(scratch.innerHTML).toBe( + "
Hello Jane
Goodbye Jane
" + ); + }); +}); diff --git a/packages/react-transform-rolldown/test/node/transform.test.ts b/packages/react-transform-rolldown/test/node/transform.test.ts new file mode 100644 index 000000000..21ddc37ed --- /dev/null +++ b/packages/react-transform-rolldown/test/node/transform.test.ts @@ -0,0 +1,1342 @@ +// @ts-nocheck + +import prettier from "prettier"; +import signalsTransform, { + type ReactSignalsTransformPluginOptions, +} from "../../src/index.ts"; +import { + CommentKind, + GeneratedCode, + assignmentComp, + objAssignComp, + declarationComp, + declarationHooks, + exportDefaultComp, + exportDefaultHooks, + exportNamedComp, + exportNamedHooks, + objectPropertyComp, + variableComp, + objMethodComp, + variableHooks, +} from "./helpers"; +import { it, describe, expect } from "vitest"; + +// Guidance for Debugging Generated Tests +// =============================== +// +// To help interactively debug a specific test case, add the test ids of the +// test cases you want to debug to the `debugTestIds` array, e.g. (["258", +// "259"]). Set to true to debug all tests. Set to false to skip all generated tests. +// +// The `debugger` statement in `runTestCases` will then trigger for the test case +// specified in the DEBUG_TEST_IDS. Follow the guide at https://vitest.dev/guide/debugging for +// instructions on debugging Vitest tests in your environment. +const DEBUG_TEST_IDS: string[] | boolean = []; + +const format = (code: string) => prettier.format(code, { parser: "babel" }); + +function normalizeTransformedCode(code: string): string { + return code + .replace( + /^(import \{ useSignals as _useSignals \} from [^\n]+;|var _useSignals = require\([^\n]+\)\.useSignals;?)\n\n/m, + "$1\n" + ) + .replace(/\{\n\s+children: ([^\n]+),?\n\s*\}/g, "{ children: $1 }") + .replace(/\{\n\s+name: ([^,\n]+),?\n\s*\}/g, "{ name: $1 }") + .replace( + /\{\n\s+name: ([^,\n]+),\n\s+watched: \(\) => \{\},?\n\s*\}/g, + "{ name: $1, watched: () => {} }" + ); +} + +async function transformCode( + code: string, + options?: ReactSignalsTransformPluginOptions, + filename = "virtual:entry.jsx" +): Promise { + const plugin = signalsTransform(options); + const handler = plugin.transform?.handler; + if (handler == null) { + return code; + } + + const result = await handler.call({}, code, filename, undefined); + + if (result == null) { + return code; + } + + return typeof result === "string" ? result : result.code; +} + +async function runTest( + input: string, + expected: string, + options: ReactSignalsTransformPluginOptions = { mode: "auto" }, + filename?: string, + _cjs?: boolean +) { + const output = await transformCode(input, options, filename); + expect(await format(normalizeTransformedCode(output))).to.equal( + await format(normalizeTransformedCode(expected)) + ); +} + +interface TestCaseConfig { + /** Whether to use components whose body contains valid code auto mode would transform (true) or not (false) */ + useValidAutoMode: boolean; + /** Whether to assert that the plugin transforms the code (true) or not (false) */ + expectTransformed: boolean; + /** What kind of opt-in or opt-out to include if any */ + comment?: CommentKind; + /** Options to pass to the babel plugin */ + options: ReactSignalsTransformPluginOptions; + /** The filename to run the transform under */ + filename?: string; +} + +let testCount = 0; +const getTestId = () => (testCount++).toString().padStart(3, "0"); + +function runTestCases(config: TestCaseConfig, testCases: GeneratedCode[]) { + testCases = testCases.toSorted((a, b) => (a.name < b.name ? -1 : 1)); + + for (const testCase of testCases) { + let testId = getTestId(); + + // Only run tests in debugTestIds + if ( + DEBUG_TEST_IDS === false || + (Array.isArray(DEBUG_TEST_IDS) && + DEBUG_TEST_IDS.length > 0 && + !DEBUG_TEST_IDS.includes(testId)) + ) { + continue; + } + + it(`(${testId}) ${testCase.name}`, async () => { + if (DEBUG_TEST_IDS === true || DEBUG_TEST_IDS.includes(testId)) { + console.log("input:", testCase.input.replace(/\s+/g, " ")); // eslint-disable-line no-console + debugger; // eslint-disable-line no-debugger + } + + const input = await format(testCase.input); + const transformed = await format(testCase.transformed); + + let expected = ""; + if (config.expectTransformed) { + expected += + 'import { useSignals as _useSignals } from "@preact/signals-react/runtime";\n'; + expected += transformed; + } else { + expected = input; + } + + await runTest(input, expected, config.options, config.filename); + }); + } +} + +function runGeneratedComponentTestCases(config: TestCaseConfig): void { + const codeConfig = { auto: config.useValidAutoMode, comment: config.comment }; + config = { + ...config, + filename: config.useValidAutoMode + ? "/path/to/Component.js" + : "C:\\path\\to\\lowercase.js", + }; + + // e.g. function C() {} + describe("function components", () => { + runTestCases(config, declarationComp(codeConfig)); + }); + + // e.g. const C = () => {}; + describe("variable declared components", () => { + runTestCases(config, variableComp(codeConfig)); + }); + + if (config.comment !== undefined) { + // e.g. const C = () => {}; + describe("variable declared components (inline comment)", () => { + runTestCases( + config, + variableComp({ + ...codeConfig, + comment: undefined, + inlineComment: config.comment, + }) + ); + }); + } + + describe("object method components", () => { + runTestCases(config, objMethodComp(codeConfig)); + }); + + // e.g. C = () => {}; + describe("assigned to variable components", () => { + runTestCases(config, assignmentComp(codeConfig)); + }); + + // e.g. obj.C = () => {}; + describe("assigned to object property components", () => { + runTestCases(config, objAssignComp(codeConfig)); + }); + + // e.g. const obj = { C: () => {} }; + describe("object property components", () => { + runTestCases(config, objectPropertyComp(codeConfig)); + }); + + // e.g. export default () => {}; + describe(`default exported components`, () => { + runTestCases(config, exportDefaultComp(codeConfig)); + }); + + // e.g. export function C() {} + describe("named exported components", () => { + runTestCases(config, exportNamedComp(codeConfig)); + }); +} + +function runGeneratedHookTestCases(config: TestCaseConfig): void { + const codeConfig = { auto: config.useValidAutoMode, comment: config.comment }; + config = { + ...config, + filename: config.useValidAutoMode + ? "/path/to/useCustomHook.js" + : "C:\\path\\to\\usecustomHook.js", + }; + + // e.g. function useCustomHook() {} + describe("function hooks", () => { + runTestCases(config, declarationHooks(codeConfig)); + }); + + // e.g. const useCustomHook = () => {} + describe("variable declared hooks", () => { + runTestCases(config, variableHooks(codeConfig)); + }); + + // e.g. export default () => {} + describe("default exported hooks", () => { + runTestCases(config, exportDefaultHooks(codeConfig)); + }); + + // e.g. export function useCustomHook() {} + describe("named exported hooks", () => { + runTestCases(config, exportNamedHooks(codeConfig)); + }); +} + +function runGeneratedTestCases(config: TestCaseConfig): void { + runGeneratedComponentTestCases(config); + runGeneratedHookTestCases(config); +} + +describe("React Signals Rolldown Transform", () => { + describe("auto mode transforms", () => { + runGeneratedTestCases({ + useValidAutoMode: true, + expectTransformed: true, + options: { mode: "auto" }, + }); + + it("detects destructuring patterns with value property", async () => { + const inputCode = ` + function MyComponent(props) { + const { value: signalValue } = props.signal; + return
{signalValue}
; + } + `; + + const expectedOutput = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + function MyComponent(props) { + var _effect = _useSignals(1); + try { + const { value: signalValue } = props.signal; + return
{signalValue}
; + } finally { + _effect.f(); + } + } + `; + + await runTest(inputCode, expectedOutput); + }); + + it("detects nested destructuring patterns with value property", async () => { + // Test case 1: Simple nested destructuring + const inputCode1 = ` + function MyComponent(props) { + const { signal: { value } } = props; + return
{value}
; + } + `; + + const expectedOutput1 = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + function MyComponent(props) { + var _effect = _useSignals(1); + try { + const { signal: { value } } = props; + return
{value}
; + } finally { + _effect.f(); + } + } + `; + + await runTest(inputCode1, expectedOutput1); + + // Test case 2: Deeply nested destructuring + const inputCode2 = ` + function MyComponent(props) { + const { data: { signal: { value: signalValue } } } = props; + return
{signalValue}
; + } + `; + + const expectedOutput2 = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + function MyComponent(props) { + var _effect = _useSignals(1); + try { + const { data: { signal: { value: signalValue } } } = props; + return
{signalValue}
; + } finally { + _effect.f(); + } + } + `; + + await runTest(inputCode2, expectedOutput2); + + // Test case 3: Multiple value properties at different levels + const inputCode3 = ` + function MyComponent(props) { + const { value: outerValue, signal: { value: innerValue } } = props; + return
{outerValue} {innerValue}
; + } + `; + + const expectedOutput3 = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + function MyComponent(props) { + var _effect = _useSignals(1); + try { + const { value: outerValue, signal: { value: innerValue } } = props; + return
{outerValue} {innerValue}
; + } finally { + _effect.f(); + } + } + `; + + await runTest(inputCode3, expectedOutput3); + }); + + it("signal access in nested functions", async () => { + const inputCode = ` + function MyComponent(props) { + return props.listSignal.value.map(function iteration(x) { + return
{x}
; + }); + }; + `; + + const expectedOutput = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + function MyComponent(props) { + var _effect = _useSignals(1); + try { + return props.listSignal.value.map(function iteration(x) { + return
{x}
; + }); + } finally { + _effect.f(); + } + } + `; + + await runTest(inputCode, expectedOutput); + }); + }); + + describe("auto mode doesn't transform", () => { + it("should not leak JSX detection outside of component scope", async () => { + const inputCode = ` + function wrapper() { + function Component() { + return
Hello
; + } + const CountModel = createModel(() => ({ + count: signal(0), + increment() { + this.count.value++; + }, + })); + } + `; + + const expectedOutput = inputCode; + + await runTest(inputCode, expectedOutput); + }); + + it("should not leak JSX detection outside of non-components", async () => { + const inputCode = ` + describe("suite", () => { + it("test 1", () => { + render(); + }); + it("test 2", () => { + const CountModel = () => signal.value; + function Counter() { + return
Hello2
; + } + render(); + }); + }); + `; + + const expectedOutput = inputCode; + + await runTest(inputCode, expectedOutput); + }); + + it("useEffect callbacks that use signals", async () => { + const inputCode = ` + function App() { + useEffect(() => { + signal.value = Hi; + }, []); + return
Hello World
; + } + `; + + const expectedOutput = inputCode; + await runTest(inputCode, expectedOutput); + }); + + runGeneratedTestCases({ + useValidAutoMode: false, + expectTransformed: false, + options: { mode: "auto" }, + }); + }); + + describe("auto mode supports opting out of transforming", () => { + it("opt-out comment overrides opt-in comment", async () => { + const inputCode = ` + /** + * @noUseSignals + * @useSignals + */ + function MyComponent() { + return
{signal.value}
; + }; + `; + + const expectedOutput = inputCode; + + await runTest(inputCode, expectedOutput, { mode: "auto" }); + }); + + runGeneratedTestCases({ + useValidAutoMode: true, + expectTransformed: false, + comment: "opt-out", + options: { mode: "auto" }, + }); + }); + + describe("auto mode supports opting into transformation", () => { + runGeneratedTestCases({ + useValidAutoMode: false, + expectTransformed: true, + comment: "opt-in", + options: { mode: "auto" }, + }); + }); + + describe("manual mode doesn't transform anything by default", () => { + it("useEffect callbacks that use signals", async () => { + const inputCode = ` + function App() { + useEffect(() => { + signal.value = Hi; + }, []); + return
Hello World
; + } + `; + + const expectedOutput = inputCode; + await runTest(inputCode, expectedOutput); + }); + + runGeneratedTestCases({ + useValidAutoMode: true, + expectTransformed: false, + options: { mode: "manual" }, + }); + }); + + describe("manual mode opts into transforming", () => { + it("opt-out comment overrides opt-in comment", async () => { + const inputCode = ` + /** + * @noUseSignals + * @useSignals + */ + function MyComponent() { + return
{signal.value}
; + }; + `; + + const expectedOutput = inputCode; + + await runTest(inputCode, expectedOutput, { mode: "auto" }); + }); + + runGeneratedTestCases({ + useValidAutoMode: true, + expectTransformed: true, + comment: "opt-in", + options: { mode: "manual" }, + }); + }); +}); + +describe("React Signals Rolldown Transform", () => { + // TODO: Figure out what to do with the following + + describe("all mode transformations", () => { + it("should not leak JSX detection outside of component scope", async () => { + const inputCode = ` + function wrapper() { + function Component() { + return
Hello
; + } + const CountModel = createModel(() => ({ + count: signal(0), + increment() { + this.count.value++; + }, + })); + } + `; + + const expectedOutput = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + function wrapper() { + function Component() { + var _effect = _useSignals(1); + try { + return
Hello
; + } finally { + _effect.f(); + } + } + const CountModel = createModel(() => ({ + count: signal(0), + increment() { + this.count.value++; + }, + })); + } + `; + + await runTest(inputCode, expectedOutput, { mode: "all" }); + }); + + it("should not leak JSX detection outside of non-components", async () => { + const inputCode = ` + describe("suite", () => { + it("test 1", () => { + render(); + }); + it("test 2", () => { + const CountModel = () => signal.value; + function Counter() { + return
Hello2
; + } + render(); + }); + }); + `; + + const expectedOutput = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + describe("suite", () => { + it("test 1", () => { + render(); + }); + it("test 2", () => { + const CountModel = () => signal.value; + function Counter() { + var _effect = _useSignals(1); + try { + return
Hello2
; + } finally { + _effect.f(); + } + } + render(); + }); + }); + `; + + await runTest(inputCode, expectedOutput, { mode: "all" }); + }); + + it("skips transforming arrow function component with leading opt-out JSDoc comment before variable declaration", async () => { + const inputCode = ` + /** @noUseSignals */ + const MyComponent = () => { + return
{signal.value}
; + }; + `; + + const expectedOutput = inputCode; + + await runTest(inputCode, expectedOutput, { mode: "all" }); + }); + + it("skips transforming function declaration components with leading opt-out JSDoc comment", async () => { + const inputCode = ` + /** @noUseSignals */ + function MyComponent() { + return
{signal.value}
; + } + `; + + const expectedOutput = inputCode; + + await runTest(inputCode, expectedOutput, { mode: "all" }); + }); + + it("transforms function declaration component that doesn't use signals", async () => { + const inputCode = ` + function MyComponent() { + return
Hello World
; + } + `; + + const expectedOutput = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + function MyComponent() { + var _effect = _useSignals(1); + try { + return
Hello World
; + } finally { + _effect.f(); + } + } + `; + + await runTest(inputCode, expectedOutput, { mode: "all" }); + }); + + it("transforms require syntax", async () => { + const inputCode = ` + const react = require("react"); + function MyComponent() { + return
Hello World
; + } + `; + + const expectedOutput = ` + var _useSignals = require("@preact/signals-react/runtime").useSignals + const react = require("react"); + function MyComponent() { + var _effect = _useSignals(1); + try { + return
Hello World
; + } finally { + _effect.f(); + } + } + `; + await runTest( + inputCode, + expectedOutput, + { mode: "all" }, + undefined, + true + ); + }); + + it("transforms arrow function component with return statement that doesn't use signals", async () => { + const inputCode = ` + const MyComponent = () => { + return
Hello World
; + }; + `; + + const expectedOutput = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + const MyComponent = () => { + var _effect = _useSignals(1); + try { + return
Hello World
; + } finally { + _effect.f(); + } + }; + `; + + await runTest(inputCode, expectedOutput, { mode: "all" }); + }); + + it("transforms function declaration component that uses signals", async () => { + const inputCode = ` + function MyComponent() { + signal.value; + return
Hello World
; + } + `; + + const expectedOutput = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + function MyComponent() { + var _effect = _useSignals(1); + try { + signal.value; + return
Hello World
; + } finally { + _effect.f(); + } + } + `; + + await runTest(inputCode, expectedOutput, { mode: "all" }); + }); + + it("transforms arrow function component with return statement that uses signals", async () => { + const inputCode = ` + const MyComponent = () => { + signal.value; + return
Hello World
; + }; + `; + + const expectedOutput = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + const MyComponent = () => { + var _effect = _useSignals(1); + try { + signal.value; + return
Hello World
; + } finally { + _effect.f(); + } + }; + `; + + await runTest(inputCode, expectedOutput, { mode: "all" }); + }); + }); + + describe("noTryFinally option", () => { + it("prepends arrow function component with useSignals call", async () => { + const inputCode = ` + const MyComponent = () => { + signal.value; + return
Hello World
; + }; + `; + + const expectedOutput = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + const MyComponent = () => { + _useSignals(); + signal.value; + return
Hello World
; + }; + `; + + await runTest(inputCode, expectedOutput, { + experimental: { noTryFinally: true }, + }); + }); + + it("prepends arrow function component with useSignals call", async () => { + const inputCode = ` + const MyComponent = () =>
{name.value}
; + `; + + const expectedOutput = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + const MyComponent = () => { + _useSignals(); + return
{name.value}
; + }; + `; + + await runTest(inputCode, expectedOutput, { + experimental: { noTryFinally: true }, + }); + }); + + it("prepends function declaration components with useSignals call", async () => { + const inputCode = ` + function MyComponent() { + signal.value; + return
Hello World
; + } + `; + + const expectedOutput = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + function MyComponent() { + _useSignals(); + signal.value; + return
Hello World
; + } + `; + + await runTest(inputCode, expectedOutput, { + experimental: { noTryFinally: true }, + }); + }); + + it("prepends function expression components with useSignals call", async () => { + const inputCode = ` + const MyComponent = function () { + signal.value; + return
Hello World
; + } + `; + + const expectedOutput = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + const MyComponent = function () { + _useSignals(); + signal.value; + return
Hello World
; + }; + `; + + await runTest(inputCode, expectedOutput, { + experimental: { noTryFinally: true }, + }); + }); + + it("prepends custom hook function declarations with useSignals call", async () => { + const inputCode = ` + function useCustomHook() { + signal.value; + return useState(0); + } + `; + + const expectedOutput = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + function useCustomHook() { + _useSignals(); + signal.value; + return useState(0); + } + `; + + await runTest(inputCode, expectedOutput, { + experimental: { noTryFinally: true }, + }); + }); + + it("recursively propogates `.value` reads to parent component", async () => { + const inputCode = ` + function MyComponent() { + return
{new Array(20).fill(null).map(() => signal.value)}
; + } + `; + + const expectedOutput = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + function MyComponent() { + _useSignals(); + return
{new Array(20).fill(null).map(() => signal.value)}
; + } + `; + + await runTest(inputCode, expectedOutput, { + experimental: { noTryFinally: true }, + }); + }); + }); + + describe("importSource option", () => { + it("imports useSignals from custom source", async () => { + const inputCode = ` + const MyComponent = () => { + signal.value; + return
Hello World
; + }; + `; + + const expectedOutput = ` + import { useSignals as _useSignals } from "custom-source"; + const MyComponent = () => { + var _effect = _useSignals(1); + try { + signal.value; + return
Hello World
; + } finally { + _effect.f(); + } + }; + `; + + await runTest(inputCode, expectedOutput, { + importSource: "custom-source", + }); + }); + }); + + describe("scope tracking", () => { + it("adds an import declaration and usage for useSignals", async () => { + const output = await transformCode( + ` + const MyComponent = () => { + signal.value; + return
Hello World
; + }; + `, + { mode: "auto" }, + "Component.jsx" + ); + + expect(output).toContain( + `import { useSignals as _useSignals } from "@preact/signals-react/runtime";` + ); + expect(output).toContain("_useSignals(1)"); + }); + }); + + describe("signal naming", () => { + const DEBUG_OPTIONS = { mode: "auto", experimental: { debug: true } }; + + const runDebugTest = async ( + inputCode: string, + expectedOutput: string, + fileName: string + ) => { + // @ts-expect-error + await runTest(inputCode, expectedOutput, DEBUG_OPTIONS, fileName); + }; + + it("injects names for signal calls", async () => { + const inputCode = ` + function MyComponent() { + const count = signal(0); + const double = computed(() => count.value * 2); + return
{double.value}
; + } + `; + + const expectedOutput = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + function MyComponent() { + var _effect = _useSignals(1, "MyComponent"); + try { + const count = signal(0, { + name: "count (Component.js:3)", + }); + const double = computed(() => count.value * 2, { + name: "double (Component.js:4)", + }); + return
{double.value}
; + } finally { + _effect.f(); + } + } + `; + + await runDebugTest(inputCode, expectedOutput, "Component.js"); + }); + + it("injects names for useSignal calls", async () => { + const inputCode = ` + function MyComponent() { + const count = useSignal(0); + const message = useSignal("hello"); + return
{count.value} {message.value}
; + } + `; + + const expectedOutput = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + function MyComponent() { + var _effect = _useSignals(1, "MyComponent"); + try { + const count = useSignal(0, { + name: "count (Component.js:3)", + }); + const message = useSignal("hello", { + name: "message (Component.js:4)", + }); + return
{count.value} {message.value}
; + } finally { + _effect.f(); + } + } + `; + + await runDebugTest(inputCode, expectedOutput, "Component.js"); + }); + + it("doesn't inject names when already provided", async () => { + const inputCode = ` + function MyComponent() { + const count = signal(0, { name: "myCounter" }); + const data = useSignal(null, { name: "userData", watched: () => {} }); + return
{count.value}
; + } + `; + + const expectedOutput = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + function MyComponent() { + var _effect = _useSignals(1, "MyComponent"); + try { + const count = signal(0, { + name: "myCounter", + }); + const data = useSignal(null, { + name: "userData", + watched: () => {}, + }); + return
{count.value}
; + } finally { + _effect.f(); + } + } + `; + + await runDebugTest(inputCode, expectedOutput, "Component.js"); + }); + + it("handles signals with no initial value", async () => { + const inputCode = ` + function MyComponent() { + const count = useSignal(); + return
{count.value}
; + } + `; + + const expectedOutput = ` + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + function MyComponent() { + var _effect = _useSignals(1, "MyComponent"); + try { + const count = useSignal(undefined, { + name: "count (Component.js:3)", + }); + return
{count.value}
; + } finally { + _effect.f(); + } + } + `; + + await runDebugTest(inputCode, expectedOutput, "Component.js"); + }); + }); + + describe("detectTransformedJSX option", () => { + it("detects elements created using react/jsx-runtime import", async () => { + const inputCode = ` + import { jsx as _jsx } from "react/jsx-runtime"; + function MyComponent() { + signal.value; + return _jsx("div", { children: "Hello World" }); + }; + `; + + const expectedOutput = ` + import { jsx as _jsx } from "react/jsx-runtime"; + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + function MyComponent() { + var _effect = _useSignals(1); + try { + signal.value; + return _jsx("div", { + children: "Hello World", + }); + } finally { + _effect.f(); + } + } + `; + + await runTest(inputCode, expectedOutput, { + detectTransformedJSX: true, + }); + }); + + it("detects elements created using react/jsx-runtime cjs require", async () => { + const inputCode = ` + const jsxRuntime = require("react/jsx-runtime"); + function MyComponent() { + signal.value; + return jsxRuntime.jsx("div", { children: "Hello World" }); + }; + `; + + const expectedOutput = ` + var _useSignals = require("@preact/signals-react/runtime").useSignals + const jsxRuntime = require("react/jsx-runtime"); + function MyComponent() { + var _effect = _useSignals(1); + try { + signal.value; + return jsxRuntime.jsx("div", { + children: "Hello World", + }); + } finally { + _effect.f(); + } + } + `; + + await runTest( + inputCode, + expectedOutput, + { + detectTransformedJSX: true, + }, + undefined, + true + ); + }); + + it("detects elements created using react/jsx-runtime cjs destuctured import", async () => { + const inputCode = ` + const { jsx } = require("react/jsx-runtime"); + function MyComponent() { + signal.value; + return jsx("div", { children: "Hello World" }); + }; + `; + + const expectedOutput = ` + var _useSignals = require("@preact/signals-react/runtime").useSignals + const { jsx } = require("react/jsx-runtime"); + function MyComponent() { + var _effect = _useSignals(1); + try { + signal.value; + return jsx("div", { + children: "Hello World", + }); + } finally { + _effect.f(); + } + } + `; + + await runTest( + inputCode, + expectedOutput, + { + detectTransformedJSX: true, + }, + undefined, + true + ); + }); + + it("does not detect jsx-runtime calls when detectJSXAlternatives is disabled", async () => { + const inputCode = ` + import { jsx as _jsx } from "react/jsx-runtime"; + function MyComponent() { + signal.value; + return _jsx("div", { children: "Hello World" }); + }; + `; + + // Should not transform because jsx-runtime detection is disabled - no useSignals import should be added + const expectedOutput = ` + import { jsx as _jsx } from "react/jsx-runtime"; + function MyComponent() { + signal.value; + return _jsx("div", { + children: "Hello World", + }); + } + `; + + await runTest(inputCode, expectedOutput, { + detectTransformedJSX: false, + }); + }); + + it("detects createElement calls created using react import", async () => { + const inputCode = ` + import { createElement } from "react"; + function MyComponent() { + signal.value; + return createElement("div", { children: "Hello World" }); + }; + `; + + const expectedOutput = ` + import { createElement } from "react"; + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + function MyComponent() { + var _effect = _useSignals(1); + try { + signal.value; + return createElement("div", { + children: "Hello World", + }); + } finally { + _effect.f(); + } + } + `; + + await runTest(inputCode, expectedOutput, { + detectTransformedJSX: true, + }); + }); + + it("detects createElement calls created using react default import", async () => { + const inputCode = ` + import React from "react"; + function MyComponent() { + signal.value; + return React.createElement("div", { children: "Hello World" }); + }; + `; + + const expectedOutput = ` + import React from "react"; + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + function MyComponent() { + var _effect = _useSignals(1); + try { + signal.value; + return React.createElement("div", { + children: "Hello World", + }); + } finally { + _effect.f(); + } + } + `; + + await runTest(inputCode, expectedOutput, { + detectTransformedJSX: true, + }); + }); + + it("detects createElement calls created using react cjs require", async () => { + const inputCode = ` + const React = require("react"); + function MyComponent() { + signal.value; + return React.createElement("div", { children: "Hello World" }); + }; + `; + + const expectedOutput = ` + var _useSignals = require("@preact/signals-react/runtime").useSignals + const React = require("react"); + function MyComponent() { + var _effect = _useSignals(1); + try { + signal.value; + return React.createElement("div", { + children: "Hello World", + }); + } finally { + _effect.f(); + } + } + `; + + await runTest( + inputCode, + expectedOutput, + { + detectTransformedJSX: true, + }, + undefined, + true + ); + }); + + it("detects createElement calls created using destructured react cjs require", async () => { + const inputCode = ` + const { createElement } = require("react"); + function MyComponent() { + signal.value; + return createElement("div", { children: "Hello World" }); + }; + `; + + const expectedOutput = ` + var _useSignals = require("@preact/signals-react/runtime").useSignals + const { createElement } = require("react"); + function MyComponent() { + var _effect = _useSignals(1); + try { + signal.value; + return createElement("div", { + children: "Hello World", + }); + } finally { + _effect.f(); + } + } + `; + + await runTest( + inputCode, + expectedOutput, + { + detectTransformedJSX: true, + }, + undefined, + true + ); + }); + + it("detects signal access in nested functions", async () => { + const inputCode = ` + import { jsx } from "react/jsx-runtime"; + function MyComponent(props) { + return props.listSignal.value.map(function iteration(x) { + return jsx("div", { children: x }); + }); + }; + `; + + const expectedOutput = ` + import { jsx } from "react/jsx-runtime"; + import { useSignals as _useSignals } from "@preact/signals-react/runtime"; + function MyComponent(props) { + var _effect = _useSignals(1); + try { + return props.listSignal.value.map(function iteration(x) { + return jsx("div", { + children: x, + }); + }); + } finally { + _effect.f(); + } + } + `; + + await runTest(inputCode, expectedOutput, { + detectTransformedJSX: true, + }); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8eb8d52c1..3da6f4087 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,13 +69,13 @@ importers: version: 20.19.30 '@vitest/browser': specifier: ^4.0.17 - version: 4.0.17(vite@6.3.5(@types/node@20.19.30))(vitest@4.0.17) + version: 4.0.17(vite@6.3.5(@types/node@20.19.30)(lightningcss@1.32.0))(vitest@4.0.17) '@vitest/browser-playwright': specifier: ^4.0.17 - version: 4.0.17(playwright@1.53.1)(vite@6.3.5(@types/node@20.19.30))(vitest@4.0.17) + version: 4.0.17(playwright@1.53.1)(vite@6.3.5(@types/node@20.19.30)(lightningcss@1.32.0))(vitest@4.0.17) '@vitest/coverage-v8': specifier: ^4.0.17 - version: 4.0.17(@vitest/browser@4.0.17(vite@6.3.5(@types/node@20.19.30))(vitest@4.0.17))(vitest@4.0.17) + version: 4.0.17(@vitest/browser@4.0.17(vite@6.3.5(@types/node@20.19.30)(lightningcss@1.32.0))(vitest@4.0.17))(vitest@4.0.17) babel-plugin-istanbul: specifier: ^6.1.1 version: 6.1.1 @@ -126,10 +126,10 @@ importers: version: 5.8.3 vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@20.19.30) + version: 6.3.5(@types/node@20.19.30)(lightningcss@1.32.0) vitest: specifier: ^4.0.17 - version: 4.0.17(@types/node@20.19.30)(@vitest/browser-playwright@4.0.17) + version: 4.0.17(@types/node@20.19.30)(@vitest/browser-playwright@4.0.17)(jsdom@26.1.0)(lightningcss@1.32.0) docs: dependencies: @@ -178,7 +178,7 @@ importers: version: 7.28.5 '@preact/preset-vite': specifier: ^2.3.0 - version: 2.3.0(@babel/core@7.28.5)(preact@10.27.2)(vite@6.3.5(@types/node@20.19.30)) + version: 2.3.0(@babel/core@7.28.5)(preact@10.27.2)(vite@6.3.5(@types/node@20.19.30)(lightningcss@1.32.0)) '@types/react': specifier: ^18.0.18 version: 18.0.18 @@ -205,7 +205,7 @@ importers: version: 0.2.9 vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@20.19.30) + version: 6.3.5(@types/node@20.19.30)(lightningcss@1.32.0) extension: dependencies: @@ -227,7 +227,7 @@ importers: devDependencies: '@preact/preset-vite': specifier: ^2.3.0 - version: 2.3.0(@babel/core@7.28.5)(preact@10.27.2)(vite@6.3.5(@types/node@20.19.30)) + version: 2.3.0(@babel/core@7.28.5)(preact@10.27.2)(vite@6.3.5(@types/node@20.19.30)(lightningcss@1.32.0)) '@types/chrome': specifier: ^0.0.270 version: 0.0.270 @@ -236,7 +236,7 @@ importers: version: 5.8.3 vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@20.19.30) + version: 6.3.5(@types/node@20.19.30)(lightningcss@1.32.0) web-ext: specifier: ^7.0.0 version: 7.12.0 @@ -272,7 +272,7 @@ importers: version: 20.19.30 '@vitest/browser': specifier: ^4.0.0 - version: 4.0.17(vite@7.3.1(@types/node@20.19.30))(vitest@4.0.17) + version: 4.0.17(vite@7.3.1(@types/node@20.19.30)(lightningcss@1.32.0))(vitest@4.0.17) playwright: specifier: ^1.53.1 version: 1.53.1 @@ -284,10 +284,10 @@ importers: version: 5.8.3 vite: specifier: ^7.0.0 - version: 7.3.1(@types/node@20.19.30) + version: 7.3.1(@types/node@20.19.30)(lightningcss@1.32.0) vitest: specifier: ^4.0.17 - version: 4.0.17(@types/node@20.19.30)(@vitest/browser-playwright@4.0.17) + version: 4.0.17(@types/node@20.19.30)(@vitest/browser-playwright@4.0.17)(jsdom@26.1.0)(lightningcss@1.32.0) packages/eslint-plugin-signals: devDependencies: @@ -458,6 +458,40 @@ importers: specifier: ^6.9.0 version: 6.10.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + packages/react-transform-rolldown: + dependencies: + rolldown-string: + specifier: ^0.3.0 + version: 0.3.0(rolldown@1.0.0-rc.9) + vite: + specifier: ^8.0.0 + version: 8.0.0(@types/node@20.19.30)(esbuild@0.27.2) + devDependencies: + '@preact/signals-core': + specifier: workspace:* + version: link:../core + '@preact/signals-react': + specifier: workspace:^3.9.1 + version: link:../react + '@types/react': + specifier: 18.0.18 + version: 18.0.18 + '@types/react-dom': + specifier: 18.0.6 + version: 18.0.6 + prettier: + specifier: ^3.0.0 + version: 3.6.2 + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + rolldown: + specifier: ^1.0.0-rc.9 + version: 1.0.0-rc.9 + packages/react/runtime: dependencies: '@preact/signals-core': @@ -507,6 +541,9 @@ importers: packages: + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -1161,6 +1198,34 @@ packages: '@changesets/write@0.3.0': resolution: {integrity: sha512-slGLb21fxZVUYbyea+94uFiD6ntQW0M2hIKNznFizDhZPDgn2c/fv1UzzlW43RVzh1BEDuIqW6hzlJ1OflNmcw==} + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@csstools/selector-specificity@2.0.2': resolution: {integrity: sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==} engines: {node: ^12 || ^14 || >=16} @@ -1181,6 +1246,15 @@ packages: engines: {node: '>= 0.10.4'} hasBin: true + '@emnapi/core@1.9.0': + resolution: {integrity: sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==} + + '@emnapi/runtime@1.9.0': + resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==} + + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + '@esbuild/aix-ppc64@0.25.5': resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} engines: {node: '>=18'} @@ -1608,6 +1682,9 @@ packages: '@mdn/browser-compat-data@5.5.29': resolution: {integrity: sha512-NHdG3QOiAsxh8ygBSKMa/WaNJwpNt87uVqW+S2RlnSqgeRdk+L3foNWTX6qd0I3NHSlCFb47rgopeNCJtRDY5A==} + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1620,6 +1697,13 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@oxc-project/runtime@0.115.0': + resolution: {integrity: sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@oxc-project/types@0.115.0': + resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} + '@oxfmt/binding-android-arm-eabi@0.35.0': resolution: {integrity: sha512-BaRKlM3DyG81y/xWTsE6gZiv89F/3pHe2BqX2H4JbiB8HNVlWWtplzgATAE5IDSdwChdeuWLDTQzJ92Lglw3ZA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1907,6 +1991,98 @@ packages: resolution: {integrity: sha512-bkUDCp8o1MvFO+qxkODcbhSqRa6P2GXgrGZVpt0dCXNW2HCSCqYI0ZoAqEOSAjRWmmlKcYgFvN4B4S+zo/f8kg==} engines: {node: '>=14'} + '@rolldown/binding-android-arm64@1.0.0-rc.9': + resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.9': + resolution: {integrity: sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.9': + resolution: {integrity: sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.9': + resolution: {integrity: sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': + resolution: {integrity: sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': + resolution: {integrity: sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': + resolution: {integrity: sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': + resolution: {integrity: sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': + resolution: {integrity: sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': + resolution: {integrity: sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': + resolution: {integrity: sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.9': + resolution: {integrity: sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==} + '@rollup/plugin-alias@3.1.9': resolution: {integrity: sha512-QI5fsEvm9bDzt32k39wpOwZhVzRcL5ydcffUHMyLVaVaLeC70I8TJZ17F1z1eMoLu4E/UOcH9BWVkKpIKdrfiw==} engines: {node: '>=8.0.0'} @@ -2069,6 +2245,9 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -2297,6 +2476,10 @@ packages: resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} engines: {node: '>=12.0'} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -2772,6 +2955,10 @@ packages: resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} engines: {node: '>=8.0.0'} + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + csstype@3.1.0: resolution: {integrity: sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==} @@ -2796,6 +2983,10 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} @@ -2840,6 +3031,9 @@ packages: resolution: {integrity: sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -2881,6 +3075,10 @@ packages: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -3633,6 +3831,10 @@ packages: hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -3642,6 +3844,10 @@ packages: http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + http-signature@1.2.0: resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} engines: {node: '>=0.8', npm: '>=1.3.7'} @@ -3650,6 +3856,10 @@ packages: resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} engines: {node: '>=10.19.0'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-id@1.0.2: resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} @@ -3666,6 +3876,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + icss-replace-symbols@1.1.0: resolution: {integrity: sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==} @@ -3844,6 +4058,9 @@ packages: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -3970,6 +4187,15 @@ packages: jsbn@0.1.1: resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.0.2: resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} engines: {node: '>=6'} @@ -4068,6 +4294,76 @@ packages: lighthouse-logger@1.4.2: resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -4387,6 +4683,9 @@ packages: resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} engines: {node: '>=0.10.0'} + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + oauth-sign@0.9.0: resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} @@ -4865,6 +5164,10 @@ packages: 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} + preact-iso@2.3.0: resolution: {integrity: sha512-taJmRidbWrjHEhoVoxXS2Kvxa6X3jXSsTtD7rSYeJuxnPNr1ghCu1JUzCrRxmZwTUNWIqwUpNi+AJoLtvCPN7g==} peerDependencies: @@ -5127,6 +5430,20 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true + rolldown-string@0.3.0: + resolution: {integrity: sha512-qGhBPNSv/27uzFBQdO+Cs4YAXC/1PKznD7Jz5Fl7NIAIlteuTPBTBWBhCpJ2AFTqLsoKvvUr7wSjqSUip0Fkpg==} + engines: {node: '>=20.19.0'} + peerDependencies: + rolldown: '*' + peerDependenciesMeta: + rolldown: + optional: true + + rolldown@1.0.0-rc.9: + resolution: {integrity: sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup-plugin-bundle-size@1.0.3: resolution: {integrity: sha512-aWj0Pvzq90fqbI5vN1IvUrlf4utOqy+AERYxwWjegH1G8PzheMnrRIgQ5tkwKVtQMDP0bHZEACW/zLDF+XgfXQ==} @@ -5168,6 +5485,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -5197,6 +5517,10 @@ packages: sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} @@ -5493,6 +5817,9 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -5544,6 +5871,13 @@ packages: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -5568,9 +5902,17 @@ packages: resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} engines: {node: '>=0.8'} + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + trim-newlines@3.0.1: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} engines: {node: '>=8'} @@ -5808,6 +6150,49 @@ packages: yaml: optional: true + vite@8.0.0: + resolution: {integrity: sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.0.0-alpha.31 + esbuild: ^0.27.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vitest@4.0.17: resolution: {integrity: sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -5842,6 +6227,10 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + watchpack@2.4.0: resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} engines: {node: '>=10.13.0'} @@ -5861,6 +6250,23 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -5961,6 +6367,10 @@ packages: resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} engines: {node: '>=12'} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + xml2js@0.5.0: resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} engines: {node: '>=4.0.0'} @@ -5969,6 +6379,9 @@ packages: resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} engines: {node: '>=4.0'} + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} @@ -6018,6 +6431,15 @@ packages: snapshots: + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + optional: true + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -6958,6 +7380,31 @@ snapshots: human-id: 1.0.2 prettier: 2.7.1 + '@csstools/color-helpers@5.1.0': + optional: true + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + optional: true + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + optional: true + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + optional: true + + '@csstools/css-tokenizer@3.0.4': + optional: true + '@csstools/selector-specificity@2.0.2(postcss-selector-parser@6.0.10)(postcss@8.5.6)': dependencies: postcss: 8.5.6 @@ -6979,6 +7426,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@emnapi/core@1.9.0': + dependencies: + '@emnapi/wasi-threads': 1.2.0 + tslib: 2.4.0 + optional: true + + '@emnapi/runtime@1.9.0': + dependencies: + tslib: 2.4.0 + optional: true + + '@emnapi/wasi-threads@1.2.0': + dependencies: + tslib: 2.4.0 + optional: true + '@esbuild/aix-ppc64@0.25.5': optional: true @@ -7286,6 +7749,13 @@ snapshots: '@mdn/browser-compat-data@5.5.29': {} + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.9.0 + '@emnapi/runtime': 1.9.0 + '@tybys/wasm-util': 0.10.1 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -7298,6 +7768,10 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.13.0 + '@oxc-project/runtime@0.115.0': {} + + '@oxc-project/types@0.115.0': {} + '@oxfmt/binding-android-arm-eabi@0.35.0': optional: true @@ -7429,18 +7903,18 @@ snapshots: '@polka/url@1.0.0-next.29': {} - '@preact/preset-vite@2.3.0(@babel/core@7.28.5)(preact@10.27.2)(vite@6.3.5(@types/node@20.19.30))': + '@preact/preset-vite@2.3.0(@babel/core@7.28.5)(preact@10.27.2)(vite@6.3.5(@types/node@20.19.30)(lightningcss@1.32.0))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.5) '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.5) - '@prefresh/vite': 2.2.8(preact@10.27.2)(vite@6.3.5(@types/node@20.19.30)) + '@prefresh/vite': 2.2.8(preact@10.27.2)(vite@6.3.5(@types/node@20.19.30)(lightningcss@1.32.0)) '@rollup/pluginutils': 4.2.1 babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.28.5) debug: 4.4.3 kolorist: 1.5.1 resolve: 1.22.10 - vite: 6.3.5(@types/node@20.19.30) + vite: 6.3.5(@types/node@20.19.30)(lightningcss@1.32.0) transitivePeerDependencies: - preact - supports-color @@ -7466,7 +7940,7 @@ snapshots: '@prefresh/utils@1.1.3': {} - '@prefresh/vite@2.2.8(preact@10.27.2)(vite@6.3.5(@types/node@20.19.30))': + '@prefresh/vite@2.2.8(preact@10.27.2)(vite@6.3.5(@types/node@20.19.30)(lightningcss@1.32.0))': dependencies: '@babel/core': 7.28.5 '@prefresh/babel-plugin': 0.4.3 @@ -7474,12 +7948,61 @@ snapshots: '@prefresh/utils': 1.1.3 '@rollup/pluginutils': 4.2.1 preact: 10.27.2 - vite: 6.3.5(@types/node@20.19.30) + vite: 6.3.5(@types/node@20.19.30)(lightningcss@1.32.0) transitivePeerDependencies: - supports-color '@remix-run/router@1.5.0': {} + '@rolldown/binding-android-arm64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.9': {} + '@rollup/plugin-alias@3.1.9(rollup@2.79.1)': dependencies: rollup: 2.79.1 @@ -7611,6 +8134,11 @@ snapshots: '@trysound/sax@0.2.0': {} + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.4.0 + optional: true + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.5 @@ -7768,29 +8296,29 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitest/browser-playwright@4.0.17(playwright@1.53.1)(vite@6.3.5(@types/node@20.19.30))(vitest@4.0.17)': + '@vitest/browser-playwright@4.0.17(playwright@1.53.1)(vite@6.3.5(@types/node@20.19.30)(lightningcss@1.32.0))(vitest@4.0.17)': dependencies: - '@vitest/browser': 4.0.17(vite@6.3.5(@types/node@20.19.30))(vitest@4.0.17) - '@vitest/mocker': 4.0.17(vite@6.3.5(@types/node@20.19.30)) + '@vitest/browser': 4.0.17(vite@6.3.5(@types/node@20.19.30)(lightningcss@1.32.0))(vitest@4.0.17) + '@vitest/mocker': 4.0.17(vite@6.3.5(@types/node@20.19.30)(lightningcss@1.32.0)) playwright: 1.53.1 tinyrainbow: 3.0.3 - vitest: 4.0.17(@types/node@20.19.30)(@vitest/browser-playwright@4.0.17) + vitest: 4.0.17(@types/node@20.19.30)(@vitest/browser-playwright@4.0.17)(jsdom@26.1.0)(lightningcss@1.32.0) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@4.0.17(vite@6.3.5(@types/node@20.19.30))(vitest@4.0.17)': + '@vitest/browser@4.0.17(vite@6.3.5(@types/node@20.19.30)(lightningcss@1.32.0))(vitest@4.0.17)': dependencies: - '@vitest/mocker': 4.0.17(vite@6.3.5(@types/node@20.19.30)) + '@vitest/mocker': 4.0.17(vite@6.3.5(@types/node@20.19.30)(lightningcss@1.32.0)) '@vitest/utils': 4.0.17 magic-string: 0.30.21 pixelmatch: 7.1.0 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.0.3 - vitest: 4.0.17(@types/node@20.19.30)(@vitest/browser-playwright@4.0.17) + vitest: 4.0.17(@types/node@20.19.30)(@vitest/browser-playwright@4.0.17)(jsdom@26.1.0)(lightningcss@1.32.0) ws: 8.18.3 transitivePeerDependencies: - bufferutil @@ -7798,16 +8326,16 @@ snapshots: - utf-8-validate - vite - '@vitest/browser@4.0.17(vite@7.3.1(@types/node@20.19.30))(vitest@4.0.17)': + '@vitest/browser@4.0.17(vite@7.3.1(@types/node@20.19.30)(lightningcss@1.32.0))(vitest@4.0.17)': dependencies: - '@vitest/mocker': 4.0.17(vite@7.3.1(@types/node@20.19.30)) + '@vitest/mocker': 4.0.17(vite@7.3.1(@types/node@20.19.30)(lightningcss@1.32.0)) '@vitest/utils': 4.0.17 magic-string: 0.30.21 pixelmatch: 7.1.0 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.0.3 - vitest: 4.0.17(@types/node@20.19.30)(@vitest/browser-playwright@4.0.17) + vitest: 4.0.17(@types/node@20.19.30)(@vitest/browser-playwright@4.0.17)(jsdom@26.1.0)(lightningcss@1.32.0) ws: 8.18.3 transitivePeerDependencies: - bufferutil @@ -7815,7 +8343,7 @@ snapshots: - utf-8-validate - vite - '@vitest/coverage-v8@4.0.17(@vitest/browser@4.0.17(vite@6.3.5(@types/node@20.19.30))(vitest@4.0.17))(vitest@4.0.17)': + '@vitest/coverage-v8@4.0.17(@vitest/browser@4.0.17(vite@6.3.5(@types/node@20.19.30)(lightningcss@1.32.0))(vitest@4.0.17))(vitest@4.0.17)': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.17 @@ -7827,9 +8355,9 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.17(@types/node@20.19.30)(@vitest/browser-playwright@4.0.17) + vitest: 4.0.17(@types/node@20.19.30)(@vitest/browser-playwright@4.0.17)(jsdom@26.1.0)(lightningcss@1.32.0) optionalDependencies: - '@vitest/browser': 4.0.17(vite@6.3.5(@types/node@20.19.30))(vitest@4.0.17) + '@vitest/browser': 4.0.17(vite@6.3.5(@types/node@20.19.30)(lightningcss@1.32.0))(vitest@4.0.17) '@vitest/expect@4.0.17': dependencies: @@ -7840,21 +8368,21 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.17(vite@6.3.5(@types/node@20.19.30))': + '@vitest/mocker@4.0.17(vite@6.3.5(@types/node@20.19.30)(lightningcss@1.32.0))': dependencies: '@vitest/spy': 4.0.17 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.3.5(@types/node@20.19.30) + vite: 6.3.5(@types/node@20.19.30)(lightningcss@1.32.0) - '@vitest/mocker@4.0.17(vite@7.3.1(@types/node@20.19.30))': + '@vitest/mocker@4.0.17(vite@7.3.1(@types/node@20.19.30)(lightningcss@1.32.0))': dependencies: '@vitest/spy': 4.0.17 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@20.19.30) + vite: 7.3.1(@types/node@20.19.30)(lightningcss@1.32.0) '@vitest/pretty-format@4.0.17': dependencies: @@ -7943,6 +8471,9 @@ snapshots: adm-zip@0.5.16: {} + agent-base@7.1.4: + optional: true + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -8488,6 +9019,12 @@ snapshots: dependencies: css-tree: 1.1.3 + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + optional: true + csstype@3.1.0: {} csv-generate@3.4.3: {} @@ -8509,6 +9046,12 @@ snapshots: data-uri-to-buffer@4.0.1: {} + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + optional: true + dataloader@1.4.0: {} debounce@1.2.1: {} @@ -8534,6 +9077,9 @@ snapshots: decamelize@6.0.0: {} + decimal.js@10.6.0: + optional: true + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -8565,6 +9111,8 @@ snapshots: detect-indent@6.1.0: {} + detect-libc@2.1.2: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -9419,6 +9967,11 @@ snapshots: hosted-git-info@2.8.9: {} + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + optional: true + html-escaper@2.0.2: {} htmlparser2@8.0.2: @@ -9430,6 +9983,14 @@ snapshots: http-cache-semantics@4.2.0: {} + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + http-signature@1.2.0: dependencies: assert-plus: 1.0.0 @@ -9441,6 +10002,14 @@ snapshots: quick-lru: 5.1.1 resolve-alpn: 1.2.1 + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + human-id@1.0.2: {} human-signals@1.1.1: {} @@ -9451,6 +10020,11 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + icss-replace-symbols@1.1.0: {} icss-utils@5.1.0(postcss@8.5.6): @@ -9591,6 +10165,9 @@ snapshots: dependencies: isobject: 3.0.1 + is-potential-custom-element-name@1.0.1: + optional: true + is-reference@1.2.1: dependencies: '@types/estree': 1.0.8 @@ -9717,6 +10294,34 @@ snapshots: jsbn@0.1.1: {} + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + optional: true + jsesc@3.0.2: {} jsesc@3.1.0: {} @@ -9819,6 +10424,55 @@ snapshots: transitivePeerDependencies: - supports-color + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lilconfig@2.1.0: {} lines-and-columns@1.2.4: {} @@ -10161,6 +10815,9 @@ snapshots: number-is-nan@1.0.1: {} + nwsapi@2.2.23: + optional: true + oauth-sign@0.9.0: {} object-assign@4.1.1: {} @@ -10639,6 +11296,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + preact-iso@2.3.0(preact-render-to-string@6.6.3(preact@10.27.2))(preact@10.27.2): dependencies: preact: 10.27.2 @@ -10908,6 +11571,33 @@ snapshots: dependencies: glob: 7.2.3 + rolldown-string@0.3.0(rolldown@1.0.0-rc.9): + dependencies: + magic-string: 0.30.21 + optionalDependencies: + rolldown: 1.0.0-rc.9 + + rolldown@1.0.0-rc.9: + dependencies: + '@oxc-project/types': 0.115.0 + '@rolldown/pluginutils': 1.0.0-rc.9 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.9 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.9 + '@rolldown/binding-darwin-x64': 1.0.0-rc.9 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.9 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.9 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.9 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.9 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.9 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.9 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.9 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.9 + rollup-plugin-bundle-size@1.0.3: dependencies: chalk: 1.1.3 @@ -10992,6 +11682,9 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.44.1 fsevents: 2.3.3 + rrweb-cssom@0.8.0: + optional: true + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -11015,6 +11708,11 @@ snapshots: sax@1.4.1: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + optional: true + scheduler@0.23.0: dependencies: loose-envify: 1.4.0 @@ -11332,6 +12030,9 @@ snapshots: picocolors: 1.1.1 stable: 0.1.8 + symbol-tree@3.2.4: + optional: true + term-size@2.2.1: {} terser@5.14.2: @@ -11381,6 +12082,14 @@ snapshots: tinyrainbow@3.0.3: {} + tldts-core@6.1.86: + optional: true + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + optional: true + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -11402,8 +12111,18 @@ snapshots: psl: 1.15.0 punycode: 2.3.1 + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + optional: true + tr46@0.0.3: {} + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + optional: true + trim-newlines@3.0.1: {} ts-api-utils@2.4.0(typescript@5.8.3): @@ -11548,7 +12267,7 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.3.0 - vite@6.3.5(@types/node@20.19.30): + vite@6.3.5(@types/node@20.19.30)(lightningcss@1.32.0): dependencies: esbuild: 0.25.5 fdir: 6.5.0(picomatch@4.0.3) @@ -11559,8 +12278,9 @@ snapshots: optionalDependencies: '@types/node': 20.19.30 fsevents: 2.3.3 + lightningcss: 1.32.0 - vite@7.3.1(@types/node@20.19.30): + vite@7.3.1(@types/node@20.19.30)(lightningcss@1.32.0): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -11571,11 +12291,25 @@ snapshots: optionalDependencies: '@types/node': 20.19.30 fsevents: 2.3.3 + lightningcss: 1.32.0 - vitest@4.0.17(@types/node@20.19.30)(@vitest/browser-playwright@4.0.17): + vite@8.0.0(@types/node@20.19.30)(esbuild@0.27.2): + dependencies: + '@oxc-project/runtime': 0.115.0 + lightningcss: 1.32.0 + picomatch: 4.0.3 + postcss: 8.5.8 + rolldown: 1.0.0-rc.9 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.19.30 + esbuild: 0.27.2 + fsevents: 2.3.3 + + vitest@4.0.17(@types/node@20.19.30)(@vitest/browser-playwright@4.0.17)(jsdom@26.1.0)(lightningcss@1.32.0): dependencies: '@vitest/expect': 4.0.17 - '@vitest/mocker': 4.0.17(vite@7.3.1(@types/node@20.19.30)) + '@vitest/mocker': 4.0.17(vite@7.3.1(@types/node@20.19.30)(lightningcss@1.32.0)) '@vitest/pretty-format': 4.0.17 '@vitest/runner': 4.0.17 '@vitest/snapshot': 4.0.17 @@ -11592,11 +12326,12 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@20.19.30) + vite: 7.3.1(@types/node@20.19.30)(lightningcss@1.32.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.19.30 - '@vitest/browser-playwright': 4.0.17(playwright@1.53.1)(vite@6.3.5(@types/node@20.19.30))(vitest@4.0.17) + '@vitest/browser-playwright': 4.0.17(playwright@1.53.1)(vite@6.3.5(@types/node@20.19.30)(lightningcss@1.32.0))(vitest@4.0.17) + jsdom: 26.1.0 transitivePeerDependencies: - jiti - less @@ -11610,6 +12345,11 @@ snapshots: - tsx - yaml + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + optional: true + watchpack@2.4.0: dependencies: glob-to-regexp: 0.4.1 @@ -11665,6 +12405,23 @@ snapshots: webidl-conversions@3.0.1: {} + webidl-conversions@7.0.0: + optional: true + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + optional: true + + whatwg-mimetype@4.0.0: + optional: true + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + optional: true + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -11759,6 +12516,9 @@ snapshots: xdg-basedir@5.1.0: {} + xml-name-validator@5.0.0: + optional: true + xml2js@0.5.0: dependencies: sax: 1.4.1 @@ -11766,6 +12526,9 @@ snapshots: xmlbuilder@11.0.1: {} + xmlchars@2.2.0: + optional: true + y18n@4.0.3: {} y18n@5.0.8: {} diff --git a/tsconfig.json b/tsconfig.json index 97d52e41b..9ab6101bb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,9 @@ "@preact/signals-react-transform": [ "./packages/react-transform/src/index.ts" ], + "@preact/signals-react-transform-rolldown": [ + "./packages/react-transform-rolldown/src/index.ts" + ], "@preact/signals-agent-vite": ["./packages/vite-plugin/src/index.ts"], "@preact/signals-preact-transform": [ "./packages/preact-transform/src/index.ts" diff --git a/vitest.config.mjs b/vitest.config.mjs index b0a627137..353f9abc3 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -49,6 +49,10 @@ export default defineConfig({ dirname, "./packages/react-transform/dist/signals-transform.mjs" ), + "@preact/signals-react-transform-rolldown": path.join( + dirname, + "./packages/react-transform-rolldown/dist/index.mjs" + ), "@preact/signals-utils": path.join( dirname, "./packages/preact/utils/dist/utils.module.js" @@ -95,6 +99,10 @@ export default defineConfig({ dirname, "./packages/react-transform/src/index.ts" ), + "@preact/signals-react-transform-rolldown": path.join( + dirname, + "./packages/react-transform-rolldown/src/index.ts" + ), "@preact/signals-debug": path.join( dirname, "./packages/debug/src/index.ts" @@ -160,6 +168,7 @@ export default defineConfig({ include: [ "packages/**/dist/**/*.js", "packages/react-transform/src/**/*.ts", + "packages/react-transform-rolldown/src/**/*.ts", ], provider: "v8", reporter: ["text-summary", "lcov"], @@ -169,7 +178,7 @@ export default defineConfig({ { extends: true, test: { - include: ["./packages/**/test/**/*.test.tsx"], + include: ["./packages/**/test/**/*.test.{ts,tsx}"], exclude: [ "./packages/**/test/browser/**/*.test.tsx", "**/node_modules/**",