Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion packages/plugin-rsc/src/transforms/hoist.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from 'node:path'
import { parseAstAsync } from 'vite'
import { describe, expect, it } from 'vitest'
import { transformHoistInlineDirective } from './hoist'
import { findDirectives, transformHoistInlineDirective } from './hoist'

describe('fixtures', () => {
const fixtures = import.meta.glob(
Expand Down Expand Up @@ -128,6 +128,43 @@ async function f() {
expect(await testTransform(input)).toMatchInlineSnapshot(`undefined`)
})

it('ignores strings outside a function directive prologue', async () => {
const input = `
async function initialized() {
initialize();
"use server";
}

async function parenthesized() {
("use server");
}
`
expect(await testTransform(input)).toBeUndefined()
})

it('recognizes a directive after another prologue directive', async () => {
const input = `
async function action() {
"use strict";
"use server";
}
`
expect(await testTransformNames(input)).toEqual(['$$hoist_0_action'])
})

it('finds directives only in directive-capable bodies', async () => {
const input = `
{
"use server";
}
async function action() {
"use server";
}
`
const ast = await parseAstAsync(input)
expect(findDirectives(ast, 'use server')).toHaveLength(1)
})

it('top level', async () => {
const input = `
const x = "x";
Expand Down
22 changes: 8 additions & 14 deletions packages/plugin-rsc/src/transforms/hoist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { walk } from 'estree-walker'
import MagicString from 'magic-string'
import type { ESTree } from 'vite'
import { buildScopeTree, type ScopeTree } from './scope'
import { isDirective } from './utils'

/**
* Turns an inline directive function into a module-level registered function.
Expand Down Expand Up @@ -263,11 +264,7 @@ function getRuntimeHoistPosition(ast: Program): number {
// Preserve leading directives so directive-based transforms can
// still compose just in case.
for (const statement of ast.body) {
const isDirective =
statement.type === 'ExpressionStatement' &&
statement.expression.type === 'Literal' &&
typeof statement.expression.value === 'string'
if (!isDirective) {
if (!isDirective(statement)) {
return statement.start
}
}
Expand All @@ -282,15 +279,12 @@ function matchDirective(
directive: RegExp,
): { match: RegExpMatchArray; node: Literal } | undefined {
for (const stmt of body) {
if (
stmt.type === 'ExpressionStatement' &&
stmt.expression.type === 'Literal' &&
typeof stmt.expression.value === 'string'
) {
const match = stmt.expression.value.match(directive)
if (match) {
return { match, node: stmt.expression }
}
if (!isDirective(stmt)) {
return
}
const match = stmt.directive.match(directive)
if (match) {
return { match, node: stmt.expression }
}
}
}
Expand Down
17 changes: 16 additions & 1 deletion packages/plugin-rsc/src/transforms/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
import { parseAstAsync } from 'vite'
import { describe, expect, test } from 'vitest'
import { transformProxyExport } from './proxy-export'
import { validateNonAsyncFunction } from './utils'
import { hasDirective, validateNonAsyncFunction } from './utils'
import { transformWrapExport } from './wrap-export'

describe(hasDirective, () => {
test.each([
[`'use server'; export {};`, true],
[`'use strict'; 'use server'; export {};`, true],
[`import './setup.js'; 'use server'; export {};`, false],
[`('use server'); export {};`, false],
[`; 'use server'; export {};`, false],
[`'use client'; export {};`, false],
[`'use strict'; 'use client'; export {};`, false],
])('recognizes directive prologues', async (input, expected) => {
const ast = await parseAstAsync(input)
expect(hasDirective(ast.body, 'use server')).toBe(expected)
})
})

describe(validateNonAsyncFunction, () => {
// next.js's validation isn't entirely consistent.
// for now we aim to make it at least as forgiving as next.js.
Expand Down
16 changes: 8 additions & 8 deletions packages/plugin-rsc/src/transforms/utils.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import type { ExportDefaultDeclaration } from 'estree'
import type { Directive, ExportDefaultDeclaration } from 'estree'
import type { Identifier, Node, Pattern, Program } from 'estree'
import type { ESTree } from 'vite'

export function isDirective(node: Node): node is Directive {
// Directive is not its own `type`
// https://github.com/estree/estree/blob/master/es5.md#directive
return node.type === 'ExpressionStatement' && 'directive' in node
}

export function hasDirective(
viteBody: ESTree.Program['body'],
directive: string,
): boolean {
const body = viteBody as unknown as Program['body']
return !!body.find(
(stmt) =>
stmt.type === 'ExpressionStatement' &&
stmt.expression.type === 'Literal' &&
typeof stmt.expression.value === 'string' &&
stmt.expression.value === directive,
)
return body.some((stmt) => isDirective(stmt) && stmt.directive === directive)
}

// Copied from periscopic `extract_names` / `extract_identifiers`
Expand Down
Loading