From b58649f3419ab6b935cdbbf9c87a0319d205dce6 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Tue, 6 Aug 2024 12:48:17 +0200 Subject: [PATCH 1/4] RSC: Add RSA unit test for module scoped 'use server' --- .../vite-plugin-rsc-transform-server.test.ts | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform-server.test.ts diff --git a/packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform-server.test.ts b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform-server.test.ts new file mode 100644 index 000000000000..b2cd282ea04c --- /dev/null +++ b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform-server.test.ts @@ -0,0 +1,72 @@ +import { vol } from 'memfs' +import { + afterAll, + beforeAll, + describe, + it, + expect, + vi, + afterEach, +} from 'vitest' + +import { rscTransformUseServerPlugin } from '../vite-plugin-rsc-transform-server' + +vi.mock('fs', async () => ({ default: (await import('memfs')).fs })) + +const RWJS_CWD = process.env.RWJS_CWD + +beforeAll(() => { + // Add a toml entry for getPaths et al. + process.env.RWJS_CWD = '/Users/tobbe/rw-app/' + vol.fromJSON( + { + 'redwood.toml': '', + }, + process.env.RWJS_CWD, + ) +}) + +afterAll(() => { + process.env.RWJS_CWD = RWJS_CWD +}) + +describe('rscTransformUseServerPlugin', () => { + afterEach(() => { + vi.resetAllMocks() + }) + + it('should handle module scoped "use server"', async () => { + const plugin = rscTransformUseServerPlugin() + + if (typeof plugin.transform !== 'function') { + expect.fail('Expected plugin to have a transform function') + } + + const id = 'some/path/to/actions.ts' + const input = `'use server' + + import fs from 'node:fs' + + export async function formAction(formData: FormData) { + await fs.promises.writeFile( + 'settings.json', + \`{ "delay": \${formData.get('delay')} }\n\` + ) + }` + + const output = await plugin.transform.bind({})(input, id) + + expect( + output.split('\n').some((line) => line.startsWith('"use server"')), + ).toBeTruthy() + expect(output).toContain( + 'import {registerServerReference} from "react-server-dom-webpack/server";', + ) + expect(output).toContain( + `registerServerReference(formAction,"${id}","formAction");`, + ) + // One import and (exactly) one call to registerServerReference, so two + // matches + expect(output.match(/registerServerReference/g)).toHaveLength(2) + }) +}) From 7dd63adb65107ed77f1767a1a6706234ee3a11ca Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Wed, 7 Aug 2024 10:13:36 +0200 Subject: [PATCH 2/4] Fix TS issues with transform-server test --- .../vite-plugin-rsc-transform-server.test.ts | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform-server.test.ts b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform-server.test.ts index b2cd282ea04c..f67621796643 100644 --- a/packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform-server.test.ts +++ b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform-server.test.ts @@ -1,4 +1,5 @@ import { vol } from 'memfs' +import type { TransformPluginContext } from 'rollup' import { afterAll, beforeAll, @@ -9,39 +10,45 @@ import { afterEach, } from 'vitest' -import { rscTransformUseServerPlugin } from '../vite-plugin-rsc-transform-server' +import { rscTransformUseServerPlugin } from '../vite-plugin-rsc-transform-server.js' vi.mock('fs', async () => ({ default: (await import('memfs')).fs })) const RWJS_CWD = process.env.RWJS_CWD beforeAll(() => { - // Add a toml entry for getPaths et al. process.env.RWJS_CWD = '/Users/tobbe/rw-app/' - vol.fromJSON( - { - 'redwood.toml': '', - }, - process.env.RWJS_CWD, - ) + + // Add a toml entry for getPaths et al. + vol.fromJSON({ 'redwood.toml': '' }, process.env.RWJS_CWD) }) afterAll(() => { process.env.RWJS_CWD = RWJS_CWD }) +function getPluginTransform() { + const plugin = rscTransformUseServerPlugin() + + if (typeof plugin.transform !== 'function') { + throw new Error('Plugin does not have a transform function') + } + + // Calling `bind` to please TS + // See https://stackoverflow.com/a/70463512/88106 + // Typecasting because we're only going to call transform, and we don't need + // anything provided by the context. + return plugin.transform.bind({} as TransformPluginContext) +} + +const pluginTransform = getPluginTransform() + describe('rscTransformUseServerPlugin', () => { afterEach(() => { vi.resetAllMocks() }) it('should handle module scoped "use server"', async () => { - const plugin = rscTransformUseServerPlugin() - - if (typeof plugin.transform !== 'function') { - expect.fail('Expected plugin to have a transform function') - } - const id = 'some/path/to/actions.ts' const input = `'use server' @@ -54,7 +61,11 @@ describe('rscTransformUseServerPlugin', () => { ) }` - const output = await plugin.transform.bind({})(input, id) + const output = await pluginTransform(input, id) + + if (typeof output !== 'string') { + throw new Error('Expected output to be a string') + } expect( output.split('\n').some((line) => line.startsWith('"use server"')), From 759e7b0fdbbd8262723c025e86d9705474eedef3 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Wed, 7 Aug 2024 12:42:54 +0200 Subject: [PATCH 3/4] More transform-server tests --- .../vite-plugin-rsc-transform-server.test.ts | 285 +++++++++++++++++- .../vite-plugin-rsc-transform-server.ts | 24 +- 2 files changed, 291 insertions(+), 18 deletions(-) diff --git a/packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform-server.test.ts b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform-server.test.ts index f67621796643..f487c8cfabf1 100644 --- a/packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform-server.test.ts +++ b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform-server.test.ts @@ -43,14 +43,15 @@ function getPluginTransform() { const pluginTransform = getPluginTransform() -describe('rscTransformUseServerPlugin', () => { +describe('rscTransformUseServerPlugin module scoped "use server"', () => { afterEach(() => { vi.resetAllMocks() }) - it('should handle module scoped "use server"', async () => { + it('should handle one function', async () => { const id = 'some/path/to/actions.ts' - const input = `'use server' + const input = ` + 'use server' import fs from 'node:fs' @@ -67,8 +68,24 @@ describe('rscTransformUseServerPlugin', () => { throw new Error('Expected output to be a string') } + // Check that the file has a "use server" directive at the top + // Comments and other directives are allowed before it. + // Maybe also imports, I'm not sure, but am going to allow it for now. If + // someone finds a problem with that, we can revisit. + const outputLines = output.split('\n') + const firstCodeLineIndex = outputLines.findIndex( + (line) => + line.startsWith('export ') || + line.startsWith('async ') || + line.startsWith('function ') || + line.startsWith('const ') || + line.startsWith('let ') || + line.startsWith('var '), + ) expect( - output.split('\n').some((line) => line.startsWith('"use server"')), + outputLines + .slice(0, firstCodeLineIndex) + .some((line) => line.startsWith('"use server"')), ).toBeTruthy() expect(output).toContain( 'import {registerServerReference} from "react-server-dom-webpack/server";', @@ -80,4 +97,264 @@ describe('rscTransformUseServerPlugin', () => { // matches expect(output.match(/registerServerReference/g)).toHaveLength(2) }) + + it('should handle two functions', async () => { + const id = 'some/path/to/actions.ts' + const input = ` + 'use server' + + import fs from 'node:fs' + + export async function formAction1(formData: FormData) { + await fs.promises.writeFile( + 'settings.json', + \`{ "delay": \${formData.get('delay')} }\n\` + ) + } + + export async function formAction2(formData: FormData) { + await fs.promises.writeFile( + 'settings.json', + \`{ "delay": \${formData.get('delay')} }\n\` + ) + }` + + const output = await pluginTransform(input, id) + + if (typeof output !== 'string') { + throw new Error('Expected output to be a string') + } + + // Check that the file has a "use server" directive at the top + // Comments and other directives are allowed before it. + // Maybe also imports, I'm not sure, but am going to allow it for now. If + // someone finds a problem with that, we can revisit. + const outputLines = output.split('\n') + const firstCodeLineIndex = outputLines.findIndex( + (line) => + line.startsWith('export ') || + line.startsWith('async ') || + line.startsWith('function ') || + line.startsWith('const ') || + line.startsWith('let ') || + line.startsWith('var '), + ) + expect( + outputLines + .slice(0, firstCodeLineIndex) + .some((line) => line.startsWith('"use server"')), + ).toBeTruthy() + expect(output).toContain( + 'import {registerServerReference} from "react-server-dom-webpack/server";', + ) + expect(output).toContain( + `registerServerReference(formAction1,"${id}","formAction1");`, + ) + expect(output).toContain( + `registerServerReference(formAction2,"${id}","formAction2");`, + ) + // One import and (exactly) two calls to registerServerReference, so three + // matches + expect(output.match(/registerServerReference/g)).toHaveLength(3) + }) + + it('should handle arrow function', async () => { + const id = 'some/path/to/actions.ts' + const input = ` + 'use server' + + import fs from 'node:fs' + + export const formAction = async (formData: FormData) => { + await fs.promises.writeFile( + 'settings.json', + \`{ "delay": \${formData.get('delay')} }\n\` + ) + }` + + const output = await pluginTransform(input, id) + + if (typeof output !== 'string') { + throw new Error('Expected output to be a string') + } + + // Check that the file has a "use server" directive at the top + // Comments and other directives are allowed before it. + // Maybe also imports, I'm not sure, but am going to allow it for now. If + // someone finds a problem with that, we can revisit. + const outputLines = output.split('\n') + const firstCodeLineIndex = outputLines.findIndex( + (line) => + line.startsWith('export ') || + line.startsWith('async ') || + line.startsWith('function ') || + line.startsWith('const ') || + line.startsWith('let ') || + line.startsWith('var '), + ) + expect( + outputLines + .slice(0, firstCodeLineIndex) + .some((line) => line.startsWith('"use server"')), + ).toBeTruthy() + expect(output).toContain( + 'import {registerServerReference} from "react-server-dom-webpack/server";', + ) + expect(output).toContain( + `registerServerReference(formAction,"${id}","formAction");`, + ) + // One import and (exactly) one call to registerServerReference, so two + // matches + expect(output.match(/registerServerReference/g)).toHaveLength(2) + }) + + it('should handle default exported arrow function', async () => { + const id = 'some/path/to/actions.ts' + const input = ` + 'use server' + + import fs from 'node:fs' + + export const formAction = async (formData: FormData) => { + await fs.promises.writeFile( + 'settings.json', + \`{ "delay": \${formData.get('delay')} }\n\` + ) + }` + + const output = await pluginTransform(input, id) + + if (typeof output !== 'string') { + throw new Error('Expected output to be a string') + } + + // Check that the file has a "use server" directive at the top + // Comments and other directives are allowed before it. + // Maybe also imports, I'm not sure, but am going to allow it for now. If + // someone finds a problem with that, we can revisit. + const outputLines = output.split('\n') + const firstCodeLineIndex = outputLines.findIndex( + (line) => + line.startsWith('export ') || + line.startsWith('async ') || + line.startsWith('function ') || + line.startsWith('const ') || + line.startsWith('let ') || + line.startsWith('var '), + ) + expect( + outputLines + .slice(0, firstCodeLineIndex) + .some((line) => line.startsWith('"use server"')), + ).toBeTruthy() + expect(output).toContain( + 'import {registerServerReference} from "react-server-dom-webpack/server";', + ) + expect(output).toContain( + `registerServerReference(formAction,"${id}","formAction");`, + ) + // One import and (exactly) one call to registerServerReference, so two + // matches + expect(output.match(/registerServerReference/g)).toHaveLength(2) + }) + + it('should handle default exported named function', async () => { + const id = 'some/path/to/actions.ts' + const input = ` + 'use server' + + import fs from 'node:fs' + + export default async function formAction(formData: FormData) { + await fs.promises.writeFile( + 'settings.json', + \`{ "delay": \${formData.get('delay')} }\n\` + ) + }` + + const output = await pluginTransform(input, id) + + if (typeof output !== 'string') { + throw new Error('Expected output to be a string') + } + + // Check that the file has a "use server" directive at the top + // Comments and other directives are allowed before it. + // Maybe also imports, I'm not sure, but am going to allow it for now. If + // someone finds a problem with that, we can revisit. + const outputLines = output.split('\n') + const firstCodeLineIndex = outputLines.findIndex( + (line) => + line.startsWith('export ') || + line.startsWith('async ') || + line.startsWith('function ') || + line.startsWith('const ') || + line.startsWith('let ') || + line.startsWith('var '), + ) + expect( + outputLines + .slice(0, firstCodeLineIndex) + .some((line) => line.startsWith('"use server"')), + ).toBeTruthy() + expect(output).toContain( + 'import {registerServerReference} from "react-server-dom-webpack/server";', + ) + expect(output).toContain( + `registerServerReference(formAction,"${id}","default");`, + ) + // One import and (exactly) one call to registerServerReference, so two + // matches + expect(output.match(/registerServerReference/g)).toHaveLength(2) + }) + + it.todo('should handle default exported anonymous function', async () => { + const id = 'some/path/to/actions.ts' + const input = ` + 'use server' + + import fs from 'node:fs' + + export default async function (formData: FormData) { + await fs.promises.writeFile( + 'settings.json', + \`{ "delay": \${formData.get('delay')} }\n\` + ) + }` + + const output = await pluginTransform(input, id) + + if (typeof output !== 'string') { + throw new Error('Expected output to be a string') + } + + // Check that the file has a "use server" directive at the top + // Comments and other directives are allowed before it. + // Maybe also imports, I'm not sure, but am going to allow it for now. If + // someone finds a problem with that, we can revisit. + const outputLines = output.split('\n') + const firstCodeLineIndex = outputLines.findIndex( + (line) => + line.startsWith('export ') || + line.startsWith('async ') || + line.startsWith('function ') || + line.startsWith('const ') || + line.startsWith('let ') || + line.startsWith('var '), + ) + expect( + outputLines + .slice(0, firstCodeLineIndex) + .some((line) => line.startsWith('"use server"')), + ).toBeTruthy() + expect(output).toContain( + 'import {registerServerReference} from "react-server-dom-webpack/server";', + ) + expect(output).toContain( + `registerServerReference(formAction,"${id}","default");`, + ) + // One import and (exactly) one call to registerServerReference, so two + // matches + expect(output.match(/registerServerReference/g)).toHaveLength(2) + }) }) diff --git a/packages/vite/src/plugins/vite-plugin-rsc-transform-server.ts b/packages/vite/src/plugins/vite-plugin-rsc-transform-server.ts index b1a1aed6d44b..5284acdcf218 100644 --- a/packages/vite/src/plugins/vite-plugin-rsc-transform-server.ts +++ b/packages/vite/src/plugins/vite-plugin-rsc-transform-server.ts @@ -30,11 +30,9 @@ export function rscTransformUseServerPlugin(): Plugin { let useClient = false let useServer = false - for (let i = 0; i < body.length; i++) { - const node = body[i] - + for (const node of body) { if (node.type !== 'ExpressionStatement' || !node.directive) { - break + continue } if (node.directive === 'use client') { @@ -46,17 +44,17 @@ export function rscTransformUseServerPlugin(): Plugin { } } - if (!useServer) { - return code - } - if (useClient && useServer) { throw new Error( 'Cannot have both "use client" and "use server" directives in the same file.', ) } - const transformedCode = transformServerModule(body, id, code) + let transformedCode = '' + + if (useServer) { + transformedCode = transformServerModule(body, id, code) + } return transformedCode }, @@ -119,9 +117,7 @@ function transformServerModule( const localNames = new Map() const localTypes = new Map() - for (let i = 0; i < body.length; i++) { - const node = body[i] - + for (const node of body) { switch (node.type) { case 'ExportAllDeclaration': // If export * is used, the other file needs to explicitly opt into "use server" too. @@ -137,7 +133,7 @@ function transformServerModule( } } - continue + break case 'ExportNamedDeclaration': if (node.declaration) { @@ -173,7 +169,7 @@ function transformServerModule( } } - continue + break } } From 2acd7897674dbf539c8dc21b15e3b45ab030e713 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Wed, 7 Aug 2024 14:26:51 +0200 Subject: [PATCH 4/4] Switch to inline snapshots --- .../vite-plugin-rsc-transform-server.test.ts | 271 +++++++----------- .../vite-plugin-rsc-transform-server.ts | 3 +- 2 files changed, 99 insertions(+), 175 deletions(-) diff --git a/packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform-server.test.ts b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform-server.test.ts index f487c8cfabf1..b42f5dd0b02e 100644 --- a/packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform-server.test.ts +++ b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform-server.test.ts @@ -58,44 +58,28 @@ describe('rscTransformUseServerPlugin module scoped "use server"', () => { export async function formAction(formData: FormData) { await fs.promises.writeFile( 'settings.json', - \`{ "delay": \${formData.get('delay')} }\n\` + \`{ "delay": \${formData.get('delay')} }\` ) - }` + }`.trim() const output = await pluginTransform(input, id) - if (typeof output !== 'string') { - throw new Error('Expected output to be a string') - } + expect(output).toMatchInlineSnapshot(` + "'use server' - // Check that the file has a "use server" directive at the top - // Comments and other directives are allowed before it. - // Maybe also imports, I'm not sure, but am going to allow it for now. If - // someone finds a problem with that, we can revisit. - const outputLines = output.split('\n') - const firstCodeLineIndex = outputLines.findIndex( - (line) => - line.startsWith('export ') || - line.startsWith('async ') || - line.startsWith('function ') || - line.startsWith('const ') || - line.startsWith('let ') || - line.startsWith('var '), - ) - expect( - outputLines - .slice(0, firstCodeLineIndex) - .some((line) => line.startsWith('"use server"')), - ).toBeTruthy() - expect(output).toContain( - 'import {registerServerReference} from "react-server-dom-webpack/server";', - ) - expect(output).toContain( - `registerServerReference(formAction,"${id}","formAction");`, - ) - // One import and (exactly) one call to registerServerReference, so two - // matches - expect(output.match(/registerServerReference/g)).toHaveLength(2) + import fs from 'node:fs' + + export async function formAction(formData: FormData) { + await fs.promises.writeFile( + 'settings.json', + \`{ "delay": \${formData.get('delay')} }\` + ) + } + + import {registerServerReference} from "react-server-dom-webpack/server"; + registerServerReference(formAction,"some/path/to/actions.ts","formAction"); + " + `) }) it('should handle two functions', async () => { @@ -108,54 +92,43 @@ describe('rscTransformUseServerPlugin module scoped "use server"', () => { export async function formAction1(formData: FormData) { await fs.promises.writeFile( 'settings.json', - \`{ "delay": \${formData.get('delay')} }\n\` + \`{ "delay": \${formData.get('delay')} }\` ) } export async function formAction2(formData: FormData) { await fs.promises.writeFile( 'settings.json', - \`{ "delay": \${formData.get('delay')} }\n\` + \`{ "delay": \${formData.get('delay')} }\` ) - }` + }`.trim() const output = await pluginTransform(input, id) - if (typeof output !== 'string') { - throw new Error('Expected output to be a string') - } - - // Check that the file has a "use server" directive at the top - // Comments and other directives are allowed before it. - // Maybe also imports, I'm not sure, but am going to allow it for now. If - // someone finds a problem with that, we can revisit. - const outputLines = output.split('\n') - const firstCodeLineIndex = outputLines.findIndex( - (line) => - line.startsWith('export ') || - line.startsWith('async ') || - line.startsWith('function ') || - line.startsWith('const ') || - line.startsWith('let ') || - line.startsWith('var '), - ) - expect( - outputLines - .slice(0, firstCodeLineIndex) - .some((line) => line.startsWith('"use server"')), - ).toBeTruthy() - expect(output).toContain( - 'import {registerServerReference} from "react-server-dom-webpack/server";', - ) - expect(output).toContain( - `registerServerReference(formAction1,"${id}","formAction1");`, - ) - expect(output).toContain( - `registerServerReference(formAction2,"${id}","formAction2");`, - ) - // One import and (exactly) two calls to registerServerReference, so three - // matches - expect(output.match(/registerServerReference/g)).toHaveLength(3) + expect(output).toMatchInlineSnapshot(` + "'use server' + + import fs from 'node:fs' + + export async function formAction1(formData: FormData) { + await fs.promises.writeFile( + 'settings.json', + \`{ "delay": \${formData.get('delay')} }\` + ) + } + + export async function formAction2(formData: FormData) { + await fs.promises.writeFile( + 'settings.json', + \`{ "delay": \${formData.get('delay')} }\` + ) + } + + import {registerServerReference} from "react-server-dom-webpack/server"; + registerServerReference(formAction1,"some/path/to/actions.ts","formAction1"); + registerServerReference(formAction2,"some/path/to/actions.ts","formAction2"); + " + `) }) it('should handle arrow function', async () => { @@ -168,144 +141,96 @@ describe('rscTransformUseServerPlugin module scoped "use server"', () => { export const formAction = async (formData: FormData) => { await fs.promises.writeFile( 'settings.json', - \`{ "delay": \${formData.get('delay')} }\n\` + \`{ "delay": \${formData.get('delay')} }\` ) - }` + }`.trim() const output = await pluginTransform(input, id) - if (typeof output !== 'string') { - throw new Error('Expected output to be a string') - } + expect(output).toMatchInlineSnapshot(` + "'use server' - // Check that the file has a "use server" directive at the top - // Comments and other directives are allowed before it. - // Maybe also imports, I'm not sure, but am going to allow it for now. If - // someone finds a problem with that, we can revisit. - const outputLines = output.split('\n') - const firstCodeLineIndex = outputLines.findIndex( - (line) => - line.startsWith('export ') || - line.startsWith('async ') || - line.startsWith('function ') || - line.startsWith('const ') || - line.startsWith('let ') || - line.startsWith('var '), - ) - expect( - outputLines - .slice(0, firstCodeLineIndex) - .some((line) => line.startsWith('"use server"')), - ).toBeTruthy() - expect(output).toContain( - 'import {registerServerReference} from "react-server-dom-webpack/server";', - ) - expect(output).toContain( - `registerServerReference(formAction,"${id}","formAction");`, - ) - // One import and (exactly) one call to registerServerReference, so two - // matches - expect(output.match(/registerServerReference/g)).toHaveLength(2) + import fs from 'node:fs' + + export const formAction = async (formData: FormData) => { + await fs.promises.writeFile( + 'settings.json', + \`{ "delay": \${formData.get('delay')} }\` + ) + } + + import {registerServerReference} from "react-server-dom-webpack/server"; + if (typeof formAction === "function") registerServerReference(formAction,"some/path/to/actions.ts","formAction"); + " + `) }) - it('should handle default exported arrow function', async () => { + it.todo('should handle default exported arrow function', async () => { const id = 'some/path/to/actions.ts' const input = ` 'use server' import fs from 'node:fs' - export const formAction = async (formData: FormData) => { + export default async (formData: FormData) => { await fs.promises.writeFile( 'settings.json', - \`{ "delay": \${formData.get('delay')} }\n\` + \`{ "delay": \${formData.get('delay')} }\` ) - }` + }`.trim() const output = await pluginTransform(input, id) - if (typeof output !== 'string') { - throw new Error('Expected output to be a string') - } + expect(output).toMatchInlineSnapshot(` + "'use server' - // Check that the file has a "use server" directive at the top - // Comments and other directives are allowed before it. - // Maybe also imports, I'm not sure, but am going to allow it for now. If - // someone finds a problem with that, we can revisit. - const outputLines = output.split('\n') - const firstCodeLineIndex = outputLines.findIndex( - (line) => - line.startsWith('export ') || - line.startsWith('async ') || - line.startsWith('function ') || - line.startsWith('const ') || - line.startsWith('let ') || - line.startsWith('var '), - ) - expect( - outputLines - .slice(0, firstCodeLineIndex) - .some((line) => line.startsWith('"use server"')), - ).toBeTruthy() - expect(output).toContain( - 'import {registerServerReference} from "react-server-dom-webpack/server";', - ) - expect(output).toContain( - `registerServerReference(formAction,"${id}","formAction");`, - ) - // One import and (exactly) one call to registerServerReference, so two - // matches - expect(output.match(/registerServerReference/g)).toHaveLength(2) + import fs from 'node:fs' + + export default async (formData: FormData) => { + await fs.promises.writeFile( + 'settings.json', + \`{ "delay": \${formData.get('delay')} }\` + ) + } + + import {registerServerReference} from "react-server-dom-webpack/server"; + registerServerReference(default,"some/path/to/actions.ts","default"); + " + `) }) it('should handle default exported named function', async () => { const id = 'some/path/to/actions.ts' const input = ` - 'use server' + "use server" import fs from 'node:fs' export default async function formAction(formData: FormData) { await fs.promises.writeFile( 'settings.json', - \`{ "delay": \${formData.get('delay')} }\n\` + \`{ "delay": \${formData.get('delay')} }\` ) - }` + }`.trim() const output = await pluginTransform(input, id) - if (typeof output !== 'string') { - throw new Error('Expected output to be a string') - } + expect(output).toMatchInlineSnapshot(` + ""use server" - // Check that the file has a "use server" directive at the top - // Comments and other directives are allowed before it. - // Maybe also imports, I'm not sure, but am going to allow it for now. If - // someone finds a problem with that, we can revisit. - const outputLines = output.split('\n') - const firstCodeLineIndex = outputLines.findIndex( - (line) => - line.startsWith('export ') || - line.startsWith('async ') || - line.startsWith('function ') || - line.startsWith('const ') || - line.startsWith('let ') || - line.startsWith('var '), - ) - expect( - outputLines - .slice(0, firstCodeLineIndex) - .some((line) => line.startsWith('"use server"')), - ).toBeTruthy() - expect(output).toContain( - 'import {registerServerReference} from "react-server-dom-webpack/server";', - ) - expect(output).toContain( - `registerServerReference(formAction,"${id}","default");`, - ) - // One import and (exactly) one call to registerServerReference, so two - // matches - expect(output.match(/registerServerReference/g)).toHaveLength(2) + import fs from 'node:fs' + + export default async function formAction(formData: FormData) { + await fs.promises.writeFile( + 'settings.json', + \`{ "delay": \${formData.get('delay')} }\` + ) + } + + import {registerServerReference} from "react-server-dom-webpack/server"; + registerServerReference(formAction,"some/path/to/actions.ts","default"); + " + `) }) it.todo('should handle default exported anonymous function', async () => { diff --git a/packages/vite/src/plugins/vite-plugin-rsc-transform-server.ts b/packages/vite/src/plugins/vite-plugin-rsc-transform-server.ts index 5284acdcf218..9b6cc64906c0 100644 --- a/packages/vite/src/plugins/vite-plugin-rsc-transform-server.ts +++ b/packages/vite/src/plugins/vite-plugin-rsc-transform-server.ts @@ -50,7 +50,7 @@ export function rscTransformUseServerPlugin(): Plugin { ) } - let transformedCode = '' + let transformedCode = code if (useServer) { transformedCode = transformServerModule(body, id, code) @@ -174,7 +174,6 @@ function transformServerModule( } let newSrc = - '"use server"\n' + code + '\n\n' + 'import {registerServerReference} from ' +