├── .gitignore ├── README.md ├── src ├── index.ts ├── client-transform.test.ts ├── client-transform.ts ├── server-transform.ts └── server-transform.test.ts ├── biome.json ├── .github └── workflows │ └── ci.yaml ├── tsconfig.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | transformed-modules-client.json 3 | transformed-modules-server.json 4 | *.tgz 5 | 6 | dist/ 7 | docs/ 8 | node_modules/ 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # babel-rsc 2 | 3 | NOTE: This repository has been archived. The work in this repository has been added upstream to [unplugin-rsc](https://github.com/jacob-ebey/unplugin-rsc). 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { ClientTransformOptions } from "./client-transform.js"; 2 | import { clientTransform } from "./client-transform.js"; 3 | 4 | import type { ServerTransformOptions } from "./server-transform.js"; 5 | import { serverTransform } from "./server-transform.js"; 6 | 7 | export type { ClientTransformOptions, ServerTransformOptions }; 8 | export { clientTransform, serverTransform }; 9 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.7.1/schema.json", 3 | "files": { 4 | "ignore": [ 5 | "**/dist/**/*", 6 | "**/docs/**/*", 7 | "**/node_modules/**/*", 8 | "package-lock.json" 9 | ] 10 | }, 11 | "organizeImports": { 12 | "enabled": true 13 | }, 14 | "linter": { 15 | "enabled": true, 16 | "rules": { 17 | "recommended": true 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | 5 | env: 6 | CI: true 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Use Node.js 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: 20 18 | 19 | - name: Install dependencies 20 | run: npm ci 21 | 22 | - name: Test 23 | run: npm test 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "include": ["src/**/*"], 4 | "compilerOptions": { 5 | "rootDir": "src", 6 | "outDir": "dist", 7 | "declaration": true, 8 | "declarationMap": true, 9 | "sourceMap": true, 10 | 11 | "lib": ["es2023"], 12 | "target": "es2022", 13 | "module": "Node16", 14 | "moduleResolution": "Node16", 15 | "esModuleInterop": true, 16 | "skipLibCheck": true 17 | }, 18 | "ts-node": { 19 | "transpileOnly": true, 20 | "compilerOptions": { 21 | "inlineSourceMap": true 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-rsc", 3 | "version": "0.0.0", 4 | "description": "Babel utilities for React Server Components.", 5 | "type": "module", 6 | "files": [ 7 | "src/**/*.ts", 8 | "src/**/*.tsx", 9 | "dist/**/*.js", 10 | "dist/**/*.d.ts", 11 | "dist/**/*.*.map", 12 | "!**/*.test.*" 13 | ], 14 | "types": "./dist/index.d.ts", 15 | "main": "dist/index.js", 16 | "exports": { 17 | ".": { 18 | "types": "./dist/index.d.ts", 19 | "default": "./dist/index.js" 20 | } 21 | }, 22 | "scripts": { 23 | "build": "tsc", 24 | "build:watch": "tsc --watch", 25 | "docs": "npx typedoc --out docs src/index.ts", 26 | "fix": "biome check --apply .", 27 | "lint": "biome check .", 28 | "test": "node --no-warnings --enable-source-maps --loader ts-node/esm --test ./src/*.test.*", 29 | "test:watch": "node --no-warnings --enable-source-maps --loader ts-node/esm --watch --test ./src/*.test.*" 30 | }, 31 | "keywords": ["babel", "react", "server", "components"], 32 | "author": "Jacob Ebey ", 33 | "license": "ISC", 34 | "dependencies": { 35 | "@babel/core": "7.24.4", 36 | "@babel/helper-module-imports": "7.24.3" 37 | }, 38 | "devDependencies": { 39 | "@babel/types": "7.24.0", 40 | "@biomejs/biome": "1.7.3", 41 | "@types/babel__core": "7.20.5", 42 | "@types/babel__helper-module-imports": "7.18.3", 43 | "@types/node": "20.12.4", 44 | "ts-node": "10.9.2", 45 | "typedoc": "0.25.7", 46 | "typedoc-plugin-markdown": "3.17.1", 47 | "typescript": "5.3.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/client-transform.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "node:assert/strict"; 2 | import { describe, test } from "node:test"; 3 | 4 | import type { ParseResult } from "@babel/core"; 5 | import { parse } from "@babel/core"; 6 | import _generate from "@babel/generator"; 7 | 8 | import type { ClientTransformOptions } from "./client-transform.js"; 9 | import { clientTransform } from "./client-transform.js"; 10 | 11 | const generate = _generate.default; 12 | const js = String.raw; 13 | 14 | function assertAST( 15 | actual: string | ParseResult, 16 | expected: string | ParseResult, 17 | log?: boolean, 18 | ) { 19 | function generateCode(code: string | ParseResult) { 20 | const ast = typeof code === "string" ? parse(code) : code; 21 | return generate(ast).code; 22 | } 23 | 24 | const actualCode = generateCode(actual); 25 | const expectedCode = generateCode(expected); 26 | 27 | if (log) { 28 | console.log("---------- ACTUAL ----------"); 29 | console.log(actualCode); 30 | console.log("----------------------------"); 31 | } 32 | 33 | assert.deepEqual(actualCode, expectedCode); 34 | } 35 | 36 | const transformOptions: ClientTransformOptions = { 37 | id: (filename, directive) => `${directive}:${filename}`, 38 | importFrom: "mwap/runtime/client", 39 | importServer: "$$server", 40 | }; 41 | 42 | describe("use server replaces modules", () => { 43 | test("with annotated exports", () => { 44 | const ast = parse(js` 45 | "use server"; 46 | import { Imported } from "third-party-imported"; 47 | export { Exported } from "third-party-exported"; 48 | export { Imported }; 49 | export const varDeclaration = "varDeclaration"; 50 | export const functionDeclaration = function functionDeclaration() {}; 51 | export function Component() {} 52 | `); 53 | 54 | clientTransform(ast, "use-server.js", transformOptions); 55 | 56 | assertAST( 57 | ast, 58 | js` 59 | import { $$server as _$$server } from "mwap/runtime/client"; 60 | export const Exported = _$$server({}, "use server:use-server.js", "Exported"); 61 | export const Imported = _$$server({}, "use server:use-server.js", "Imported"); 62 | export const varDeclaration = _$$server({}, "use server:use-server.js", "varDeclaration"); 63 | export const functionDeclaration = _$$server({}, "use server:use-server.js", "functionDeclaration"); 64 | export const Component = _$$server({}, "use server:use-server.js", "Component"); 65 | `, 66 | ); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/client-transform.ts: -------------------------------------------------------------------------------- 1 | import type { NodePath, ParseResult } from "@babel/core"; 2 | import { types as t, traverse } from "@babel/core"; 3 | import { addNamed as addNamedImport } from "@babel/helper-module-imports"; 4 | 5 | export type ClientTransformOptions = { 6 | id(filename: string, directive: "use server"): string; 7 | importFrom: string; 8 | importServer: string; 9 | }; 10 | 11 | export function clientTransform( 12 | ast: ParseResult, 13 | filename: string, 14 | { id: _id, importFrom, importServer }: ClientTransformOptions, 15 | ): void { 16 | const onceCache = new Map(); 17 | function once(key: string, todo: () => T) { 18 | if (onceCache.has(key)) { 19 | return onceCache.get(key) as T; 20 | } 21 | const r = todo(); 22 | onceCache.set(key, r); 23 | return r; 24 | } 25 | 26 | const id = (directive: "use server") => 27 | once(`id:${filename}:${directive}`, () => _id(filename, directive)); 28 | 29 | let programPath: NodePath; 30 | let moduleUseClient = false; 31 | let moduleUseServer = false; 32 | let hasUseServer = false; 33 | const namedExports = new Map(); 34 | 35 | traverse(ast, { 36 | Program(path) { 37 | programPath = path; 38 | 39 | for (const directive of path.node.directives) { 40 | const value = directive.value.value; 41 | switch (value) { 42 | case "use client": 43 | moduleUseClient = true; 44 | break; 45 | case "use server": 46 | hasUseServer = moduleUseServer = true; 47 | break; 48 | } 49 | } 50 | if (moduleUseClient && moduleUseServer) { 51 | throw new Error( 52 | 'Cannot have both "use client" and "use server" in the same module', 53 | ); 54 | } 55 | if (moduleUseServer) { 56 | path.node.directives = path.node.directives.filter( 57 | (d) => d.value.value !== "use server", 58 | ); 59 | } 60 | if (moduleUseClient) { 61 | path.node.directives = path.node.directives.filter( 62 | (d) => d.value.value !== "use client", 63 | ); 64 | } 65 | }, 66 | ExportDefaultDeclaration() { 67 | if (!moduleUseClient) return false; 68 | namedExports.set("default", "default"); 69 | }, 70 | ExportDefaultSpecifier() { 71 | if (!moduleUseClient) return false; 72 | namedExports.set("default", "default"); 73 | }, 74 | ExportNamedDeclaration(path) { 75 | for (const specifier of path.node.specifiers) { 76 | if (t.isExportSpecifier(specifier)) { 77 | const exp = t.isIdentifier(specifier.exported) 78 | ? specifier.exported.name 79 | : specifier.exported.value; 80 | namedExports.set(exp, specifier.local.name); 81 | } 82 | } 83 | 84 | if (t.isVariableDeclaration(path.node.declaration)) { 85 | for (const declaration of path.node.declaration.declarations) { 86 | if (t.isIdentifier(declaration.id)) { 87 | namedExports.set(declaration.id.name, declaration.id.name); 88 | } 89 | } 90 | } else if (t.isFunctionDeclaration(path.node.declaration)) { 91 | if (path.node.declaration.id) { 92 | namedExports.set( 93 | path.node.declaration.id.name, 94 | path.node.declaration.id.name, 95 | ); 96 | } 97 | } 98 | }, 99 | }); 100 | 101 | if (moduleUseClient && hasUseServer) { 102 | throw new Error( 103 | 'Cannot have both "use client" and "use server" in the same module', 104 | ); 105 | } 106 | 107 | if (moduleUseServer) { 108 | ast.program.directives = ast.program.directives.filter( 109 | (d) => d.value.value !== "use server", 110 | ); 111 | ast.program.body = []; 112 | for (const [publicName, localName] of namedExports) { 113 | once(`export:${localName}`, () => { 114 | const toCall = once( 115 | `import { ${importServer} } from "${importFrom}"`, 116 | () => addNamedImport(programPath, importServer, importFrom), 117 | ); 118 | 119 | if (publicName === "default") { 120 | throw new Error( 121 | "Cannot use default export with 'use server' at module scope.", 122 | ); 123 | } 124 | 125 | ast.program.body.push( 126 | t.exportNamedDeclaration( 127 | t.variableDeclaration("const", [ 128 | t.variableDeclarator( 129 | t.identifier(publicName), 130 | t.callExpression(toCall, [ 131 | t.objectExpression([]), 132 | t.stringLiteral(id("use server")), 133 | t.stringLiteral(publicName), 134 | ]), 135 | ), 136 | ]), 137 | ), 138 | ); 139 | }); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/server-transform.ts: -------------------------------------------------------------------------------- 1 | // Adapted from @lubieowoce https://github.com/lubieowoce/tangle/blob/main/packages/babel-rsc/src/babel-rsc-actions.ts 2 | 3 | import type { NodePath, ParseResult } from "@babel/core"; 4 | import { types as t, template, traverse } from "@babel/core"; 5 | import { addNamed as addNamedImport } from "@babel/helper-module-imports"; 6 | 7 | type FnPath = 8 | | NodePath 9 | | NodePath 10 | | NodePath; 11 | 12 | type Scope = NodePath["scope"]; 13 | 14 | export type ServerTransformOptions = { 15 | id(filename: string, directive: "use client" | "use server"): string; 16 | importClient: string; 17 | importFrom: string; 18 | importServer: string; 19 | encryption?: { 20 | importSource: string; 21 | decryptFn: string; 22 | encryptFn: string; 23 | }; 24 | }; 25 | 26 | const LAZY_WRAPPER_VALUE_KEY = "value"; 27 | 28 | // React doesn't like non-enumerable properties on serialized objects (see `isSimpleObject`), 29 | // so we have to use closure scope for the cache (instead of a non-enumerable `this._cache`) 30 | const _buildLazyWrapperHelper = template(`(thunk) => { 31 | let cache = undefined; 32 | return { 33 | get ${LAZY_WRAPPER_VALUE_KEY}() { 34 | if (!cache) { 35 | cache = thunk(); 36 | } 37 | return cache; 38 | } 39 | } 40 | }`); 41 | 42 | const buildLazyWrapperHelper = () => { 43 | return (_buildLazyWrapperHelper({}) as t.ExpressionStatement).expression; 44 | }; 45 | 46 | export function serverTransform( 47 | ast: ParseResult, 48 | filename: string, 49 | { 50 | encryption, 51 | id: _id, 52 | importClient, 53 | importFrom, 54 | importServer, 55 | }: ServerTransformOptions, 56 | ): void { 57 | const onceCache = new Map(); 58 | function once(key: string, todo: () => T) { 59 | if (onceCache.has(key)) { 60 | return onceCache.get(key) as T; 61 | } 62 | const r = todo(); 63 | onceCache.set(key, r); 64 | return r; 65 | } 66 | 67 | let programPath: NodePath; 68 | let moduleUseClient = false; 69 | let moduleUseServer = false; 70 | let hasUseServer = false; 71 | const namedExports = new Map(); 72 | const topLevelFunctions = new Set(); 73 | 74 | const hasUseServerDirective = (path: FnPath) => { 75 | const { body } = path.node; 76 | if (!t.isBlockStatement(body)) { 77 | return false; 78 | } 79 | if ( 80 | !( 81 | body.directives.length >= 1 && 82 | body.directives.some((d) => d.value.value === "use server") 83 | ) 84 | ) { 85 | return false; 86 | } 87 | // remove the use server directive 88 | body.directives = body.directives.filter( 89 | (d) => d.value.value !== "use server", 90 | ); 91 | return true; 92 | }; 93 | 94 | const defineBoundArgsWrapperHelper = () => 95 | once("defineBoundArgsWrapperHelper", () => { 96 | const id = programPath.scope.generateUidIdentifier("wrapBoundArgs"); 97 | programPath.scope.push({ 98 | id, 99 | kind: "var", 100 | init: buildLazyWrapperHelper(), 101 | }); 102 | return id; 103 | }); 104 | 105 | const id = (directive: "use client" | "use server") => 106 | once(`id:${filename}:${directive}`, () => _id(filename, directive)); 107 | 108 | const addCryptImport = (): { 109 | decryptFn: t.Identifier; 110 | encryptFn: t.Identifier; 111 | } | null => { 112 | if (!encryption) return null; 113 | return { 114 | decryptFn: once( 115 | `import { ${encryption.decryptFn} } from "${encryption.importSource}"`, 116 | () => 117 | addNamedImport( 118 | programPath, 119 | encryption.decryptFn, 120 | encryption.importSource, 121 | ), 122 | ), 123 | encryptFn: once( 124 | `import { ${encryption.encryptFn} } from "${encryption.importSource}"`, 125 | () => 126 | addNamedImport( 127 | programPath, 128 | encryption.encryptFn, 129 | encryption.importSource, 130 | ), 131 | ), 132 | }; 133 | }; 134 | 135 | traverse(ast, { 136 | Program(path) { 137 | programPath = path; 138 | 139 | for (const directive of path.node.directives) { 140 | const value = directive.value.value; 141 | switch (value) { 142 | case "use client": 143 | moduleUseClient = true; 144 | break; 145 | case "use server": 146 | hasUseServer = moduleUseServer = true; 147 | break; 148 | } 149 | } 150 | if (moduleUseClient && moduleUseServer) { 151 | throw new Error( 152 | 'Cannot have both "use client" and "use server" in the same module', 153 | ); 154 | } 155 | if (moduleUseServer) { 156 | path.node.directives = path.node.directives.filter( 157 | (d) => d.value.value !== "use server", 158 | ); 159 | } 160 | if (moduleUseClient) { 161 | path.node.directives = path.node.directives.filter( 162 | (d) => d.value.value !== "use client", 163 | ); 164 | } 165 | }, 166 | ExportDefaultDeclaration() { 167 | if (!moduleUseClient) return false; 168 | namedExports.set("default", "default"); 169 | }, 170 | ExportDefaultSpecifier() { 171 | if (!moduleUseClient) return false; 172 | namedExports.set("default", "default"); 173 | }, 174 | ExportNamedDeclaration(path) { 175 | for (const specifier of path.node.specifiers) { 176 | if (t.isExportSpecifier(specifier)) { 177 | const exp = t.isIdentifier(specifier.exported) 178 | ? specifier.exported.name 179 | : specifier.exported.value; 180 | namedExports.set(exp, specifier.local.name); 181 | } 182 | } 183 | 184 | if (t.isVariableDeclaration(path.node.declaration)) { 185 | for (const declaration of path.node.declaration.declarations) { 186 | if (t.isIdentifier(declaration.id)) { 187 | namedExports.set(declaration.id.name, declaration.id.name); 188 | } 189 | } 190 | } else if (t.isFunctionDeclaration(path.node.declaration)) { 191 | if (path.node.declaration.id) { 192 | namedExports.set( 193 | path.node.declaration.id.name, 194 | path.node.declaration.id.name, 195 | ); 196 | } 197 | } 198 | }, 199 | ArrowFunctionExpression(path) { 200 | if (moduleUseClient) return false; 201 | const tlb = getTopLevelBinding(path); 202 | if (tlb && tlb.scope === path.scope.getProgramParent()) { 203 | topLevelFunctions.add(tlb.identifier.name); 204 | } 205 | if (!tlb && hasUseServerDirective(path)) { 206 | const vars = getNonLocalVariables(path); 207 | const { getReplacement } = extractInlineActionToTopLevel(path, { 208 | addCryptImport, 209 | addRSDServerImport() { 210 | return once(`import { ${importServer} } from "${importFrom}"`, () => 211 | addNamedImport(programPath, importServer, importFrom), 212 | ); 213 | }, 214 | id: id("use server"), 215 | vars: Array.from(vars), 216 | wrapBoundArgs(expr) { 217 | const wrapperFn = t.cloneNode(defineBoundArgsWrapperHelper()); 218 | return t.callExpression(wrapperFn, [ 219 | t.arrowFunctionExpression([], expr), 220 | ]); 221 | }, 222 | }); 223 | 224 | path.replaceWith(getReplacement()); 225 | } 226 | }, 227 | FunctionDeclaration(path) { 228 | if (moduleUseClient) return false; 229 | const tlb = getTopLevelBinding(path); 230 | if (tlb && tlb.scope === path.scope.getProgramParent()) { 231 | topLevelFunctions.add(tlb.identifier.name); 232 | } 233 | if (!tlb && hasUseServerDirective(path)) { 234 | const vars = getNonLocalVariables(path); 235 | const { extractedIdentifier, getReplacement } = 236 | extractInlineActionToTopLevel(path, { 237 | addCryptImport, 238 | addRSDServerImport() { 239 | return once( 240 | `import { ${importServer} } from "${importFrom}"`, 241 | () => addNamedImport(programPath, importServer, importFrom), 242 | ); 243 | }, 244 | id: id("use server"), 245 | vars: Array.from(vars), 246 | wrapBoundArgs(expr) { 247 | const wrapperFn = t.cloneNode(defineBoundArgsWrapperHelper()); 248 | return t.callExpression(wrapperFn, [ 249 | t.arrowFunctionExpression([], expr), 250 | ]); 251 | }, 252 | }); 253 | 254 | const tlb = getTopLevelBinding(path); 255 | const fnId = path.node.id; 256 | if (!fnId) { 257 | throw new Error("Expected a function with an id"); 258 | } 259 | if (tlb) { 260 | // we're at the top level, and we might be enclosed within a `export` decl. 261 | // we have to keep the export in place, because it might be used elsewhere, 262 | // so we can't just remove this node. 263 | // replace the function decl with a (hopefully) equivalent var declaration 264 | // `var [name] = $$INLINE_ACTION_{N}` 265 | // TODO: this'll almost certainly break when using default exports, 266 | // but tangle's build doesn't support those anyway 267 | const bindingKind = "var"; 268 | const [inserted] = path.replaceWith( 269 | t.variableDeclaration(bindingKind, [ 270 | t.variableDeclarator(fnId, extractedIdentifier), 271 | ]), 272 | ); 273 | tlb.scope.registerBinding(bindingKind, inserted); 274 | } else { 275 | // note: if we do this *after* adding the new declaration, the bindings get messed up 276 | path.remove(); 277 | // add a declaration in the place where the function decl would be hoisted to. 278 | // (this avoids issues with functions defined after `return`, see `test-cases/named-after-return.jsx`) 279 | path.scope.push({ 280 | id: fnId, 281 | init: getReplacement(), 282 | kind: "var", 283 | unique: true, 284 | }); 285 | } 286 | } 287 | }, 288 | FunctionExpression(path) { 289 | if (moduleUseClient) return false; 290 | const tlb = getTopLevelBinding(path); 291 | if (tlb && tlb.scope === path.scope.getProgramParent()) { 292 | topLevelFunctions.add(tlb.identifier.name); 293 | } 294 | 295 | if (!tlb && hasUseServerDirective(path)) { 296 | const vars = getNonLocalVariables(path); 297 | const { getReplacement } = extractInlineActionToTopLevel(path, { 298 | addCryptImport, 299 | addRSDServerImport() { 300 | return once(`import { ${importServer} } from "${importFrom}"`, () => 301 | addNamedImport(programPath, importServer, importFrom), 302 | ); 303 | }, 304 | id: id("use server"), 305 | vars: Array.from(vars), 306 | wrapBoundArgs(expr) { 307 | const wrapperFn = t.cloneNode(defineBoundArgsWrapperHelper()); 308 | return t.callExpression(wrapperFn, [ 309 | t.arrowFunctionExpression([], expr), 310 | ]); 311 | }, 312 | }); 313 | 314 | path.replaceWith(getReplacement()); 315 | } 316 | }, 317 | }); 318 | 319 | if (moduleUseClient && hasUseServer) { 320 | throw new Error( 321 | 'Cannot have both "use client" and "use server" in the same module', 322 | ); 323 | } 324 | 325 | if (moduleUseServer) { 326 | for (const [publicName, localName] of namedExports) { 327 | if (!topLevelFunctions.has(localName)) { 328 | continue; 329 | } 330 | if (publicName === "default") { 331 | throw new Error( 332 | "Cannot use default export with 'use server' at module scope.", 333 | ); 334 | } 335 | 336 | once(`export:${localName}`, () => { 337 | const toCall = once( 338 | `import { ${importServer} } from "${importFrom}"`, 339 | () => addNamedImport(programPath, importServer, importFrom), 340 | ); 341 | 342 | ast.program.body.push( 343 | t.expressionStatement( 344 | t.callExpression(toCall, [ 345 | t.identifier(localName), 346 | t.stringLiteral(id("use server")), 347 | t.stringLiteral(publicName), 348 | ]), 349 | ), 350 | ); 351 | }); 352 | } 353 | } else if (moduleUseClient) { 354 | ast.program.directives = ast.program.directives.filter( 355 | (d) => d.value.value !== "use client", 356 | ); 357 | ast.program.body = []; 358 | for (const [publicName, localName] of namedExports) { 359 | once(`export:${localName}`, () => { 360 | const toCall = once( 361 | `import { ${importClient} } from "${importFrom}"`, 362 | () => addNamedImport(programPath, importClient, importFrom), 363 | ); 364 | 365 | if (publicName === "default") { 366 | ast.program.body.push( 367 | t.exportDefaultDeclaration( 368 | t.callExpression(toCall, [ 369 | t.objectExpression([]), 370 | t.stringLiteral(id("use client")), 371 | t.stringLiteral(publicName), 372 | ]), 373 | ), 374 | ); 375 | } else { 376 | ast.program.body.push( 377 | t.exportNamedDeclaration( 378 | t.variableDeclaration("const", [ 379 | t.variableDeclarator( 380 | t.identifier(publicName), 381 | t.callExpression(toCall, [ 382 | t.objectExpression([]), 383 | t.stringLiteral(id("use client")), 384 | t.stringLiteral(publicName), 385 | ]), 386 | ), 387 | ]), 388 | ), 389 | ); 390 | } 391 | }); 392 | } 393 | } 394 | } 395 | 396 | function getNonLocalVariables(path: FnPath) { 397 | const nonLocalVariables = new Set(); 398 | const programScope = path.scope.getProgramParent(); 399 | 400 | path.traverse({ 401 | Identifier(identPath) { 402 | const { name } = identPath.node; 403 | if (nonLocalVariables.has(name) || !identPath.isReferencedIdentifier()) { 404 | return; 405 | } 406 | 407 | const binding = identPath.scope.getBinding(name); 408 | if (!binding) { 409 | // probably a global, or an unbound variable. ignore it. 410 | return; 411 | } 412 | if (binding.scope === programScope) { 413 | // module-level declaration. no need to close over it. 414 | return; 415 | } 416 | 417 | if ( 418 | // function args or a var at the top-level of its body 419 | binding.scope === path.scope || 420 | // decls from blocks within the function 421 | isChildScope({ 422 | parent: path.scope, 423 | child: binding.scope, 424 | root: programScope, 425 | }) 426 | ) { 427 | // the binding came from within the function = it's not closed-over, so don't add it. 428 | return; 429 | } 430 | 431 | nonLocalVariables.add(name); 432 | }, 433 | }); 434 | 435 | return nonLocalVariables; 436 | } 437 | 438 | function isChildScope({ 439 | root, 440 | parent, 441 | child, 442 | }: { 443 | root: Scope; 444 | parent: Scope; 445 | child: Scope; 446 | }) { 447 | let curScope = child; 448 | while (curScope !== root) { 449 | if (curScope.parent === parent) { 450 | return true; 451 | } 452 | curScope = curScope.parent; 453 | } 454 | return false; 455 | } 456 | 457 | function findImmediatelyEnclosingDeclaration(path: FnPath) { 458 | let currentPath: NodePath = path; 459 | while (!currentPath.isProgram()) { 460 | if ( 461 | // const foo = async () => { ... } 462 | // ^^^^^^^^^^^^^^^^^^^^^^^^^ 463 | currentPath.isVariableDeclarator() || 464 | // async function foo() { ... } 465 | // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 466 | currentPath.isDeclaration() 467 | ) { 468 | return currentPath; 469 | } 470 | // if we encounter an expression on the way, this isn't a top level decl, and needs to be hoisted. 471 | // e.g. `export const foo = withAuth(async () => { ... })` 472 | if (currentPath !== path && currentPath.isExpression()) { 473 | return null; 474 | } 475 | if (!currentPath.parentPath) { 476 | return null; 477 | } 478 | currentPath = currentPath.parentPath; 479 | } 480 | return null; 481 | } 482 | 483 | function getTopLevelBinding(path: FnPath) { 484 | const decl = findImmediatelyEnclosingDeclaration(path); 485 | if (!decl) { 486 | return null; 487 | } 488 | 489 | if (!("id" in decl.node) || !decl.node.id) { 490 | return null; 491 | } 492 | if (!("name" in decl.node.id)) { 493 | return null; 494 | } 495 | 496 | const declBinding = decl.scope.getBinding(decl.node.id.name); 497 | if (!declBinding) return null; 498 | const isTopLevel = declBinding.scope === path.scope.getProgramParent(); 499 | 500 | return isTopLevel ? declBinding : null; 501 | } 502 | 503 | function extractInlineActionToTopLevel( 504 | path: NodePath< 505 | t.ArrowFunctionExpression | t.FunctionDeclaration | t.FunctionExpression 506 | >, 507 | ctx: { 508 | addCryptImport(): { 509 | decryptFn: t.Identifier; 510 | encryptFn: t.Identifier; 511 | } | null; 512 | addRSDServerImport(): t.Identifier; 513 | id: string; 514 | vars: string[]; 515 | wrapBoundArgs(boundArgs: t.Expression): t.Expression; 516 | }, 517 | ) { 518 | const { addCryptImport, addRSDServerImport, id, vars } = ctx; 519 | 520 | const moduleScope = path.scope.getProgramParent(); 521 | const extractedIdentifier = 522 | moduleScope.generateUidIdentifier("$$INLINE_ACTION"); 523 | 524 | let extractedFunctionParams = [...path.node.params]; 525 | let extractedFunctionBody: t.Statement[] = [path.node.body] as t.Statement[]; 526 | 527 | if (vars.length > 0) { 528 | // only add a closure object if we're not closing over anything. 529 | // const [x, y, z] = await _decryptActionBoundArgs(await $$CLOSURE.value); 530 | 531 | const encryption = addCryptImport(); 532 | 533 | const closureParam = path.scope.generateUidIdentifier("$$CLOSURE"); 534 | const freeVarsPat = t.arrayPattern( 535 | vars.map((variable) => t.identifier(variable)), 536 | ); 537 | 538 | const closureExpr = encryption 539 | ? t.awaitExpression( 540 | t.callExpression(encryption.decryptFn, [ 541 | t.awaitExpression( 542 | t.memberExpression( 543 | closureParam, 544 | t.identifier(LAZY_WRAPPER_VALUE_KEY), 545 | ), 546 | ), 547 | t.stringLiteral(id), 548 | t.stringLiteral(extractedIdentifier.name), 549 | ]), 550 | ) 551 | : t.memberExpression(closureParam, t.identifier(LAZY_WRAPPER_VALUE_KEY)); 552 | 553 | extractedFunctionParams = [closureParam, ...path.node.params]; 554 | extractedFunctionBody = [ 555 | t.variableDeclaration("var", [ 556 | t.variableDeclarator(t.assignmentPattern(freeVarsPat, closureExpr)), 557 | ]), 558 | ...extractedFunctionBody, 559 | ]; 560 | } 561 | 562 | const wrapInRegister = (expr: t.Expression, exportedName: string) => { 563 | const registerServerReferenceId = addRSDServerImport(); 564 | 565 | return t.callExpression(registerServerReferenceId, [ 566 | expr, 567 | t.stringLiteral(id), 568 | t.stringLiteral(exportedName), 569 | ]); 570 | }; 571 | 572 | const extractedFunctionExpr = wrapInRegister( 573 | t.arrowFunctionExpression( 574 | extractedFunctionParams, 575 | t.blockStatement(extractedFunctionBody), 576 | true /* async */, 577 | ), 578 | extractedIdentifier.name, 579 | ); 580 | 581 | // Create a top-level declaration for the extracted function. 582 | const bindingKind = "const"; 583 | const functionDeclaration = t.exportNamedDeclaration( 584 | t.variableDeclaration(bindingKind, [ 585 | t.variableDeclarator(extractedIdentifier, extractedFunctionExpr), 586 | ]), 587 | ); 588 | 589 | // TODO: this is cacheable, no need to recompute 590 | const programBody = moduleScope.path.get("body"); 591 | const lastImportPath = findLast( 592 | Array.isArray(programBody) ? programBody : [programBody], 593 | (stmt) => stmt.isImportDeclaration(), 594 | ); 595 | if (!lastImportPath) { 596 | throw new Error("Could not find last import declaration"); 597 | } 598 | 599 | const [inserted] = lastImportPath.insertAfter(functionDeclaration); 600 | moduleScope.registerBinding(bindingKind, inserted); 601 | 602 | return { 603 | extractedIdentifier, 604 | getReplacement: () => 605 | getInlineActionReplacement(extractedIdentifier, vars, ctx), 606 | }; 607 | } 608 | 609 | function getInlineActionReplacement( 610 | actionId: t.Identifier, 611 | vars: string[], 612 | { 613 | addCryptImport, 614 | id, 615 | wrapBoundArgs, 616 | }: { 617 | addCryptImport(): { 618 | decryptFn: t.Identifier; 619 | encryptFn: t.Identifier; 620 | } | null; 621 | addRSDServerImport(): t.Identifier; 622 | id: string; 623 | wrapBoundArgs(boundArgs: t.Expression): t.Expression; 624 | }, 625 | ) { 626 | if (vars.length === 0) { 627 | return actionId; 628 | } 629 | const encryption = addCryptImport(); 630 | 631 | const capturedVarsExpr = t.arrayExpression( 632 | vars.map((variable) => t.identifier(variable)), 633 | ); 634 | const boundArgs = wrapBoundArgs( 635 | encryption 636 | ? t.callExpression(encryption.encryptFn, [ 637 | capturedVarsExpr, 638 | t.stringLiteral(id), 639 | t.stringLiteral(actionId.name), 640 | ]) 641 | : capturedVarsExpr, 642 | ); 643 | 644 | // _ACTION.bind(null, { get value() { return _encryptActionBoundArgs([x, y, z]) } }) 645 | 646 | return t.callExpression(t.memberExpression(actionId, t.identifier("bind")), [ 647 | t.nullLiteral(), 648 | boundArgs, 649 | ]); 650 | } 651 | 652 | function findLastIndex( 653 | arr: T[], 654 | pred: (el: T) => boolean, 655 | ): number | undefined { 656 | for (let i = arr.length - 1; i >= 0; i--) { 657 | const el = arr[i]; 658 | if (pred(el)) { 659 | return i; 660 | } 661 | } 662 | return undefined; 663 | } 664 | 665 | function findLast(arr: T[], pred: (el: T) => boolean): T | undefined { 666 | const index = findLastIndex(arr, pred); 667 | if (index === undefined) { 668 | return undefined; 669 | } 670 | return arr[index]; 671 | } 672 | -------------------------------------------------------------------------------- /src/server-transform.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "node:assert/strict"; 2 | import { describe, test } from "node:test"; 3 | 4 | import type { ParseResult } from "@babel/core"; 5 | import { parse } from "@babel/core"; 6 | import _generate from "@babel/generator"; 7 | 8 | import type { ServerTransformOptions } from "./server-transform.js"; 9 | import { serverTransform } from "./server-transform.js"; 10 | 11 | const generate = _generate.default; 12 | const js = String.raw; 13 | 14 | function assertAST( 15 | actual: string | ParseResult, 16 | expected: string | ParseResult, 17 | log?: boolean, 18 | ) { 19 | function generateCode(code: string | ParseResult) { 20 | const ast = typeof code === "string" ? parse(code) : code; 21 | return generate(ast).code; 22 | } 23 | 24 | const actualCode = generateCode(actual); 25 | const expectedCode = generateCode(expected); 26 | 27 | if (log) { 28 | console.log("---------- ACTUAL ----------"); 29 | console.log(actualCode); 30 | console.log("----------------------------"); 31 | } 32 | 33 | assert.deepEqual(actualCode, expectedCode); 34 | } 35 | 36 | const transformOptions: ServerTransformOptions = { 37 | id: (filename, directive) => `${directive}:${filename}`, 38 | importClient: "$$client", 39 | importFrom: "mwap/runtime/server", 40 | importServer: "$$server", 41 | }; 42 | 43 | const transformOptionsWithEncryption: ServerTransformOptions = { 44 | ...transformOptions, 45 | encryption: { 46 | importSource: "mwap/runtime/server", 47 | decryptFn: "decrypt", 48 | encryptFn: "encrypt", 49 | }, 50 | }; 51 | 52 | const wrapBoundArgs = js` 53 | var _wrapBoundArgs = thunk => { 54 | let cache = undefined; 55 | return { 56 | get value() { 57 | if (!cache) { 58 | cache = thunk(); 59 | } 60 | return cache; 61 | } 62 | }; 63 | }; 64 | `; 65 | 66 | describe("use client replaces modules", () => { 67 | test("with annotated exports", () => { 68 | const ast = parse(js` 69 | "use client"; 70 | import { Imported } from "third-party-imported"; 71 | export { Exported } from "third-party-exported"; 72 | export { Imported }; 73 | export const varDeclaration = "varDeclaration"; 74 | export const functionDeclaration = function functionDeclaration() {}; 75 | export function Component() {} 76 | export default function DefaultComponent() {} 77 | `); 78 | 79 | serverTransform(ast, "use-client.js", transformOptions); 80 | 81 | assertAST( 82 | ast, 83 | js` 84 | import { $$client as _$$client } from "mwap/runtime/server"; 85 | export const Exported = _$$client({}, "use client:use-client.js", "Exported"); 86 | export const Imported = _$$client({}, "use client:use-client.js", "Imported"); 87 | export const varDeclaration = _$$client({}, "use client:use-client.js", "varDeclaration"); 88 | export const functionDeclaration = _$$client({}, "use client:use-client.js", "functionDeclaration"); 89 | export const Component = _$$client({}, "use client:use-client.js", "Component"); 90 | export default _$$client({}, "use client:use-client.js", "default"); 91 | `, 92 | ); 93 | }); 94 | }); 95 | 96 | describe("use server module arrow functions", () => { 97 | test("annotates direct export arrow function", () => { 98 | const ast = parse(js` 99 | "use server"; 100 | export const sayHello = (a) => { 101 | return "Hello, " + a + "!"; 102 | } 103 | `); 104 | 105 | serverTransform(ast, "use-server.js", transformOptions); 106 | 107 | assertAST( 108 | ast, 109 | js` 110 | import { $$server as _$$server } from "mwap/runtime/server"; 111 | export const sayHello = (a) => { 112 | return "Hello, " + a + "!"; 113 | } 114 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 115 | `, 116 | ); 117 | }); 118 | 119 | test("annotates later export arrow function", () => { 120 | const ast = parse(js` 121 | "use server"; 122 | const sayHello = (a) => { 123 | return "Hello, " + a + "!"; 124 | } 125 | export { sayHello }; 126 | `); 127 | 128 | serverTransform(ast, "use-server.js", transformOptions); 129 | 130 | assertAST( 131 | ast, 132 | js` 133 | import { $$server as _$$server } from "mwap/runtime/server"; 134 | const sayHello = (a) => { 135 | return "Hello, " + a + "!"; 136 | } 137 | export { sayHello }; 138 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 139 | `, 140 | ); 141 | }); 142 | 143 | test("annotates later rename export arrow function", () => { 144 | const ast = parse(js` 145 | "use server"; 146 | const sayHello = (a) => { 147 | return "Hello, " + a + "!"; 148 | } 149 | export { sayHello as sayHello2 }; 150 | `); 151 | 152 | serverTransform(ast, "use-server.js", transformOptions); 153 | 154 | assertAST( 155 | ast, 156 | js` 157 | import { $$server as _$$server } from "mwap/runtime/server"; 158 | const sayHello = (a) => { 159 | return "Hello, " + a + "!"; 160 | } 161 | export { sayHello as sayHello2 }; 162 | _$$server(sayHello, "use server:use-server.js", "sayHello2"); 163 | `, 164 | ); 165 | }); 166 | 167 | test("annotates later rename export of already exported arrow function", () => { 168 | const ast = parse(js` 169 | "use server"; 170 | export const sayHello = (a) => { 171 | return "Hello, " + a + "!"; 172 | }; 173 | export { sayHello as sayHello2 }; 174 | `); 175 | 176 | serverTransform(ast, "use-server.js", transformOptions); 177 | 178 | assertAST( 179 | ast, 180 | js` 181 | import { $$server as _$$server } from "mwap/runtime/server"; 182 | export const sayHello = (a) => { 183 | return "Hello, " + a + "!"; 184 | }; 185 | export { sayHello as sayHello2 }; 186 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 187 | `, 188 | ); 189 | }); 190 | 191 | test("annotates direct export arrow function while ignoring local", () => { 192 | const ast = parse(js` 193 | "use server"; 194 | const sayHelloLocal = (a) => { 195 | return "Hello, " + a + "!"; 196 | }; 197 | export const sayHello = (a) => sayHelloLocal(a); 198 | `); 199 | 200 | serverTransform(ast, "use-server.js", transformOptions); 201 | 202 | assertAST( 203 | ast, 204 | js` 205 | import { $$server as _$$server } from "mwap/runtime/server"; 206 | const sayHelloLocal = (a) => { 207 | return "Hello, " + a + "!"; 208 | }; 209 | export const sayHello = (a) => sayHelloLocal(a); 210 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 211 | `, 212 | ); 213 | }); 214 | 215 | test("annotates direct export arrow function while ignoring function level", () => { 216 | const ast = parse(js` 217 | "use server"; 218 | export const sayHello = (a) => { 219 | const sayHelloLocal = (a) => { 220 | return "Hello, " + a + "!"; 221 | }; 222 | return sayHelloLocal(a); 223 | }; 224 | `); 225 | 226 | serverTransform(ast, "use-server.js", transformOptions); 227 | 228 | assertAST( 229 | ast, 230 | js` 231 | import { $$server as _$$server } from "mwap/runtime/server"; 232 | export const sayHello = (a) => { 233 | const sayHelloLocal = (a) => { 234 | return "Hello, " + a + "!"; 235 | }; 236 | return sayHelloLocal(a); 237 | }; 238 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 239 | `, 240 | ); 241 | }); 242 | }); 243 | 244 | describe("use server module function declarations", () => { 245 | test("annotates direct export function declaration", () => { 246 | const ast = parse(js` 247 | "use server"; 248 | export function sayHello(a) { 249 | return "Hello, " + a + "!"; 250 | } 251 | `); 252 | 253 | serverTransform(ast, "use-server.js", transformOptions); 254 | 255 | assertAST( 256 | ast, 257 | js` 258 | import { $$server as _$$server } from "mwap/runtime/server"; 259 | export function sayHello(a) { 260 | return "Hello, " + a + "!"; 261 | } 262 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 263 | `, 264 | ); 265 | }); 266 | 267 | test("annotates later export function declaration", () => { 268 | const ast = parse(js` 269 | "use server"; 270 | function sayHello(a) { 271 | return "Hello, " + a + "!"; 272 | } 273 | export { sayHello }; 274 | `); 275 | 276 | serverTransform(ast, "use-server.js", transformOptions); 277 | 278 | assertAST( 279 | ast, 280 | js` 281 | import { $$server as _$$server } from "mwap/runtime/server"; 282 | function sayHello(a) { 283 | return "Hello, " + a + "!"; 284 | } 285 | export { sayHello }; 286 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 287 | `, 288 | ); 289 | }); 290 | 291 | test("annotates later rename export of already exported function declaration", () => { 292 | const ast = parse(js` 293 | "use server"; 294 | export function sayHello(a) { 295 | return "Hello, " + a + "!"; 296 | } 297 | export { sayHello as sayHello2 }; 298 | `); 299 | 300 | serverTransform(ast, "use-server.js", transformOptions); 301 | 302 | assertAST( 303 | ast, 304 | js` 305 | import { $$server as _$$server } from "mwap/runtime/server"; 306 | export function sayHello(a) { 307 | return "Hello, " + a + "!"; 308 | } 309 | export { sayHello as sayHello2 }; 310 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 311 | `, 312 | ); 313 | }); 314 | 315 | test("annotates direct export function declaration while ignoring local", () => { 316 | const ast = parse(js` 317 | "use server"; 318 | function sayHelloLocal(a) { 319 | return "Hello, " + a + "!"; 320 | } 321 | export function sayHello(a) { 322 | return sayHelloLocal(a); 323 | } 324 | `); 325 | 326 | serverTransform(ast, "use-server.js", transformOptions); 327 | 328 | assertAST( 329 | ast, 330 | js` 331 | import { $$server as _$$server } from "mwap/runtime/server"; 332 | function sayHelloLocal(a) { 333 | return "Hello, " + a + "!"; 334 | } 335 | export function sayHello(a) { 336 | return sayHelloLocal(a); 337 | } 338 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 339 | `, 340 | ); 341 | }); 342 | 343 | test("annotates direct export function declaration while ignoring function level", () => { 344 | const ast = parse(js` 345 | "use server"; 346 | export function sayHello(a) { 347 | function sayHelloLocal(a) { 348 | return "Hello, " + a + "!"; 349 | }; 350 | return sayHelloLocal(a); 351 | }; 352 | `); 353 | 354 | serverTransform(ast, "use-server.js", transformOptions); 355 | 356 | assertAST( 357 | ast, 358 | js` 359 | import { $$server as _$$server } from "mwap/runtime/server"; 360 | export function sayHello(a) { 361 | function sayHelloLocal(a) { 362 | return "Hello, " + a + "!"; 363 | }; 364 | return sayHelloLocal(a); 365 | }; 366 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 367 | `, 368 | ); 369 | }); 370 | 371 | test("hoists scoped function declaration", () => { 372 | const ast = parse(js` 373 | import * as React from "react"; 374 | export function SayHello({ name }) { 375 | function formAction() { 376 | "use server"; 377 | console.log(name); 378 | } 379 | return React.createElement("button", { formAction }, "Say hello!"); 380 | }; 381 | `); 382 | 383 | serverTransform(ast, "use-server.js", transformOptions); 384 | 385 | assertAST( 386 | ast, 387 | js` 388 | ${wrapBoundArgs} 389 | import { $$server as _$$server } from "mwap/runtime/server"; 390 | import * as React from "react"; 391 | export const _$$INLINE_ACTION = _$$server( 392 | async _$$CLOSURE => { 393 | var [name] = _$$CLOSURE.value; 394 | { 395 | console.log(name); 396 | } 397 | }, 398 | "use server:use-server.js", 399 | "_$$INLINE_ACTION" 400 | ); 401 | export function SayHello({ name }) { 402 | var formAction = _$$INLINE_ACTION.bind( 403 | null, 404 | _wrapBoundArgs(() => [name]) 405 | ); 406 | return React.createElement("button", { formAction }, "Say hello!"); 407 | }; 408 | `, 409 | ); 410 | }); 411 | 412 | test("hoists scoped function declaration with multiple arguments", () => { 413 | const ast = parse(js` 414 | import * as React from "react"; 415 | export function SayHello({ name, age }) { 416 | function formAction() { 417 | "use server"; 418 | console.log(name, age); 419 | } 420 | return React.createElement("button", { formAction }, "Say hello!"); 421 | }; 422 | `); 423 | 424 | serverTransform(ast, "use-server.js", transformOptions); 425 | 426 | assertAST( 427 | ast, 428 | js` 429 | ${wrapBoundArgs} 430 | import { $$server as _$$server } from "mwap/runtime/server"; 431 | import * as React from "react"; 432 | export const _$$INLINE_ACTION = _$$server( 433 | async _$$CLOSURE => { 434 | var [name, age] = _$$CLOSURE.value; 435 | { 436 | console.log(name, age); 437 | } 438 | }, 439 | "use server:use-server.js", 440 | "_$$INLINE_ACTION" 441 | ); 442 | export function SayHello({ name, age }) { 443 | var formAction = _$$INLINE_ACTION.bind( 444 | null, 445 | _wrapBoundArgs(() => [name, age]) 446 | ); 447 | return React.createElement("button", { formAction }, "Say hello!"); 448 | }; 449 | `, 450 | ); 451 | }); 452 | 453 | test("hoists scoped function declaration with argument and closure", () => { 454 | const ast = parse(js` 455 | import * as React from "react"; 456 | export function SayHello({ name, age }) { 457 | function formAction(formData) { 458 | "use server"; 459 | console.log({ name, age, formData }); 460 | } 461 | return React.createElement("button", { formAction }, "Say hello!"); 462 | }; 463 | `); 464 | 465 | serverTransform(ast, "use-server.js", transformOptions); 466 | 467 | assertAST( 468 | ast, 469 | js` 470 | ${wrapBoundArgs} 471 | import { $$server as _$$server } from "mwap/runtime/server"; 472 | import * as React from "react"; 473 | export const _$$INLINE_ACTION = _$$server( 474 | async (_$$CLOSURE, formData) => { 475 | var [name, age] = _$$CLOSURE.value; 476 | { 477 | console.log({ name, age, formData }); 478 | } 479 | }, 480 | "use server:use-server.js", 481 | "_$$INLINE_ACTION" 482 | ); 483 | export function SayHello({ name, age }) { 484 | var formAction = _$$INLINE_ACTION.bind( 485 | null, 486 | _wrapBoundArgs(() => [name, age]) 487 | ); 488 | return React.createElement("button", { formAction }, "Say hello!"); 489 | }; 490 | `, 491 | ); 492 | }); 493 | 494 | test("hoists scoped function declaration with no arguments", () => { 495 | const ast = parse(js` 496 | import * as React from "react"; 497 | export function SayHello() { 498 | function formAction() { 499 | "use server"; 500 | console.log("Hello, world!"); 501 | } 502 | return React.createElement("button", { formAction }, "Say hello!"); 503 | }; 504 | `); 505 | 506 | serverTransform(ast, "use-server.js", transformOptions); 507 | 508 | assertAST( 509 | ast, 510 | js` 511 | import { $$server as _$$server } from "mwap/runtime/server"; 512 | import * as React from "react"; 513 | export const _$$INLINE_ACTION = _$$server( 514 | async () => { 515 | { 516 | console.log("Hello, world!"); 517 | } 518 | }, 519 | "use server:use-server.js", 520 | "_$$INLINE_ACTION" 521 | ); 522 | export function SayHello() { 523 | var formAction = _$$INLINE_ACTION; 524 | return React.createElement("button", { formAction }, "Say hello!"); 525 | }; 526 | `, 527 | ); 528 | }); 529 | 530 | test("hoists multiple scoped function declaration", () => { 531 | const ast = parse(js` 532 | import * as React from "react"; 533 | 534 | export function SayHello({ name, age }) { 535 | return React.createElement( 536 | React.Fragment, 537 | null, 538 | React.createElement( 539 | "button", 540 | { 541 | formAction: () => { 542 | "use server"; 543 | console.log(name); 544 | } 545 | }, 546 | "Say name" 547 | ), 548 | React.createElement( 549 | "button", 550 | { 551 | formAction: () => { 552 | "use server"; 553 | console.log(age); 554 | } 555 | }, 556 | "Say age" 557 | ) 558 | ); 559 | } 560 | `); 561 | 562 | serverTransform(ast, "use-server.js", transformOptions); 563 | 564 | assertAST( 565 | ast, 566 | js` 567 | ${wrapBoundArgs} 568 | import { $$server as _$$server } from "mwap/runtime/server"; 569 | import * as React from "react"; 570 | export const _$$INLINE_ACTION2 = _$$server( 571 | async _$$CLOSURE2 => { 572 | var [age] = _$$CLOSURE2.value; 573 | { 574 | console.log(age); 575 | } 576 | }, 577 | "use server:use-server.js", 578 | "_$$INLINE_ACTION2" 579 | ); 580 | export const _$$INLINE_ACTION = _$$server( 581 | async _$$CLOSURE => { 582 | var [name] = _$$CLOSURE.value; 583 | { 584 | console.log(name); 585 | } 586 | }, 587 | "use server:use-server.js", 588 | "_$$INLINE_ACTION" 589 | ); 590 | export function SayHello({ name, age }) { 591 | return React.createElement( 592 | React.Fragment, 593 | null, 594 | React.createElement("button", { formAction: _$$INLINE_ACTION.bind(null, _wrapBoundArgs(() => [name])) }, "Say name"), 595 | React.createElement("button", { formAction: _$$INLINE_ACTION2.bind(null, _wrapBoundArgs(() => [age])) }, "Say age") 596 | ); 597 | } 598 | `, 599 | ); 600 | }); 601 | }); 602 | 603 | describe("use server module function expressions", () => { 604 | test("annotates direct export function expression", () => { 605 | const ast = parse(js` 606 | "use server"; 607 | export const sayHello = function(a) { 608 | return "Hello, " + a + "!"; 609 | }; 610 | `); 611 | 612 | serverTransform(ast, "use-server.js", transformOptions); 613 | 614 | assertAST( 615 | ast, 616 | js` 617 | import { $$server as _$$server } from "mwap/runtime/server"; 618 | export const sayHello = function(a) { 619 | return "Hello, " + a + "!"; 620 | }; 621 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 622 | `, 623 | ); 624 | }); 625 | 626 | test("annotates later export function expression", () => { 627 | const ast = parse(js` 628 | "use server"; 629 | const sayHello = function(a) { 630 | return "Hello, " + a + "!"; 631 | }; 632 | export { sayHello }; 633 | `); 634 | 635 | serverTransform(ast, "use-server.js", transformOptions); 636 | 637 | assertAST( 638 | ast, 639 | js` 640 | import { $$server as _$$server } from "mwap/runtime/server"; 641 | const sayHello = function(a) { 642 | return "Hello, " + a + "!"; 643 | }; 644 | export { sayHello }; 645 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 646 | `, 647 | ); 648 | }); 649 | 650 | test("annotates later rename export of already exported function expression", () => { 651 | const ast = parse(js` 652 | "use server"; 653 | export const sayHello = function(a) { 654 | return "Hello, " + a + "!"; 655 | }; 656 | export { sayHello as sayHello2 }; 657 | `); 658 | 659 | serverTransform(ast, "use-server.js", transformOptions); 660 | 661 | assertAST( 662 | ast, 663 | js` 664 | import { $$server as _$$server } from "mwap/runtime/server"; 665 | export const sayHello = function(a) { 666 | return "Hello, " + a + "!"; 667 | }; 668 | export { sayHello as sayHello2 }; 669 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 670 | `, 671 | ); 672 | }); 673 | 674 | test("annotates direct export function expression while ignoring local", () => { 675 | const ast = parse(js` 676 | "use server"; 677 | const sayHelloLocal = function(a) { 678 | return "Hello, " + a + "!"; 679 | }; 680 | export const sayHello = function(a) { 681 | return sayHelloLocal(a); 682 | }; 683 | `); 684 | 685 | serverTransform(ast, "use-server.js", transformOptions); 686 | 687 | assertAST( 688 | ast, 689 | js` 690 | import { $$server as _$$server } from "mwap/runtime/server"; 691 | const sayHelloLocal = function(a) { 692 | return "Hello, " + a + "!"; 693 | }; 694 | export const sayHello = function(a) { 695 | return sayHelloLocal(a); 696 | }; 697 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 698 | `, 699 | ); 700 | }); 701 | 702 | test("annotates direct export function expression while ignoring function level", () => { 703 | const ast = parse(js` 704 | "use server"; 705 | export const sayHello = function(a) { 706 | const sayHelloLocal = function(a) { 707 | return "Hello, " + a + "!"; 708 | }; 709 | return sayHelloLocal(a); 710 | }; 711 | `); 712 | 713 | serverTransform(ast, "use-server.js", transformOptions); 714 | 715 | assertAST( 716 | ast, 717 | js` 718 | import { $$server as _$$server } from "mwap/runtime/server"; 719 | export const sayHello = function(a) { 720 | const sayHelloLocal = function(a) { 721 | return "Hello, " + a + "!"; 722 | }; 723 | return sayHelloLocal(a); 724 | }; 725 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 726 | `, 727 | ); 728 | }); 729 | 730 | test("hoists scoped function expression", () => { 731 | const ast = parse(js` 732 | import * as React from "react"; 733 | export const SayHello = function({ name }) { 734 | const formAction = function() { 735 | "use server"; 736 | console.log(name); 737 | } 738 | return React.createElement("button", { formAction }, "Say hello!"); 739 | }; 740 | `); 741 | 742 | serverTransform(ast, "use-server.js", transformOptions); 743 | 744 | assertAST( 745 | ast, 746 | js` 747 | ${wrapBoundArgs} 748 | import { $$server as _$$server } from "mwap/runtime/server"; 749 | import * as React from "react"; 750 | export const _$$INLINE_ACTION = _$$server( 751 | async _$$CLOSURE => { 752 | var [name] = _$$CLOSURE.value; 753 | { 754 | console.log(name); 755 | } 756 | }, 757 | "use server:use-server.js", 758 | "_$$INLINE_ACTION" 759 | ); 760 | export const SayHello = function({ name }) { 761 | const formAction = _$$INLINE_ACTION.bind( 762 | null, 763 | _wrapBoundArgs(() => [name]) 764 | ); 765 | return React.createElement("button", { formAction }, "Say hello!"); 766 | }; 767 | `, 768 | ); 769 | }); 770 | 771 | test("hoists scoped function expression with multiple arguments", () => { 772 | const ast = parse(js` 773 | import * as React from "react"; 774 | export const SayHello = function({ name, age }) { 775 | const formAction = function() { 776 | "use server"; 777 | console.log(name, age); 778 | } 779 | return React.createElement("button", { formAction }, "Say hello!"); 780 | }; 781 | `); 782 | 783 | serverTransform(ast, "use-server.js", transformOptions); 784 | 785 | assertAST( 786 | ast, 787 | js` 788 | ${wrapBoundArgs} 789 | import { $$server as _$$server } from "mwap/runtime/server"; 790 | import * as React from "react"; 791 | export const _$$INLINE_ACTION = _$$server( 792 | async _$$CLOSURE => { 793 | var [name, age] = _$$CLOSURE.value; 794 | { 795 | console.log(name, age); 796 | } 797 | }, 798 | "use server:use-server.js", 799 | "_$$INLINE_ACTION" 800 | ); 801 | export const SayHello = function({ name, age }) { 802 | const formAction = _$$INLINE_ACTION.bind( 803 | null, 804 | _wrapBoundArgs(() => [name, age]) 805 | ); 806 | return React.createElement("button", { formAction }, "Say hello!"); 807 | }; 808 | `, 809 | ); 810 | }); 811 | 812 | test("hoists scoped function expression with no arguments", () => { 813 | const ast = parse(js` 814 | import * as React from "react"; 815 | export const SayHello = function() { 816 | const formAction = function() { 817 | "use server"; 818 | console.log("Hello, world!"); 819 | } 820 | return React.createElement("button", { formAction }, "Say hello!"); 821 | }; 822 | `); 823 | 824 | serverTransform(ast, "use-server.js", transformOptions); 825 | 826 | assertAST( 827 | ast, 828 | js` 829 | import { $$server as _$$server } from "mwap/runtime/server"; 830 | import * as React from "react"; 831 | export const _$$INLINE_ACTION = _$$server( 832 | async () => { 833 | { 834 | console.log("Hello, world!"); 835 | } 836 | }, 837 | "use server:use-server.js", 838 | "_$$INLINE_ACTION" 839 | ); 840 | export const SayHello = function() { 841 | const formAction = _$$INLINE_ACTION; 842 | return React.createElement("button", { formAction }, "Say hello!"); 843 | }; 844 | `, 845 | ); 846 | }); 847 | 848 | test("hoists multiple scoped function expression", () => { 849 | const ast = parse(js` 850 | import * as React from "react"; 851 | 852 | export const SayHello = function({ name, age }) { 853 | return React.createElement( 854 | React.Fragment, 855 | null, 856 | React.createElement( 857 | "button", 858 | { 859 | formAction: function() { 860 | "use server"; 861 | console.log(name); 862 | } 863 | }, 864 | "Say name" 865 | ), 866 | React.createElement( 867 | "button", 868 | { 869 | formAction: function() { 870 | "use server"; 871 | console.log(age); 872 | } 873 | }, 874 | "Say age" 875 | ) 876 | ); 877 | } 878 | `); 879 | 880 | serverTransform(ast, "use-server.js", transformOptions); 881 | 882 | assertAST( 883 | ast, 884 | js` 885 | ${wrapBoundArgs} 886 | import { $$server as _$$server } from "mwap/runtime/server"; 887 | import * as React from "react"; 888 | export const _$$INLINE_ACTION2 = _$$server( 889 | async _$$CLOSURE2 => { 890 | var [age] = _$$CLOSURE2.value; 891 | { 892 | console.log(age); 893 | } 894 | }, 895 | "use server:use-server.js", 896 | "_$$INLINE_ACTION2" 897 | ); 898 | export const _$$INLINE_ACTION = _$$server( 899 | async _$$CLOSURE => { 900 | var [name] = _$$CLOSURE.value; 901 | { 902 | console.log(name); 903 | } 904 | }, 905 | "use server:use-server.js", 906 | "_$$INLINE_ACTION" 907 | ); 908 | export const SayHello = function({ name, age }) { 909 | return React.createElement( 910 | React.Fragment, 911 | null, 912 | React.createElement("button", { formAction: _$$INLINE_ACTION.bind(null, _wrapBoundArgs(() => [name])) }, "Say name"), 913 | React.createElement("button", { formAction: _$$INLINE_ACTION2.bind(null, _wrapBoundArgs(() => [age])) }, "Say age") 914 | ); 915 | } 916 | `, 917 | ); 918 | }); 919 | }); 920 | 921 | describe("use server function arrow functions", () => { 922 | test("annotates direct export arrow function", () => { 923 | const ast = parse(js` 924 | "use server"; 925 | export const sayHello = (a) => { 926 | return "Hello, " + a + "!"; 927 | } 928 | `); 929 | 930 | serverTransform(ast, "use-server.js", transformOptions); 931 | 932 | assertAST( 933 | ast, 934 | js` 935 | import { $$server as _$$server } from "mwap/runtime/server"; 936 | export const sayHello = (a) => { 937 | return "Hello, " + a + "!"; 938 | } 939 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 940 | `, 941 | ); 942 | }); 943 | 944 | test("annotates later export arrow function", () => { 945 | const ast = parse(js` 946 | "use server"; 947 | const sayHello = (a) => { 948 | return "Hello, " + a + "!"; 949 | } 950 | export { sayHello }; 951 | `); 952 | 953 | serverTransform(ast, "use-server.js", transformOptions); 954 | 955 | assertAST( 956 | ast, 957 | js` 958 | import { $$server as _$$server } from "mwap/runtime/server"; 959 | const sayHello = (a) => { 960 | return "Hello, " + a + "!"; 961 | } 962 | export { sayHello }; 963 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 964 | `, 965 | ); 966 | }); 967 | 968 | test("annotates later rename export of already exported arrow function", () => { 969 | const ast = parse(js` 970 | "use server"; 971 | export const sayHello = (a) => { 972 | return "Hello, " + a + "!"; 973 | }; 974 | export { sayHello as sayHello2 }; 975 | `); 976 | 977 | serverTransform(ast, "use-server.js", transformOptions); 978 | 979 | assertAST( 980 | ast, 981 | js` 982 | import { $$server as _$$server } from "mwap/runtime/server"; 983 | export const sayHello = (a) => { 984 | return "Hello, " + a + "!"; 985 | }; 986 | export { sayHello as sayHello2 }; 987 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 988 | `, 989 | ); 990 | }); 991 | 992 | test("annotates direct export arrow function while ignoring local", () => { 993 | const ast = parse(js` 994 | "use server"; 995 | const sayHelloLocal = (a) => { 996 | return "Hello, " + a + "!"; 997 | }; 998 | export const sayHello = (a) => sayHelloLocal(a); 999 | `); 1000 | 1001 | serverTransform(ast, "use-server.js", transformOptions); 1002 | 1003 | assertAST( 1004 | ast, 1005 | js` 1006 | import { $$server as _$$server } from "mwap/runtime/server"; 1007 | const sayHelloLocal = (a) => { 1008 | return "Hello, " + a + "!"; 1009 | }; 1010 | export const sayHello = (a) => sayHelloLocal(a); 1011 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 1012 | `, 1013 | ); 1014 | }); 1015 | 1016 | test("annotates direct export arrow function while ignoring function level", () => { 1017 | const ast = parse(js` 1018 | "use server"; 1019 | export const sayHello = (a) => { 1020 | const sayHelloLocal = (a) => { 1021 | return "Hello, " + a + "!"; 1022 | }; 1023 | return sayHelloLocal(a); 1024 | }; 1025 | `); 1026 | 1027 | serverTransform(ast, "use-server.js", transformOptions); 1028 | 1029 | assertAST( 1030 | ast, 1031 | js` 1032 | import { $$server as _$$server } from "mwap/runtime/server"; 1033 | export const sayHello = (a) => { 1034 | const sayHelloLocal = (a) => { 1035 | return "Hello, " + a + "!"; 1036 | }; 1037 | return sayHelloLocal(a); 1038 | }; 1039 | _$$server(sayHello, "use server:use-server.js", "sayHello"); 1040 | `, 1041 | ); 1042 | }); 1043 | 1044 | test("hoists scoped arrow function", () => { 1045 | const ast = parse(js` 1046 | import * as React from "react"; 1047 | export const SayHello = ({ name }) => { 1048 | const formAction = () => { 1049 | "use server"; 1050 | console.log(name); 1051 | } 1052 | return React.createElement("button", { formAction }, "Say hello!"); 1053 | }; 1054 | `); 1055 | 1056 | serverTransform(ast, "use-server.js", transformOptions); 1057 | 1058 | assertAST( 1059 | ast, 1060 | js` 1061 | ${wrapBoundArgs} 1062 | import { $$server as _$$server } from "mwap/runtime/server"; 1063 | import * as React from "react"; 1064 | export const _$$INLINE_ACTION = _$$server( 1065 | async _$$CLOSURE => { 1066 | var [name] = _$$CLOSURE.value; 1067 | { 1068 | console.log(name); 1069 | } 1070 | }, 1071 | "use server:use-server.js", 1072 | "_$$INLINE_ACTION" 1073 | ); 1074 | export const SayHello = ({ name }) => { 1075 | const formAction = _$$INLINE_ACTION.bind( 1076 | null, 1077 | _wrapBoundArgs(() => [name]) 1078 | ); 1079 | return React.createElement("button", { formAction }, "Say hello!"); 1080 | }; 1081 | `, 1082 | ); 1083 | }); 1084 | 1085 | test("hoists scoped arrow function with multiple arguments", () => { 1086 | const ast = parse(js` 1087 | import * as React from "react"; 1088 | export const SayHello = ({ name, age }) => { 1089 | const formAction = () => { 1090 | "use server"; 1091 | console.log(name, age); 1092 | } 1093 | return React.createElement("button", { formAction }, "Say hello!"); 1094 | }; 1095 | `); 1096 | 1097 | serverTransform(ast, "use-server.js", transformOptions); 1098 | 1099 | assertAST( 1100 | ast, 1101 | js` 1102 | ${wrapBoundArgs} 1103 | import { $$server as _$$server } from "mwap/runtime/server"; 1104 | import * as React from "react"; 1105 | export const _$$INLINE_ACTION = _$$server( 1106 | async _$$CLOSURE => { 1107 | var [name, age] = _$$CLOSURE.value; 1108 | { 1109 | console.log(name, age); 1110 | } 1111 | }, 1112 | "use server:use-server.js", 1113 | "_$$INLINE_ACTION" 1114 | ); 1115 | export const SayHello = ({ name, age }) => { 1116 | const formAction = _$$INLINE_ACTION.bind( 1117 | null, 1118 | _wrapBoundArgs(() => [name, age]) 1119 | ); 1120 | return React.createElement("button", { formAction }, "Say hello!"); 1121 | }; 1122 | `, 1123 | ); 1124 | }); 1125 | 1126 | test("hoists scoped arrow function with no arguments", () => { 1127 | const ast = parse(js` 1128 | import * as React from "react"; 1129 | export const SayHello = () => { 1130 | const formAction = () => { 1131 | "use server"; 1132 | console.log("Hello, world!"); 1133 | } 1134 | return React.createElement("button", { formAction }, "Say hello!"); 1135 | }; 1136 | `); 1137 | 1138 | serverTransform(ast, "use-server.js", transformOptions); 1139 | 1140 | assertAST( 1141 | ast, 1142 | js` 1143 | import { $$server as _$$server } from "mwap/runtime/server"; 1144 | import * as React from "react"; 1145 | export const _$$INLINE_ACTION = _$$server( 1146 | async () => { 1147 | { 1148 | console.log("Hello, world!"); 1149 | } 1150 | }, 1151 | "use server:use-server.js", 1152 | "_$$INLINE_ACTION" 1153 | ); 1154 | export const SayHello = () => { 1155 | const formAction = _$$INLINE_ACTION; 1156 | return React.createElement("button", { formAction }, "Say hello!"); 1157 | }; 1158 | `, 1159 | ); 1160 | }); 1161 | 1162 | test("hoists multiple scoped arrow functions", () => { 1163 | const ast = parse(js` 1164 | import * as React from "react"; 1165 | 1166 | export function SayHello({ name, age }) { 1167 | return React.createElement( 1168 | React.Fragment, 1169 | null, 1170 | React.createElement( 1171 | "button", 1172 | { 1173 | formAction: () => { 1174 | "use server"; 1175 | console.log(name); 1176 | } 1177 | }, 1178 | "Say name" 1179 | ), 1180 | React.createElement( 1181 | "button", 1182 | { 1183 | formAction: () => { 1184 | "use server"; 1185 | console.log(age); 1186 | } 1187 | }, 1188 | "Say age" 1189 | ) 1190 | ); 1191 | } 1192 | `); 1193 | 1194 | serverTransform(ast, "use-server.js", transformOptions); 1195 | 1196 | assertAST( 1197 | ast, 1198 | js` 1199 | ${wrapBoundArgs} 1200 | import { $$server as _$$server } from "mwap/runtime/server"; 1201 | import * as React from "react"; 1202 | export const _$$INLINE_ACTION2 = _$$server( 1203 | async _$$CLOSURE2 => { 1204 | var [age] = _$$CLOSURE2.value; 1205 | { 1206 | console.log(age); 1207 | } 1208 | }, 1209 | "use server:use-server.js", 1210 | "_$$INLINE_ACTION2" 1211 | ); 1212 | export const _$$INLINE_ACTION = _$$server( 1213 | async _$$CLOSURE => { 1214 | var [name] = _$$CLOSURE.value; 1215 | { 1216 | console.log(name); 1217 | } 1218 | }, 1219 | "use server:use-server.js", 1220 | "_$$INLINE_ACTION" 1221 | ); 1222 | export function SayHello({ name, age }) { 1223 | return React.createElement( 1224 | React.Fragment, 1225 | null, 1226 | React.createElement("button", { formAction: _$$INLINE_ACTION.bind(null, _wrapBoundArgs(() => [name])) }, "Say name"), 1227 | React.createElement("button", { formAction: _$$INLINE_ACTION2.bind(null, _wrapBoundArgs(() => [age])) }, "Say age") 1228 | ); 1229 | } 1230 | `, 1231 | ); 1232 | }); 1233 | }); 1234 | 1235 | describe("use server variable encryption", () => { 1236 | test("hoists scoped arrow function", () => { 1237 | const ast = parse(js` 1238 | import * as React from "react"; 1239 | export const SayHello = ({ name }) => { 1240 | const formAction = () => { 1241 | "use server"; 1242 | console.log(name); 1243 | } 1244 | return React.createElement("button", { formAction }, "Say hello!"); 1245 | }; 1246 | `); 1247 | 1248 | serverTransform(ast, "use-server.js", transformOptionsWithEncryption); 1249 | 1250 | assertAST( 1251 | ast, 1252 | js` 1253 | ${wrapBoundArgs} 1254 | import { decrypt as _decrypt, encrypt as _encrypt, $$server as _$$server } from "mwap/runtime/server"; 1255 | import * as React from "react"; 1256 | export const _$$INLINE_ACTION = _$$server(async _$$CLOSURE => { 1257 | var [name] = await _decrypt(await _$$CLOSURE.value, "use server:use-server.js", "_$$INLINE_ACTION"); 1258 | { 1259 | console.log(name); 1260 | } 1261 | }, "use server:use-server.js", "_$$INLINE_ACTION"); 1262 | export const SayHello = ({ 1263 | name 1264 | }) => { 1265 | const formAction = _$$INLINE_ACTION.bind(null, _wrapBoundArgs(() => _encrypt([name], "use server:use-server.js", "_$$INLINE_ACTION"))); 1266 | return React.createElement("button", { 1267 | formAction 1268 | }, "Say hello!"); 1269 | }; 1270 | `, 1271 | ); 1272 | }); 1273 | }); 1274 | --------------------------------------------------------------------------------