Skip to content

Commit 2fe7f44

Browse files
hi-ogawaclaude
andcommitted
docs(rsc): add manifest-based approach for viteRsc.import
Document the preferred implementation using static import functions in a manifest file, which supports custom entryFileNames and enables proper static analysis by bundlers. Key additions: - Manifest with static import functions (not dynamic lookups) - Bidirectional support (RSC → SSR and SSR → RSC) - Updated data flow showing entry injection after both scans - Per-environment manifest generation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3591f12 commit 2fe7f44

File tree

1 file changed

+200
-7
lines changed

1 file changed

+200
-7
lines changed

packages/plugin-rsc/docs/notes/2026-01-16-vitersc-import.md

Lines changed: 200 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -246,12 +246,205 @@ import.meta.viteRsc.importAsset('./entry.browser', { asset: 'client' }) // retur
246246

247247
This avoids breaking changes and keeps each method's return type clear.
248248

249+
## Manifest-Based Approach (Preferred)
250+
251+
The initial implementation hardcoded output filenames (`${entryName}.js`), which breaks with custom `entryFileNames` config. A manifest approach solves this.
252+
253+
### Problem with Hardcoded Filenames
254+
255+
```ts
256+
// renderChunk - current implementation
257+
const targetFileName = `${entryName}.js` // ❌ Breaks with custom entryFileNames
258+
```
259+
260+
Build order means RSC builds before SSR:
261+
262+
```
263+
rsc (real) → client → ssr (real)
264+
↑ ↑
265+
RSC renderChunk SSR output filenames known
266+
```
267+
268+
### Solution: Manifest with Static Imports
269+
270+
Generate a manifest file with **static import functions** that bundlers can analyze:
271+
272+
```ts
273+
// __vite_rsc_env_imports_manifest.js (generated in buildApp after SSR build)
274+
export default {
275+
'/abs/path/to/entry.ssr.tsx': () => import('../ssr/entry.ssr.js'),
276+
}
277+
```
278+
279+
Transform emits manifest lookup:
280+
281+
```ts
282+
// Original:
283+
await import.meta.viteRsc
284+
.import('./entry.ssr', { environment: 'ssr' })
285+
(
286+
// Build transform to:
287+
await import('./__vite_rsc_env_imports_manifest.js'),
288+
)
289+
.default['/abs/path/to/entry.ssr.tsx']()
290+
```
291+
292+
### Why Static Import Functions?
293+
294+
Dynamic imports like `import(manifest['key'])` break post-bundling and static analysis. By using functions with static import strings, bundlers can:
295+
296+
- Analyze the dependency graph
297+
- Apply optimizations (tree-shaking, code-splitting)
298+
- Work correctly with further bundling
299+
300+
### Implementation Changes
301+
302+
**1. Transform (in RSC):**
303+
304+
- Dev: unchanged (`globalThis.__VITE_ENVIRONMENT_RUNNER_IMPORT__`)
305+
- Build: emit manifest lookup instead of markers
306+
307+
```ts
308+
// Build mode transform
309+
const manifestImport = `await import('./__vite_rsc_env_imports_manifest.js')`
310+
replacement = `(${manifestImport}).default[${JSON.stringify(resolvedId)}]()`
311+
```
312+
313+
**2. Remove renderChunk:**
314+
315+
- No longer needed for this feature (markers eliminated)
316+
317+
**3. SSR generateBundle:**
318+
319+
- Track output filenames for discovered entries
320+
321+
```ts
322+
generateBundle(options, bundle) {
323+
for (const [fileName, chunk] of Object.entries(bundle)) {
324+
if (chunk.type === 'chunk' && chunk.isEntry) {
325+
const resolvedId = chunk.facadeModuleId
326+
if (resolvedId && resolvedId in manager.environmentImportMetaMap) {
327+
manager.environmentImportOutputMap[resolvedId] = fileName
328+
}
329+
}
330+
}
331+
}
332+
```
333+
334+
**4. buildApp - writeEnvironmentImportsManifest:**
335+
336+
- Generate manifest after SSR build completes
337+
- Calculate relative paths from manifest location to target chunks
338+
339+
```ts
340+
function writeEnvironmentImportsManifest() {
341+
const rscOutDir = config.environments.rsc.build.outDir
342+
const manifestPath = path.join(
343+
rscOutDir,
344+
'__vite_rsc_env_imports_manifest.js',
345+
)
346+
347+
let code = 'export default {\n'
348+
for (const [resolvedId, meta] of Object.entries(
349+
manager.environmentImportMetaMap,
350+
)) {
351+
const outputFileName = manager.environmentImportOutputMap[resolvedId]
352+
const targetOutDir = config.environments[meta.targetEnv].build.outDir
353+
const relativePath = normalizeRelativePath(
354+
path.relative(rscOutDir, path.join(targetOutDir, outputFileName)),
355+
)
356+
code += ` ${JSON.stringify(resolvedId)}: () => import(${JSON.stringify(relativePath)}),\n`
357+
}
358+
code += '}\n'
359+
360+
fs.writeFileSync(manifestPath, code)
361+
}
362+
```
363+
364+
### Bidirectional Support
365+
366+
Both directions are supported:
367+
368+
- **RSC → SSR**: `import('./entry.ssr', { environment: 'ssr' })` in RSC code
369+
- **SSR → RSC**: `import('./entry.rsc', { environment: 'rsc' })` in SSR code
370+
371+
This is similar to "use client" / "use server" discovery - each scan phase can discover entries for other environments.
372+
373+
**Key insight**: Entry injection must happen AFTER both scan phases but BEFORE real builds.
374+
375+
### Data Flow
376+
377+
```
378+
┌─────────────────────────────────────────────────────────────────────────┐
379+
│ RSC Scan │
380+
│ transform: discover viteRsc.import → populate environmentImportMetaMap│
381+
│ (discovers RSC → SSR imports) │
382+
└─────────────────────────────────────────────────────────────────────────┘
383+
384+
┌─────────────────────────────────────────────────────────────────────────┐
385+
│ SSR Scan │
386+
│ transform: discover viteRsc.import → populate environmentImportMetaMap│
387+
│ (discovers SSR → RSC imports) │
388+
└─────────────────────────────────────────────────────────────────────────┘
389+
390+
┌─────────────────────────────────────────────────────────────────────────┐
391+
│ Inject Discovered Entries (in buildApp, after both scans) │
392+
│ for each meta in environmentImportMetaMap: │
393+
│ inject meta.resolvedId into target environment's rollupOptions.input│
394+
└─────────────────────────────────────────────────────────────────────────┘
395+
396+
┌─────────────────────────────────────────────────────────────────────────┐
397+
│ RSC Real Build │
398+
│ transform: emit manifest lookup code │
399+
│ generateBundle: track resolvedId → outputFileName (for SSR → RSC) │
400+
└─────────────────────────────────────────────────────────────────────────┘
401+
402+
┌─────────────────────────────────────────────────────────────────────────┐
403+
│ Client Build │
404+
└─────────────────────────────────────────────────────────────────────────┘
405+
406+
┌─────────────────────────────────────────────────────────────────────────┐
407+
│ SSR Real Build │
408+
│ transform: emit manifest lookup code │
409+
│ generateBundle: track resolvedId → outputFileName (for RSC → SSR) │
410+
└─────────────────────────────────────────────────────────────────────────┘
411+
412+
┌─────────────────────────────────────────────────────────────────────────┐
413+
│ buildApp Post-Build │
414+
│ writeEnvironmentImportsManifest: │
415+
│ - Write manifest to RSC output (for RSC → SSR imports) │
416+
│ - Write manifest to SSR output (for SSR → RSC imports) │
417+
└─────────────────────────────────────────────────────────────────────────┘
418+
```
419+
420+
### Manifest Per Source Environment
421+
422+
Each source environment gets its own manifest with imports pointing to target environments:
423+
424+
```ts
425+
// dist/rsc/__vite_rsc_env_imports_manifest.js (for RSC → SSR)
426+
export default {
427+
'/abs/path/entry.ssr.tsx': () => import('../ssr/entry.ssr.js'),
428+
}
429+
430+
// dist/ssr/__vite_rsc_env_imports_manifest.js (for SSR → RSC)
431+
export default {
432+
'/abs/path/entry.rsc.tsx': () => import('../rsc/index.js'),
433+
}
434+
```
435+
249436
## Implementation Steps
250437

