Skip to content

Commit

Permalink
fix: edge cases for server action
Browse files Browse the repository at this point in the history
  • Loading branch information
himself65 committed Jun 18, 2024
1 parent 81a7bc3 commit e1b49ca
Show file tree
Hide file tree
Showing 12 changed files with 544 additions and 374 deletions.
21 changes: 21 additions & 0 deletions e2e/fixtures/rsc-basic/modules/ai/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "ai",
"type": "module",
"version": "1.0.0",
"description": "Vercel AI mockup",
"exports": {
"./rsc": {
"types": "./src/index.d.ts",
"react-server": "./src/server.js",
"import": "./src/client.js"
}
},
"devDependencies": {
"react-dom": "^18",
"react-server-dom-webpack": "18.3.0-canary-eb33bd747-20240312"
},
"peerDependencies": {
"react": "^18 || ^19"
},
"private": true
}
8 changes: 8 additions & 0 deletions e2e/fixtures/rsc-basic/modules/ai/src/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client';
import { useActions } from './shared.js';

export { useActions };

export function createAI() {
throw new Error('You should not call createAI in the client side');
}
7 changes: 7 additions & 0 deletions e2e/fixtures/rsc-basic/modules/ai/src/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { ReactNode } from 'react';

declare function createAI(
actions: Record<string, any>,
): (props: { children: ReactNode }) => ReactNode;

declare function useActions(): Record<string, any>;
28 changes: 28 additions & 0 deletions e2e/fixtures/rsc-basic/modules/ai/src/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use server';
import { InternalProvider } from './shared.js';
import { jsx } from 'react/jsx-runtime';

async function innerAction({ action, options }, state, ...args) {
'use server';
const result = await action(...args);
// eslint-disable-next-line no-undef
console.log('wrapped action', result);
return result;
}

function wrapAction(action, options) {
return innerAction.bind(null, { action, options });
}

export function createAI(actions) {
const wrappedActions = {};
for (const name in actions) {
wrappedActions[name] = wrapAction(actions[name]);
}
return function AI(props) {
return jsx(InternalProvider, {
actions: wrappedActions,
children: props.children,
});
};
}
19 changes: 19 additions & 0 deletions e2e/fixtures/rsc-basic/modules/ai/src/shared.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client';
import { createContext, useContext } from 'react';
import { jsx } from 'react/jsx-runtime';

const ActionContext = createContext(null);

export function useActions() {
return useContext(ActionContext);
}

export function InternalProvider(props) {
return jsx('div', {
'data-testid': 'ai-internal-provider',
children: jsx(ActionContext.Provider, {
value: props.actions,
children: props.children,
}),
});
}
1 change: 1 addition & 0 deletions e2e/fixtures/rsc-basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"start": "waku start"
},
"dependencies": {
"ai": "link:./modules/ai",
"react": "19.0.0-rc.0",
"react-dom": "19.0.0-rc.0",
"react-server-dom-webpack": "19.0.0-rc.0",
Expand Down
5 changes: 5 additions & 0 deletions e2e/fixtures/rsc-basic/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import { ClientCounter } from './ClientCounter.js';
import { ServerPing } from './ServerPing/index.js';
import { ServerBox } from './Box.js';
import { ServerProvider } from './ServerAction/Server.js';
import { ClientActionsConsumer } from './ServerAction/Client.js';

const App = ({ name }: { name: string }) => {
return (
Expand All @@ -12,6 +14,9 @@ const App = ({ name }: { name: string }) => {
<p data-testid="app-name">{name}</p>
<ClientCounter />
<ServerPing />
<ServerProvider>
<ClientActionsConsumer />
</ServerProvider>
</ServerBox>
);
};
Expand Down
14 changes: 14 additions & 0 deletions e2e/fixtures/rsc-basic/src/components/ServerAction/Client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client';

import { useActions } from 'ai/rsc';
import { useEffect } from 'react';

export const ClientActionsConsumer = () => {
const actions = useActions();
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
globalThis.actions = actions;
}, [actions]);
return <div>{JSON.stringify(Object.keys(actions))}</div>;
};
15 changes: 15 additions & 0 deletions e2e/fixtures/rsc-basic/src/components/ServerAction/Server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { ReactNode } from 'react';
import { createAI } from 'ai/rsc';

const AI = createAI({
actions: {
foo: async () => {
'use server';
return 0;
},
},
});

export function ServerProvider({ children }: { children: ReactNode }) {
return <AI>{children}</AI>;
}
14 changes: 14 additions & 0 deletions e2e/rsc-basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,19 @@ for (const { build, command } of commands) {
page.getByTestId('server-ping').getByTestId('counter'),
).toHaveText('2');
});

test('server action', async ({ page }) => {
await page.goto(`http://localhost:${port}/`);
await expect(page.getByTestId('app-name')).toHaveText('Waku');
await expect(page.getByTestId('ai-internal-provider')).toHaveText(
'["foo"]',
);
const result = await page.evaluate(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return globalThis.actions.foo();
});
expect(result).toBe(0);
});
});
}
13 changes: 12 additions & 1 deletion packages/waku/src/lib/plugins/vite-plugin-rsc-transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,18 @@ import { parseOpts } from '../utils/swc.js';
const collectExportNames = (mod: swc.Module) => {
const exportNames = new Set<string>();
for (const item of mod.body) {
if (item.type === 'ExportDeclaration') {
if (item.type === 'FunctionDeclaration') {
const rscDeclaration = item.body?.stmts[0];
if (
rscDeclaration?.type === 'ExpressionStatement' &&
rscDeclaration.expression.type === 'StringLiteral' &&
rscDeclaration.expression.value === 'use server'
) {
exportNames.add(item.identifier.value);
}
}
// fixme: this might be incorrect, not all exports from server file can be registered as server references
else if (item.type === 'ExportDeclaration') {
if (item.declaration.type === 'FunctionDeclaration') {
exportNames.add(item.declaration.identifier.value);
} else if (item.declaration.type === 'VariableDeclaration') {
Expand Down
Loading

0 comments on commit e1b49ca

Please sign in to comment.