Skip to content

Commit 261bde2

Browse files
authored
feat(rsc): support optional single entry for loadModule (#1040)
1 parent 04cba94 commit 261bde2

File tree

5 files changed

+127
-19
lines changed

5 files changed

+127
-19
lines changed

packages/plugin-rsc/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,9 +224,9 @@ The plugin provides an additional helper for multi environment interaction.
224224

225225
#### `import.meta.viteRsc.loadModule`
226226

227-
- Type: `(environmentName: "ssr" | "rsc", entryName: string) => Promise<T>`
227+
- Type: `(environmentName: "ssr" | "rsc", entryName?: string) => Promise<T>`
228228

229-
This allows importing `ssr` environment module specified by `environments.ssr.build.rollupOptions.input[entryName]` inside `rsc` environment and vice versa.
229+
This allows importing `ssr` environment module specified by `environments.ssr.build.rollupOptions.input[entryName]` inside `rsc` environment and vice versa. When `entryName` is omitted, the function automatically uses the single entry from the target environment's `rollupOptions.input`.
230230

231231
During development, by default, this API assumes both `rsc` and `ssr` environments execute under the main Vite process as `RunnableDevEnvironment`. Internally, `loadModule` uses the global `__VITE_ENVIRONMENT_RUNNER_IMPORT__` function to import modules in the target environment (see [`__VITE_ENVIRONMENT_RUNNER_IMPORT__`](#__vite_environment_runner_import__) below).
232232

@@ -550,7 +550,7 @@ Types for global API are defined in `@vitejs/plugin-rsc/types`. For example, you
550550
```ts
551551
import.meta.viteRsc.loadModule
552552
// ^^^^^^^^^^
553-
// <T>(environmentName: string, entryName: string) => Promise<T>
553+
// <T>(environmentName: string, entryName?: string) => Promise<T>
554554
```
555555
556556
See also [Vite documentation](https://vite.dev/guide/api-hmr.html#intellisense-for-typescript) for `vite/client` types.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { test } from '@playwright/test'
2+
import { setupInlineFixture, useFixture } from './fixture'
3+
import { defineStarterTest } from './starter'
4+
5+
test.describe(() => {
6+
const root = 'examples/e2e/temp/single-entry'
7+
8+
test.beforeAll(async () => {
9+
await setupInlineFixture({
10+
src: 'examples/starter',
11+
dest: root,
12+
files: {
13+
'src/framework/entry.rsc.tsx': {
14+
edit: (s) => s.replace(`('ssr', 'index')`, `('ssr')`),
15+
},
16+
'vite.config.base.ts': { cp: 'vite.config.ts' },
17+
'vite.config.ts': /* js */ `
18+
import { defineConfig, mergeConfig } from 'vite'
19+
import baseConfig from './vite.config.base.ts'
20+
21+
const overrideConfig = defineConfig({
22+
environments: {
23+
ssr: {
24+
build: {
25+
rollupOptions: {
26+
input: './src/framework/entry.ssr.tsx',
27+
},
28+
},
29+
},
30+
},
31+
})
32+
33+
export default mergeConfig(baseConfig, overrideConfig)
34+
`,
35+
},
36+
})
37+
})
38+
39+
test.describe('dev-single-entry', () => {
40+
const f = useFixture({ root, mode: 'dev' })
41+
defineStarterTest(f)
42+
})
43+
44+
test.describe('build-single-entry', () => {
45+
const f = useFixture({ root, mode: 'build' })
46+
defineStarterTest(f)
47+
})
48+
})

packages/plugin-rsc/src/plugin.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
getFetchHandlerExport,
5050
sortObject,
5151
withRollupError,
52+
getFallbackRollupEntry,
5253
} from './plugins/utils'
5354
import { createDebug } from '@hiogawa/utils'
5455
import { scanBuildStripPlugin } from './plugins/scan'
@@ -609,7 +610,6 @@ export default function vitePluginRsc(
609610
environmentName: 'rsc',
610611
entryName: 'index',
611612
}
612-
613613
const entryFile = path.join(
614614
manager.config.environments[options.environmentName]!.build.outDir,
615615
`${options.entryName}.js`,
@@ -779,7 +779,8 @@ export default function vitePluginRsc(
779779
)) {
780780
const [argStart, argEnd] = match.indices![1]!
781781
const argCode = code.slice(argStart, argEnd).trim()
782-
const [environmentName, entryName] = evalValue(`[${argCode}]`)
782+
const [environmentName, entryName]: [string, string | undefined] =
783+
evalValue(`[${argCode}]`)
783784
let replacement: string
784785
if (
785786
this.environment.mode === 'dev' &&
@@ -793,8 +794,21 @@ export default function vitePluginRsc(
793794
assert(resolved, `[vite-rsc] failed to resolve entry '${source}'`)
794795
replacement = `globalThis.__VITE_ENVIRONMENT_RUNNER_IMPORT__(${JSON.stringify(environmentName)}, ${JSON.stringify(resolved.id)})`
795796
} else {
797+
const environment = manager.config.environments[environmentName]!
798+
const targetName =
799+
entryName ||
800+
getFallbackRollupEntry(environment.build.rollupOptions.input).name
796801
replacement = JSON.stringify(
797-
`__vite_rsc_load_module:${this.environment.name}:${environmentName}:${entryName}`,
802+
`__vite_rsc_load_module_start__:` +
803+
JSON.stringify({
804+
fromEnv: this.environment.name,
805+
toEnv: environmentName,
806+
// TODO: custom entyFileNames
807+
// we can probably use an intermediate file like "__vite_rsc_assets_manifest.js"
808+
// and generate during buildApp.
809+
targetFileName: `${targetName}.js`,
810+
}) +
811+
`:__vite_rsc_load_module_end__`,
798812
)
799813
}
800814
const [start, end] = match.indices![0]!
@@ -812,9 +826,15 @@ export default function vitePluginRsc(
812826
const { config } = manager
813827
const s = new MagicString(code)
814828
for (const match of code.matchAll(
815-
/['"]__vite_rsc_load_module:(\w+):(\w+):(\w+)['"]/dg,
829+
/[`'"]__vite_rsc_load_module_start__:([\s\S]*?):__vite_rsc_load_module_end__[`'"]/dg,
816830
)) {
817-
const [fromEnv, toEnv, entryName] = match.slice(1)
831+
const markerString = evalValue(match[0])
832+
const { fromEnv, toEnv, targetFileName } = JSON.parse(
833+
markerString.slice(
834+
'__vite_rsc_load_module_start__:'.length,
835+
-'__:vite_rsc_load_module_end__'.length,
836+
),
837+
)
818838
const importPath = normalizeRelativePath(
819839
path.relative(
820840
path.join(
@@ -824,8 +844,7 @@ export default function vitePluginRsc(
824844
),
825845
path.join(
826846
config.environments[toEnv!]!.build.outDir,
827-
// TODO: this breaks when custom entyFileNames
828-
`${entryName}.js`,
847+
targetFileName,
829848
),
830849
),
831850
)
@@ -851,7 +870,6 @@ export default function vitePluginRsc(
851870
url.searchParams,
852871
)
853872
assert(environmentName)
854-
assert(entryName)
855873
const environment = server.environments[
856874
environmentName
857875
] as RunnableDevEnvironment

packages/plugin-rsc/src/plugins/utils.ts

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import assert from 'node:assert'
21
import { createHash } from 'node:crypto'
2+
import path from 'node:path'
33
import {
44
normalizePath,
55
type Plugin,
@@ -62,17 +62,59 @@ export function normalizeRelativePath(s: string): string {
6262

6363
export function getEntrySource(
6464
config: Pick<ResolvedConfig, 'build'>,
65-
name: string = 'index',
65+
name?: string,
6666
): string {
6767
const input = config.build.rollupOptions.input
68-
assert(
68+
if (!name) {
69+
return getFallbackRollupEntry(input).source
70+
}
71+
if (
6972
typeof input === 'object' &&
70-
!Array.isArray(input) &&
71-
name in input &&
72-
typeof input[name] === 'string',
73+
!Array.isArray(input) &&
74+
name in input &&
75+
typeof input[name] === 'string'
76+
) {
77+
return input[name]
78+
}
79+
throw new Error(
7380
`[vite-rsc:getEntrySource] expected 'build.rollupOptions.input' to be an object with a '${name}' property that is a string, but got ${JSON.stringify(input)}`,
7481
)
75-
return input[name]
82+
}
83+
84+
export function getFallbackRollupEntry(
85+
input: Rollup.InputOptions['input'] = {},
86+
): {
87+
name: string
88+
source: string
89+
} {
90+
const inputEntries = Object.entries(normalizeRollupOpitonsInput(input))
91+
if (inputEntries.length === 1) {
92+
const [name, source] = inputEntries[0]!
93+
return { name, source }
94+
}
95+
throw new Error(
96+
`[vite-rsc] cannot determine fallback entry name from multiple entries, please specify the entry name explicitly`,
97+
)
98+
}
99+
100+
// normalize to object form
101+
// https://rollupjs.org/configuration-options/#input
102+
// https://rollupjs.org/configuration-options/#output-entryfilenames
103+
function normalizeRollupOpitonsInput(
104+
input: Rollup.InputOptions['input'] = {},
105+
): Record<string, string> {
106+
if (typeof input === 'string') {
107+
input = [input]
108+
}
109+
if (Array.isArray(input)) {
110+
return Object.fromEntries(
111+
input.map((file) => [
112+
path.basename(file).slice(0, -path.extname(file).length),
113+
file,
114+
]),
115+
)
116+
}
117+
return input
76118
}
77119

78120
export function hashString(v: string): string {

packages/plugin-rsc/types/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ declare global {
22
interface ImportMeta {
33
readonly viteRsc: {
44
loadCss: (importer?: string) => import('react').ReactNode
5-
loadModule: <T>(environmentName: string, entryName: string) => Promise<T>
5+
loadModule: <T>(environmentName: string, entryName?: string) => Promise<T>
66
loadBootstrapScriptContent: (entryName: string) => Promise<string>
77
}
88
}

0 commit comments

Comments
 (0)