251-
1. [ ] Add `environmentImportMetaMap` to RscPluginManager
252-
2. [ ] Clean up `import.ts`: add imports, parameterize with manager
253-
3. [ ] Implement transform to discover and track imports
254-
4. [ ] Add `buildStart` hook to emit discovered entries in target environment
255-
5. [ ] Implement renderChunk to resolve markers
256-
6. [ ] Test with basic example
257-
7. [ ] Update documentation
438+
1. [x] Add `environmentImportMetaMap` to RscPluginManager
439+
2. [x] Clean up `import-environment.ts`: add imports, parameterize with manager
440+
3. [x] Implement transform to discover and track imports
441+
4. [x] Inject discovered entries into target environment's input
442+
5. [ ] **Fix**: Move entry injection to AFTER both scans (currently after RSC scan only)
443+
6. [ ] Update transform to emit manifest lookup (build mode)
444+
7. [ ] Remove renderChunk marker replacement
445+
8. [ ] Add `environmentImportOutputMap` to track resolvedId → outputFileName
446+
9. [ ] Add generateBundle hook to populate output map (in both RSC and SSR)
447+
10. [ ] Add `writeEnvironmentImportsManifest` in buildApp (per source environment)
448+
11. [ ] Test with basic example (RSC → SSR)
449+
12. [ ] Test bidirectional (SSR → RSC) if applicable
450+
13. [ ] Update documentation

0 commit comments

Comments
 (0)