diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts
index f5bb8aa62..01cadf3b0 100644
--- a/packages/plugin-rsc/e2e/basic.test.ts
+++ b/packages/plugin-rsc/e2e/basic.test.ts
@@ -376,6 +376,27 @@ function defineTest(f: Fixture) {
)
})
+ test('use server export all @js', async ({ page }) => {
+ await page.goto(f.url())
+ await waitForHydration(page)
+
+ await expect(page.getByTestId('test-action-export-all-server')).toHaveText(
+ 'export-all-server-to-server: 0',
+ )
+ await page.getByTestId('test-action-export-all-server').click()
+ await expect(page.getByTestId('test-action-export-all-server')).toHaveText(
+ 'export-all-server-to-server: 1',
+ )
+
+ await expect(page.getByTestId('test-action-export-all-client')).toHaveText(
+ 'export-all-server-to-client: ?',
+ )
+ await page.getByTestId('test-action-export-all-client').click()
+ await expect(page.getByTestId('test-action-export-all-client')).toHaveText(
+ 'export-all-server-to-client: export-all-client',
+ )
+ })
+
test('useActionState with jsx @js', async ({ page }) => {
await page.goto(f.url())
await waitForHydration(page)
@@ -1874,7 +1895,7 @@ function defineTest(f: Fixture) {
)
})
- test('export *', async ({ page }) => {
+ test('export all', async ({ page }) => {
await page.goto(f.url())
await waitForHydration(page)
await expect(page.getByTestId('test-export-all')).toHaveText(
diff --git a/packages/plugin-rsc/examples/basic/src/routes/action-export-all/action-impl.ts b/packages/plugin-rsc/examples/basic/src/routes/action-export-all/action-impl.ts
new file mode 100644
index 000000000..b14fd254c
--- /dev/null
+++ b/packages/plugin-rsc/examples/basic/src/routes/action-export-all/action-impl.ts
@@ -0,0 +1,13 @@
+let serverValue = 0
+
+export async function readExportAllServerValue() {
+ return serverValue
+}
+
+export async function incrementExportAllServerValue() {
+ serverValue++
+}
+
+export async function getExportAllClientValue() {
+ return 'export-all-client'
+}
diff --git a/packages/plugin-rsc/examples/basic/src/routes/action-export-all/actions.ts b/packages/plugin-rsc/examples/basic/src/routes/action-export-all/actions.ts
new file mode 100644
index 000000000..bf9797022
--- /dev/null
+++ b/packages/plugin-rsc/examples/basic/src/routes/action-export-all/actions.ts
@@ -0,0 +1,3 @@
+'use server'
+
+export * from './action-impl'
diff --git a/packages/plugin-rsc/examples/basic/src/routes/action-export-all/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/action-export-all/client.tsx
new file mode 100644
index 000000000..716cb9568
--- /dev/null
+++ b/packages/plugin-rsc/examples/basic/src/routes/action-export-all/client.tsx
@@ -0,0 +1,19 @@
+'use client'
+
+import React from 'react'
+import { getExportAllClientValue } from './actions'
+
+export function TestActionExportAllClient() {
+ const [result, setResult] = React.useState('?')
+
+ return (
+
+ )
+}
diff --git a/packages/plugin-rsc/examples/basic/src/routes/action-export-all/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/action-export-all/server.tsx
new file mode 100644
index 000000000..243d3c35e
--- /dev/null
+++ b/packages/plugin-rsc/examples/basic/src/routes/action-export-all/server.tsx
@@ -0,0 +1,18 @@
+import {
+ incrementExportAllServerValue,
+ readExportAllServerValue,
+} from './actions'
+import { TestActionExportAllClient } from './client'
+
+export function TestActionExportAll() {
+ return (
+ <>
+
+
+ >
+ )
+}
diff --git a/packages/plugin-rsc/examples/basic/src/routes/root.tsx b/packages/plugin-rsc/examples/basic/src/routes/root.tsx
index 5192bca23..ee7718b9a 100644
--- a/packages/plugin-rsc/examples/basic/src/routes/root.tsx
+++ b/packages/plugin-rsc/examples/basic/src/routes/root.tsx
@@ -9,6 +9,7 @@ import {
TestServerActionBindMember,
} from './action-bind/server'
import { TestServerActionError } from './action-error/server'
+import { TestActionExportAll } from './action-export-all/server'
import {
TestActionFromClient,
TestNonFormActionArgs,
@@ -101,6 +102,7 @@ export function Root(props: { url: URL }) {
+
diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts
index 023729878..01ec7b083 100644
--- a/packages/plugin-rsc/src/plugin.ts
+++ b/packages/plugin-rsc/src/plugin.ts
@@ -65,12 +65,13 @@ import {
} from './plugins/vite-utils'
import {
type TransformWrapExportFilter,
- extractNames,
hasDirective,
transformDirectiveProxyExport,
+ transformExpandExportAll,
transformServerActionServer,
transformWrapExport,
findDirectives,
+ type TransformExpandExportAllContext,
} from './transforms'
import { generateEncryptionKey, toBase64 } from './utils/encryption-utils'
import { createRpcServer } from './utils/rpc'
@@ -1376,141 +1377,26 @@ function globalAsyncLocalStoragePlugin(): Plugin[] {
]
}
-// Strip TS/JSX so `parseAstAsync` can read the result. Prefer oxc when
-// available (Vite 8+); fall back to esbuild for older Vite versions.
-async function transformSourceForExportScan(
- code: string,
- filename: string,
-): Promise {
- const v = vite as Partial<{
- transformWithOxc: (
- code: string,
- filename: string,
- options?: { sourcemap?: boolean },
- ) => Promise<{ code: string }>
- transformWithEsbuild: (
- code: string,
- filename: string,
- options?: { sourcemap?: boolean },
- ) => Promise<{ code: string }>
- }>
- const transform = v.transformWithOxc ?? v.transformWithEsbuild
- if (!transform) return undefined
- const result = await transform(code, filename, { sourcemap: false })
- return result.code
-}
-
-// Recursively collect the named exports of a module (following `export * from`
-// chains), so that the RSC `use client`/`use server` proxy transforms can
-// expand bare `export *` re-exports into explicit named re-exports before
-// proxy generation. The pure proxy transform cannot do this on its own because
-// the names live in another file.
-async function collectExportNames(
+function createTransformExpandExportAllContext(
ctx: Rollup.TransformPluginContext,
- resolvedId: string,
- seen: Set,
-): Promise {
- if (seen.has(resolvedId)) return []
- seen.add(resolvedId)
-
- // Read the source from disk and strip TS/JSX so the AST walk below sees
- // standard ESM exports. We don't go through `this.load` /
- // `transformRequest` here — in dev they return module-runner output
- // (`__vite_ssr_exportName__(...)`) the walker can't read, and on build
- // there's no practical benefit over reading the source directly for the
- // simple TS/JSX modules we care about.
- let moduleCode: string | undefined
- try {
- const raw = await fs.promises.readFile(resolvedId, 'utf-8')
- moduleCode = await transformSourceForExportScan(raw, resolvedId)
- } catch {
- return []
- }
- if (!moduleCode) return []
-
- let ast: Awaited>
- try {
- ast = await parseAstAsync(moduleCode)
- } catch {
- return []
- }
-
- const names: string[] = []
- for (const node of ast.body) {
- if (node.type === 'ExportNamedDeclaration') {
- if (node.declaration) {
- if (
- node.declaration.type === 'FunctionDeclaration' ||
- node.declaration.type === 'ClassDeclaration'
- ) {
- if (node.declaration.id) names.push(node.declaration.id.name)
- } else if (node.declaration.type === 'VariableDeclaration') {
- for (const decl of node.declaration.declarations) {
- names.push(...extractNames(decl.id))
- }
- }
- } else {
- for (const spec of node.specifiers) {
- if (
- spec.exported.type === 'Identifier' &&
- spec.exported.name !== 'default'
- ) {
- names.push(spec.exported.name)
- }
- }
- }
- } else if (node.type === 'ExportAllDeclaration') {
- if (node.exported?.type === 'Identifier') {
- names.push(node.exported.name)
- } else if (node.source) {
- const subResolved = await ctx.resolve(
- node.source.value as string,
- resolvedId,
- )
- if (subResolved) {
- names.push(...(await collectExportNames(ctx, subResolved.id, seen)))
- }
- }
- }
- }
- return names
-}
-
-async function expandExportAllDeclarations(
- ctx: Rollup.TransformPluginContext,
- ast: Awaited>,
- code: string,
- id: string,
-): Promise<{
- code: string
- ast: Awaited>
-} | null> {
- const targets = ast.body.filter(
- (n) => n.type === 'ExportAllDeclaration' && !n.exported,
- )
- if (targets.length === 0) return null
-
- const output = new MagicString(code)
- for (const node of targets) {
- if (node.type !== 'ExportAllDeclaration') continue
- const source = node.source.value as string
- const resolved = await ctx.resolve(source, id)
- if (!resolved) continue
- const names = await collectExportNames(ctx, resolved.id, new Set())
- if (names.length === 0) {
- output.remove(node.start, node.end)
- } else {
- output.update(
- node.start,
- node.end,
- `export { ${names.join(', ')} } from ${JSON.stringify(source)};`,
- )
- }
+): TransformExpandExportAllContext {
+ return {
+ resolve: async (source, importer) => {
+ return (await ctx.resolve(source, importer))?.id
+ },
+ load: async (id) => {
+ // Read the source from disk and strip TS/JSX so the AST walk sees
+ // standard ESM exports. We don't go through `this.load` /
+ // `transformRequest` here — in dev they return module-runner output
+ // (`__vite_ssr_exportName__(...)`) the walker can't read, and on build
+ // there's no practical benefit over reading the source directly for the
+ // simple TS/JSX modules we care about.
+ const raw = await fs.promises.readFile(id, 'utf-8')
+ const transform = vite.transformWithOxc ?? vite.transformWithEsbuild
+ const result = await transform(raw, id, { sourcemap: false })
+ return parseAstAsync(result.code)
+ },
}
- if (!output.hasChanged()) return null
- const newCode = output.toString()
- const newAst = await parseAstAsync(newCode)
- return { code: newCode, ast: newAst }
}
function vitePluginUseClient(
@@ -1580,15 +1466,15 @@ function vitePluginUseClient(
}
}
- const expanded = await expandExportAllDeclarations(
- this,
- ast,
+ const expanded = await transformExpandExportAll({
code,
- id,
- )
+ ast,
+ importer: id,
+ ...createTransformExpandExportAllContext(this),
+ })
if (expanded) {
code = expanded.code
- ast = expanded.ast
+ ast = await parseAstAsync(code)
}
let importId: string
@@ -2065,15 +1951,15 @@ function vitePluginUseServer(
}
let ast = await parseAstAsync(code)
if (hasDirective(ast.body, 'use server')) {
- const expanded = await expandExportAllDeclarations(
- this,
- ast,
+ const expanded = await transformExpandExportAll({
code,
- id,
- )
+ ast,
+ importer: id,
+ ...createTransformExpandExportAllContext(this),
+ })
if (expanded) {
code = expanded.code
- ast = expanded.ast
+ ast = await parseAstAsync(code)
}
}
diff --git a/packages/plugin-rsc/src/transforms/expand-export-all.test.ts b/packages/plugin-rsc/src/transforms/expand-export-all.test.ts
new file mode 100644
index 000000000..ae083c26d
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/expand-export-all.test.ts
@@ -0,0 +1,72 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import { parseAstAsync } from 'vite'
+import { describe, expect, it } from 'vitest'
+import { transformExpandExportAll } from './expand-export-all'
+
+describe('fixtures', () => {
+ const fixtures = import.meta.glob(
+ ['./fixtures/expand-export-all/**/entry.js', '!**/*.snap.*'],
+ {
+ query: 'raw',
+ },
+ )
+ async function transformFixture(input: string, importer: string) {
+ const ast = await parseAstAsync(input)
+ const result = await transformExpandExportAll({
+ code: input,
+ ast,
+ importer,
+ resolve: async (source, importer) => {
+ if (importer.includes('/bad-resolve/')) return
+ return path.join(path.dirname(importer), source)
+ },
+ load: async (id) => {
+ if (!fs.existsSync(id)) {
+ throw new Error(`failed to load ${JSON.stringify(path.basename(id))}`)
+ }
+ const code = fs.readFileSync(id, 'utf-8')
+ return parseAstAsync(code)
+ },
+ })
+ if (!result) {
+ return '/* NO CHANGE */\n'
+ }
+ try {
+ await parseAstAsync(result.code)
+ } catch (e) {
+ return `\
+${result.code}
+
+/* PARSE ERROR
+
+${(e as Error).message}
+
+*/
+`
+ }
+ return result.code.trim() + '\n'
+ }
+
+ for (const [file, mod] of Object.entries(fixtures)) {
+ it(path.basename(path.dirname(file)), async () => {
+ const input = ((await mod()) as any).default as string
+ let output: string
+ try {
+ output = await transformFixture(
+ input,
+ path.join(import.meta.dirname, file),
+ )
+ } catch (e) {
+ output = `\
+/* ERROR
+
+${(e as Error).message}
+
+*/
+`
+ }
+ await expect(output).toMatchFileSnapshot(file + '.snap.js')
+ })
+ }
+})
diff --git a/packages/plugin-rsc/src/transforms/expand-export-all.ts b/packages/plugin-rsc/src/transforms/expand-export-all.ts
new file mode 100644
index 000000000..6799744d8
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/expand-export-all.ts
@@ -0,0 +1,217 @@
+import type { ExportAllDeclaration, Program } from 'estree'
+import MagicString from 'magic-string'
+import { extractNames } from './utils'
+
+export interface TransformExpandExportAllContext {
+ resolve: (source: string, importer: string) => Promise
+ load: (id: string) => Promise
+}
+
+export interface TransformExpandExportAllOptions extends TransformExpandExportAllContext {
+ code: string
+ ast: Program
+ importer: string
+}
+
+type ModuleExportScan = {
+ explicitNames: Set
+ starSources: StarExportSource[]
+}
+
+type StarExportSource = {
+ node: ExportAllDeclaration
+ scan: ExportNameScan
+}
+
+// `names` and `ambiguousNames` describe this module's star-visible export
+// state. A name in `names` can be safely re-exported by a parent. A name in
+// `ambiguousNames` must be propagated even though it is not visible, because a
+// parent star export must also treat that name as ambiguous unless the parent
+// defines it explicitly.
+type ExportNameScan = {
+ names: string[]
+ ambiguousNames: Set
+}
+
+type StarExportResolution = {
+ ambiguousNames: Set
+ plans: StarExportRewritePlan[]
+}
+
+type StarExportRewritePlan = {
+ node: ExportAllDeclaration
+ names: string[]
+}
+
+export async function transformExpandExportAll(
+ options: TransformExpandExportAllOptions,
+): Promise<{ code: string } | undefined> {
+ const scan = await scanModuleExports(options.ast, options.importer, options)
+ const { plans } = resolveStarExports(scan)
+ if (plans.length === 0) {
+ return
+ }
+ const output = new MagicString(options.code)
+ for (const item of plans) {
+ const newExport = `export {${item.names.join(', ')}} from ${JSON.stringify(item.node.source.value)};`
+ output.update(item.node.start, item.node.end, newExport)
+ }
+ // TODO: return a sourcemap so callers can compose this pre-rewrite with
+ // their follow-up proxy/wrap transform maps.
+ return { code: output.toString() }
+}
+
+// Scan a module into local explicit exports and recursively scanned direct
+// `export *` sources. This does not decide conflicts; resolveStarExports does.
+async function scanModuleExports(
+ ast: Program,
+ importer: string,
+ context: TransformExpandExportAllContext,
+ seen = new Set(),
+): Promise {
+ const starSources: StarExportSource[] = []
+ const bareStars = ast.body.filter(
+ (n): n is ExportAllDeclaration =>
+ n.type === 'ExportAllDeclaration' && !n.exported,
+ )
+
+ for (const node of bareStars) {
+ const source = node.source.value as string
+ const resolved = await context.resolve(source, importer)
+ if (!resolved) {
+ throw Object.assign(
+ new Error(
+ `failed to resolve export-all source ${JSON.stringify(source)}`,
+ ),
+ { pos: node.start },
+ )
+ }
+ starSources.push({
+ node,
+ scan: await collectExportScan(resolved, context, new Set(seen)),
+ })
+ }
+
+ const explicitNames = collectExplicitExportNames(ast)
+ return { explicitNames, starSources }
+}
+
+// Return the names that a resolved dependency visibly exports, plus names whose
+// resolution is ambiguous and must continue to poison parent star resolution.
+async function collectExportScan(
+ resolvedId: string,
+ context: TransformExpandExportAllContext,
+ seen: Set,
+): Promise {
+ if (seen.has(resolvedId)) {
+ // TODO: This module-level bailout is only a termination guard. A fully
+ // spec-accurate resolver would track per-name resolution state and binding
+ // identity, so cyclic paths that resolve to the same binding can survive.
+ return { names: [], ambiguousNames: new Set() }
+ }
+ seen.add(resolvedId)
+
+ const ast = await context.load(resolvedId)
+ const scan = await scanModuleExports(ast, resolvedId, context, seen)
+ const resolved = resolveStarExports(scan)
+ const names = [
+ ...scan.explicitNames,
+ ...resolved.plans.flatMap((item) => item.names),
+ ]
+ return {
+ names,
+ ambiguousNames: resolved.ambiguousNames,
+ }
+}
+
+// Collect names declared directly by the module, including namespace re-exports.
+// These names shadow star exports at this module boundary.
+function collectExplicitExportNames(ast: Program): Set {
+ const names = new Set()
+ for (const node of ast.body) {
+ if (node.type === 'ExportNamedDeclaration') {
+ if (node.declaration) {
+ if (
+ node.declaration.type === 'FunctionDeclaration' ||
+ node.declaration.type === 'ClassDeclaration'
+ ) {
+ if (node.declaration.id) names.add(node.declaration.id.name)
+ } else if (node.declaration.type === 'VariableDeclaration') {
+ for (const decl of node.declaration.declarations) {
+ for (const name of extractNames(decl.id)) {
+ names.add(name)
+ }
+ }
+ }
+ } else {
+ for (const spec of node.specifiers) {
+ if (spec.exported.type === 'Identifier') {
+ names.add(spec.exported.name)
+ } else {
+ throw Object.assign(
+ new Error('unsupported string literal export name'),
+ { pos: spec.exported.start },
+ )
+ }
+ }
+ }
+ } else if (node.type === 'ExportDefaultDeclaration') {
+ names.add('default')
+ } else if (node.type === 'ExportAllDeclaration') {
+ if (node.exported?.type === 'Identifier') {
+ names.add(node.exported.name)
+ }
+ }
+ }
+ return names
+}
+
+// Apply ESM export-star conflict rules for one module boundary and build the
+// rewrite for each direct `export *`. Ambiguity from child modules is preserved
+// so a parent does not accidentally make an invalid name explicit.
+function resolveStarExports(scan: ModuleExportScan): StarExportResolution {
+ // find multiple re-exported names through "export *"
+ // and treat as ambiguous
+ const seenStarNames = new Set()
+ const ambiguousNames = new Set()
+ for (const source of scan.starSources) {
+ for (const name of source.scan.names) {
+ if (name === 'default' || scan.explicitNames.has(name)) {
+ continue
+ }
+ if (seenStarNames.has(name)) {
+ ambiguousNames.add(name)
+ } else {
+ seenStarNames.add(name)
+ }
+ }
+ }
+
+ // propagate ambiguous star re-exports to the parent
+ for (const source of scan.starSources) {
+ for (const name of source.scan.ambiguousNames) {
+ if (!scan.explicitNames.has(name)) {
+ ambiguousNames.add(name)
+ }
+ }
+ }
+
+ const plans: StarExportRewritePlan[] = []
+ for (const source of scan.starSources) {
+ const names = source.scan.names.filter(
+ (name) =>
+ name !== 'default' &&
+ !scan.explicitNames.has(name) &&
+ !ambiguousNames.has(name),
+ )
+ plans.push({
+ node: source.node,
+ names,
+ })
+ }
+
+ return {
+ ambiguousNames,
+ plans,
+ }
+}
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/bad-load/entry.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/bad-load/entry.js
new file mode 100644
index 000000000..4bae3c345
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/bad-load/entry.js
@@ -0,0 +1 @@
+export * from './missing.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/bad-load/entry.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/bad-load/entry.js.snap.js
new file mode 100644
index 000000000..73ec11e53
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/bad-load/entry.js.snap.js
@@ -0,0 +1,5 @@
+/* ERROR
+
+failed to load "missing.js"
+
+*/
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/bad-resolve/entry.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/bad-resolve/entry.js
new file mode 100644
index 000000000..e5d44c46a
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/bad-resolve/entry.js
@@ -0,0 +1 @@
+export * from './dep.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/bad-resolve/entry.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/bad-resolve/entry.js.snap.js
new file mode 100644
index 000000000..ba74313e2
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/bad-resolve/entry.js.snap.js
@@ -0,0 +1,5 @@
+/* ERROR
+
+failed to resolve export-all source "./dep.js"
+
+*/
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/basic/default.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/basic/default.js
new file mode 100644
index 000000000..bc40bc291
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/basic/default.js
@@ -0,0 +1 @@
+export default function Default() {}
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/basic/dep.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/basic/dep.js
new file mode 100644
index 000000000..fbbbd8789
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/basic/dep.js
@@ -0,0 +1,5 @@
+export function a() {}
+export const b = 1
+export { default } from './default.js'
+export * from './nested.js'
+export * as ns from './nested.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/basic/entry.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/basic/entry.js
new file mode 100644
index 000000000..0b5a53754
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/basic/entry.js
@@ -0,0 +1,2 @@
+export * from './dep.js'
+export const local = 1
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/basic/entry.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/basic/entry.js.snap.js
new file mode 100644
index 000000000..9bf9a0427
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/basic/entry.js.snap.js
@@ -0,0 +1,2 @@
+export {a, b, ns, C} from "./dep.js";
+export const local = 1
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/basic/nested.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/basic/nested.js
new file mode 100644
index 000000000..1ec0ebf40
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/basic/nested.js
@@ -0,0 +1 @@
+export class C {}
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular-same-binding/a.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular-same-binding/a.js
new file mode 100644
index 000000000..80fd85b47
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular-same-binding/a.js
@@ -0,0 +1 @@
+export * from './b.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular-same-binding/b.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular-same-binding/b.js
new file mode 100644
index 000000000..d1a3afc49
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular-same-binding/b.js
@@ -0,0 +1,2 @@
+export * from './a.js'
+export const value = 1
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular-same-binding/entry.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular-same-binding/entry.js
new file mode 100644
index 000000000..64c1f05bc
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular-same-binding/entry.js
@@ -0,0 +1,3 @@
+// TODO: support cyclic star exports that resolve to the same binding.
+export * from './a.js'
+export * from './b.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular-same-binding/entry.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular-same-binding/entry.js.snap.js
new file mode 100644
index 000000000..498b8d1f4
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular-same-binding/entry.js.snap.js
@@ -0,0 +1,3 @@
+// TODO: support cyclic star exports that resolve to the same binding.
+export {} from "./a.js";
+export {} from "./b.js";
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular/dep1.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular/dep1.js
new file mode 100644
index 000000000..3e5f5ce9c
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular/dep1.js
@@ -0,0 +1,2 @@
+export * from './dep2.js'
+export const a = 1
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular/dep2.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular/dep2.js
new file mode 100644
index 000000000..392cf2b68
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular/dep2.js
@@ -0,0 +1,2 @@
+export * from './dep1.js'
+export const b = 2
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular/entry.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular/entry.js
new file mode 100644
index 000000000..14fdb1a99
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular/entry.js
@@ -0,0 +1 @@
+export * from './dep1.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular/entry.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular/entry.js.snap.js
new file mode 100644
index 000000000..1ee19e2d3
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/circular/entry.js.snap.js
@@ -0,0 +1 @@
+export {a, b} from "./dep1.js";
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/conflict-star/a.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/conflict-star/a.js
new file mode 100644
index 000000000..605a790bb
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/conflict-star/a.js
@@ -0,0 +1,2 @@
+export const shared = 1
+export const onlyA = 1
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/conflict-star/b.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/conflict-star/b.js
new file mode 100644
index 000000000..73366dadc
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/conflict-star/b.js
@@ -0,0 +1,2 @@
+export const shared = 2
+export const onlyB = 2
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/conflict-star/entry.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/conflict-star/entry.js
new file mode 100644
index 000000000..2f940ab3d
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/conflict-star/entry.js
@@ -0,0 +1,2 @@
+export * from './a.js'
+export * from './b.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/conflict-star/entry.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/conflict-star/entry.js.snap.js
new file mode 100644
index 000000000..dcabdda5c
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/conflict-star/entry.js.snap.js
@@ -0,0 +1,2 @@
+export {onlyA} from "./a.js";
+export {onlyB} from "./b.js";
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/default-only/dep.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/default-only/dep.js
new file mode 100644
index 000000000..33c69bb17
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/default-only/dep.js
@@ -0,0 +1,2 @@
+console.log('dep')
+export default 1
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/default-only/entry.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/default-only/entry.js
new file mode 100644
index 000000000..e5d44c46a
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/default-only/entry.js
@@ -0,0 +1 @@
+export * from './dep.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/default-only/entry.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/default-only/entry.js.snap.js
new file mode 100644
index 000000000..686a3c75a
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/default-only/entry.js.snap.js
@@ -0,0 +1 @@
+export {} from "./dep.js";
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/duplicate-star-same-source/dep.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/duplicate-star-same-source/dep.js
new file mode 100644
index 000000000..f5e5bd3f1
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/duplicate-star-same-source/dep.js
@@ -0,0 +1,2 @@
+export const a = 1
+export const b = 2
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/duplicate-star-same-source/entry.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/duplicate-star-same-source/entry.js
new file mode 100644
index 000000000..19c5db7b5
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/duplicate-star-same-source/entry.js
@@ -0,0 +1,4 @@
+// TODO: support duplicate star exports that resolve to the same bindings.
+export * from './dep.js'
+export * from './dep.js'
+export * from './dep.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/duplicate-star-same-source/entry.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/duplicate-star-same-source/entry.js.snap.js
new file mode 100644
index 000000000..fd6f94519
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/duplicate-star-same-source/entry.js.snap.js
@@ -0,0 +1,4 @@
+// TODO: support duplicate star exports that resolve to the same bindings.
+export {} from "./dep.js";
+export {} from "./dep.js";
+export {} from "./dep.js";
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/explicit-wins/dep.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/explicit-wins/dep.js
new file mode 100644
index 000000000..07d338c59
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/explicit-wins/dep.js
@@ -0,0 +1,2 @@
+export const shared = 2
+export const fromDep = 2
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/explicit-wins/entry.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/explicit-wins/entry.js
new file mode 100644
index 000000000..c7b86f1c8
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/explicit-wins/entry.js
@@ -0,0 +1,2 @@
+export * from './dep.js'
+export const shared = 1
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/explicit-wins/entry.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/explicit-wins/entry.js.snap.js
new file mode 100644
index 000000000..f6a87d7a2
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/explicit-wins/entry.js.snap.js
@@ -0,0 +1,2 @@
+export {fromDep} from "./dep.js";
+export const shared = 1
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/namespace-same-binding/dep-target.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/namespace-same-binding/dep-target.js
new file mode 100644
index 000000000..1d772ee2c
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/namespace-same-binding/dep-target.js
@@ -0,0 +1 @@
+export const value = 1
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/namespace-same-binding/dep1.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/namespace-same-binding/dep1.js
new file mode 100644
index 000000000..f989d83ec
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/namespace-same-binding/dep1.js
@@ -0,0 +1 @@
+export * as target from './dep-target.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/namespace-same-binding/dep2.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/namespace-same-binding/dep2.js
new file mode 100644
index 000000000..f989d83ec
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/namespace-same-binding/dep2.js
@@ -0,0 +1 @@
+export * as target from './dep-target.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/namespace-same-binding/entry.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/namespace-same-binding/entry.js
new file mode 100644
index 000000000..97b6b3db2
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/namespace-same-binding/entry.js
@@ -0,0 +1,4 @@
+// TODO: support duplicate star exports that resolve to the same namespace binding.
+// This is also currently broken in Node 24.16.0 due to a V8 bug.
+export * from './dep1.js'
+export * from './dep2.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/namespace-same-binding/entry.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/namespace-same-binding/entry.js.snap.js
new file mode 100644
index 000000000..a6c8b0741
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/namespace-same-binding/entry.js.snap.js
@@ -0,0 +1,4 @@
+// TODO: support duplicate star exports that resolve to the same namespace binding.
+// This is also currently broken in Node 24.16.0 due to a V8 bug.
+export {} from "./dep1.js";
+export {} from "./dep2.js";
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/a.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/a.js
new file mode 100644
index 000000000..605a790bb
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/a.js
@@ -0,0 +1,2 @@
+export const shared = 1
+export const onlyA = 1
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/b.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/b.js
new file mode 100644
index 000000000..73366dadc
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/b.js
@@ -0,0 +1,2 @@
+export const shared = 2
+export const onlyB = 2
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/dep.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/dep.js
new file mode 100644
index 000000000..2f940ab3d
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/dep.js
@@ -0,0 +1,2 @@
+export * from './a.js'
+export * from './b.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/entry.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/entry.js
new file mode 100644
index 000000000..ef6ac0100
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/entry.js
@@ -0,0 +1 @@
+export * from './middle.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/entry.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/entry.js.snap.js
new file mode 100644
index 000000000..14f241bfd
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/entry.js.snap.js
@@ -0,0 +1 @@
+export {shared, onlyA, onlyB} from "./middle.js";
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/middle.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/middle.js
new file mode 100644
index 000000000..402b3fd36
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/middle.js
@@ -0,0 +1,2 @@
+export * from './dep.js'
+export { shared } from './override.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/override.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/override.js
new file mode 100644
index 000000000..9b592ef09
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-explicit-reexport-wins/override.js
@@ -0,0 +1 @@
+export const shared = 3
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-star/a.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-star/a.js
new file mode 100644
index 000000000..605a790bb
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-star/a.js
@@ -0,0 +1,2 @@
+export const shared = 1
+export const onlyA = 1
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-star/b.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-star/b.js
new file mode 100644
index 000000000..73366dadc
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-star/b.js
@@ -0,0 +1,2 @@
+export const shared = 2
+export const onlyB = 2
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-star/dep.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-star/dep.js
new file mode 100644
index 000000000..2f940ab3d
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-star/dep.js
@@ -0,0 +1,2 @@
+export * from './a.js'
+export * from './b.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-star/entry.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-star/entry.js
new file mode 100644
index 000000000..e5d44c46a
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-star/entry.js
@@ -0,0 +1 @@
+export * from './dep.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-star/entry.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-star/entry.js.snap.js
new file mode 100644
index 000000000..82914593b
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-conflict-star/entry.js.snap.js
@@ -0,0 +1 @@
+export {onlyA, onlyB} from "./dep.js";
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/dep1.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/dep1.js
new file mode 100644
index 000000000..257d79057
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/dep1.js
@@ -0,0 +1,3 @@
+export const a = 1
+export const b = 1
+export const c = 1
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/dep2-1.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/dep2-1.js
new file mode 100644
index 000000000..6fd2947a5
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/dep2-1.js
@@ -0,0 +1,3 @@
+export const a = 1
+export const b = 1
+export const x = 1
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/dep2-2.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/dep2-2.js
new file mode 100644
index 000000000..6cf48bbbb
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/dep2-2.js
@@ -0,0 +1,2 @@
+export const a = 2
+export const y = 2
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/dep2.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/dep2.js
new file mode 100644
index 000000000..f9b593c3a
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/dep2.js
@@ -0,0 +1,2 @@
+export * from './dep2-1.js'
+export * from './dep2-2.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/entry.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/entry.js
new file mode 100644
index 000000000..bcbfe36b7
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/entry.js
@@ -0,0 +1,2 @@
+export * from './dep1.js'
+export * from './dep2.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/entry.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/entry.js.snap.js
new file mode 100644
index 000000000..a72df6029
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/entry.js.snap.js
@@ -0,0 +1,2 @@
+export {c} from "./dep1.js";
+export {x, y} from "./dep2.js";
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/repro1.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/repro1.js
new file mode 100644
index 000000000..328601cb2
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/repro1.js
@@ -0,0 +1,7 @@
+// this doesn't fail but it should. potentially node/v8 bug.
+// node packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/repro1.js
+// similar class of issue but different
+// https://github.com/nodejs/node/issues/53707
+// https://chromium.googlesource.com/v8/v8/+/88760dadfba1457aea76e50e6ead3b87f868c7fb
+import * as ns from './entry.js'
+console.log(ns.a)
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/repro2.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/repro2.js
new file mode 100644
index 000000000..c45ffb94f
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/repro2.js
@@ -0,0 +1,4 @@
+// this fails as expected.
+// node packages/plugin-rsc/src/transforms/fixtures/expand-export-all/nested-filtered-conflict/repro2.js
+import { a } from './entry.js'
+console.log(a)
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/no-export-star/dep.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/no-export-star/dep.js
new file mode 100644
index 000000000..1d772ee2c
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/no-export-star/dep.js
@@ -0,0 +1 @@
+export const value = 1
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/no-export-star/entry.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/no-export-star/entry.js
new file mode 100644
index 000000000..d16b69db0
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/no-export-star/entry.js
@@ -0,0 +1,2 @@
+export * as ns from './dep.js'
+export const local = 1
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/no-export-star/entry.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/no-export-star/entry.js.snap.js
new file mode 100644
index 000000000..aed6a1387
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/no-export-star/entry.js.snap.js
@@ -0,0 +1 @@
+/* NO CHANGE */
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/string-export-name/dep.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/string-export-name/dep.js
new file mode 100644
index 000000000..6a37bac3a
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/string-export-name/dep.js
@@ -0,0 +1,2 @@
+const x = 1
+export { x as 'my thing' }
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/string-export-name/entry.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/string-export-name/entry.js
new file mode 100644
index 000000000..3a374f6b3
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/string-export-name/entry.js
@@ -0,0 +1,2 @@
+// TODO: support string literal export names.
+export * from './dep.js'
diff --git a/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/string-export-name/entry.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/string-export-name/entry.js.snap.js
new file mode 100644
index 000000000..3ff9186b4
--- /dev/null
+++ b/packages/plugin-rsc/src/transforms/fixtures/expand-export-all/string-export-name/entry.js.snap.js
@@ -0,0 +1,5 @@
+/* ERROR
+
+unsupported string literal export name
+
+*/
diff --git a/packages/plugin-rsc/src/transforms/index.ts b/packages/plugin-rsc/src/transforms/index.ts
index 55b054c08..b37480dae 100644
--- a/packages/plugin-rsc/src/transforms/index.ts
+++ b/packages/plugin-rsc/src/transforms/index.ts
@@ -3,3 +3,4 @@ export * from './wrap-export'
export * from './proxy-export'
export * from './utils'
export * from './server-action'
+export * from './expand-export-all'
diff --git a/packages/plugin-rsc/src/transforms/proxy-export.test.ts b/packages/plugin-rsc/src/transforms/proxy-export.test.ts
index 4d5ce63d2..4530d237a 100644
--- a/packages/plugin-rsc/src/transforms/proxy-export.test.ts
+++ b/packages/plugin-rsc/src/transforms/proxy-export.test.ts
@@ -171,6 +171,16 @@ export { x as y }
`)
})
+ test('export string name throws', async () => {
+ const input = `
+const x = 0;
+export { x as "my thing" }
+`
+ await expect(testTransform(input)).rejects.toThrow(
+ 'unsupported string literal export name',
+ )
+ })
+
test('re-export simple', async () => {
const input = `export { x } from "./dep"`
expect(await testTransform(input)).toMatchInlineSnapshot(`
@@ -210,23 +220,6 @@ export { x as y }
`)
})
- test('re-export all (resolved)', async () => {
- // when caller resolves names ahead of time, the source is rewritten so
- // the transform never sees a bare `export *`.
- const input = `export { x, y } from "./dep"`
- expect(await testTransform(input)).toMatchInlineSnapshot(`
- {
- "exportNames": [
- "x",
- "y",
- ],
- "output": "export const x = /* #__PURE__ */ $$proxy("", "x");
- export const y = /* #__PURE__ */ $$proxy("", "y");
- ",
- }
- `)
- })
-
test('re-export all (ignoreExportAllDeclaration)', async () => {
const input = `export * from "./dep"`
expect(await testTransform(input, { ignoreExportAllDeclaration: true }))
diff --git a/packages/plugin-rsc/src/transforms/proxy-export.ts b/packages/plugin-rsc/src/transforms/proxy-export.ts
index 9ba0e9cc3..026b6da71 100644
--- a/packages/plugin-rsc/src/transforms/proxy-export.ts
+++ b/packages/plugin-rsc/src/transforms/proxy-export.ts
@@ -1,4 +1,3 @@
-import { tinyassert } from '@hiogawa/utils'
import type { Node, Program } from 'estree'
import MagicString from 'magic-string'
import { extractNames, hasDirective, validateNonAsyncFunction } from './utils'
@@ -107,7 +106,12 @@ export function transformProxyExport(
*/
const names: string[] = []
for (const spec of node.specifiers) {
- tinyassert(spec.exported.type === 'Identifier')
+ if (spec.exported.type !== 'Identifier') {
+ throw Object.assign(
+ new Error('unsupported string literal export name'),
+ { pos: spec.exported.start },
+ )
+ }
names.push(spec.exported.name)
}
createExport(node, names)
diff --git a/packages/plugin-rsc/src/transforms/utils.ts b/packages/plugin-rsc/src/transforms/utils.ts
index 4fd949359..a0769edfe 100644
--- a/packages/plugin-rsc/src/transforms/utils.ts
+++ b/packages/plugin-rsc/src/transforms/utils.ts
@@ -1,4 +1,3 @@
-import { tinyassert } from '@hiogawa/utils'
import type { ExportDefaultDeclaration } from 'estree'
import type { Identifier, Node, Pattern, Program } from 'estree'
@@ -15,72 +14,6 @@ export function hasDirective(
)
}
-export function getExportNames(
- ast: Program,
- options: {
- ignoreExportAllDeclaration?: boolean
- },
-): {
- exportNames: string[]
-} {
- const exportNames: string[] = []
-
- for (const node of ast.body) {
- if (node.type === 'ExportNamedDeclaration') {
- if (node.declaration) {
- if (
- node.declaration.type === 'FunctionDeclaration' ||
- node.declaration.type === 'ClassDeclaration'
- ) {
- /**
- * export function foo() {}
- */
- exportNames.push(node.declaration.id.name)
- } else if (node.declaration.type === 'VariableDeclaration') {
- /**
- * export const foo = 1, bar = 2
- */
- for (const decl of node.declaration.declarations) {
- exportNames.push(...extractNames(decl.id))
- }
- } else {
- node.declaration satisfies never
- }
- } else {
- /**
- * export { foo, bar as car } from './foo'
- * export { foo, bar as car }
- */
- for (const spec of node.specifiers) {
- tinyassert(spec.exported.type === 'Identifier')
- exportNames.push(spec.exported.name)
- }
- }
- }
-
- /**
- * export * from './foo'
- */
- if (
- !options.ignoreExportAllDeclaration &&
- node.type === 'ExportAllDeclaration'
- ) {
- throw new Error('unsupported ExportAllDeclaration')
- }
-
- /**
- * export default function foo() {}
- * export default class Foo {}
- * export default () => {}
- */
- if (node.type === 'ExportDefaultDeclaration') {
- exportNames.push('default')
- }
- }
-
- return { exportNames }
-}
-
// Copied from periscopic `extract_names` / `extract_identifiers`
export function extractNames(param: Pattern): string[] {
return extractIdentifiers(param).map((n) => n.name)
diff --git a/packages/plugin-rsc/src/transforms/wrap-export.test.ts b/packages/plugin-rsc/src/transforms/wrap-export.test.ts
index 97d9f4f29..c3cb7473a 100644
--- a/packages/plugin-rsc/src/transforms/wrap-export.test.ts
+++ b/packages/plugin-rsc/src/transforms/wrap-export.test.ts
@@ -178,6 +178,16 @@ export { x as y }
`)
})
+ test('export string name throws', async () => {
+ const input = `
+const x = 0;
+export { x as "my thing" }
+`
+ await expect(testTransform(input)).rejects.toThrow(
+ 'unsupported string literal export name',
+ )
+ })
+
test('re-export simple', async () => {
const input = `export { x } from "./dep"`
expect(await testTransform(input)).toMatchInlineSnapshot(`
@@ -207,13 +217,7 @@ export { x as y }
test('re-export all rename', async () => {
const input = `export * as all from "./dep"`
- expect(await testTransform(input)).toMatchInlineSnapshot(`
- ";
- import * as $$import_all from "./dep";
- const $$wrap_$$import_all = /* #__PURE__ */ $$wrap($$import_all, "", "all");
- export { $$wrap_$$import_all as all };
- "
- `)
+ expect(await testTransform(input)).toMatchInlineSnapshot(`false`)
})
test('filter', async () => {
diff --git a/packages/plugin-rsc/src/transforms/wrap-export.ts b/packages/plugin-rsc/src/transforms/wrap-export.ts
index 26113c2f9..f5328e967 100644
--- a/packages/plugin-rsc/src/transforms/wrap-export.ts
+++ b/packages/plugin-rsc/src/transforms/wrap-export.ts
@@ -146,7 +146,12 @@ export function transformWrapExport(
output.remove(node.start, node.end)
for (const spec of node.specifiers) {
tinyassert(spec.local.type === 'Identifier')
- tinyassert(spec.exported.type === 'Identifier')
+ if (spec.exported.type !== 'Identifier') {
+ throw Object.assign(
+ new Error('unsupported string literal export name'),
+ { pos: spec.exported.start },
+ )
+ }
const name = spec.local.name
toAppend.push(
`import { ${name} as $$import_${name} } from ${node.source.raw}`,
@@ -160,7 +165,12 @@ export function transformWrapExport(
output.remove(node.start, node.end)
for (const spec of node.specifiers) {
tinyassert(spec.local.type === 'Identifier')
- tinyassert(spec.exported.type === 'Identifier')
+ if (spec.exported.type !== 'Identifier') {
+ throw Object.assign(
+ new Error('unsupported string literal export name'),
+ { pos: spec.exported.start },
+ )
+ }
wrapExport(spec.local.name, spec.exported.name)
}
}
@@ -175,14 +185,7 @@ export function transformWrapExport(
// for now we just give an option to not throw for this case.
// https://github.com/vitejs/vite-plugin-vue/blob/30a97c1ddbdfb0e23b7dc14a1d2fb609668b9987/packages/plugin-vue/src/main.ts#L372
if (node.type === 'ExportAllDeclaration') {
- if (node.exported?.type === 'Identifier') {
- tinyassert(node.source.type === 'Literal')
- const exportedName = node.exported.name
- const localName = `$$import_${exportedName}`
- output.remove(node.start, node.end)
- toAppend.push(`import * as ${localName} from ${node.source.raw}`)
- wrapExport(localName, exportedName)
- } else if (!options.ignoreExportAllDeclaration) {
+ if (!options.ignoreExportAllDeclaration) {
throw Object.assign(new Error('unsupported ExportAllDeclaration'), {
pos: node.start,
})