Skip to content

Conversation

@hi-ogawa
Copy link
Contributor

Summary

  • Add import.meta.viteRsc.importAsset API for importing client assets from server environments (SSR/RSC)
  • Returns asset URL, providing a more flexible alternative to loadBootstrapScriptContent
  • Supports entry: true option for HMR support in dev mode via virtual wrapper

API Signature:

importAsset: (specifier: string, options?: { entry?: boolean }) => Promise<{ url: string }>

Usage Example:

// In SSR entry
const asset = await import.meta.viteRsc.importAsset("./entry.browser.tsx", { entry: true });
const bootstrapScriptContent = `import(${JSON.stringify(asset.url)})`;

Test plan

  • TypeScript check passes (pnpm -C packages/plugin-rsc tsc --noEmit)
  • Dev server starts successfully
  • Build completes and generates manifest in dist/ssr/__vite_rsc_asset_imports_manifest.js
  • Manual testing of dev mode with entry: true (virtual wrapper with HMR)
  • Manual testing of dev mode with entry: false (direct URL)
  • Manual testing of production build

🤖 Generated with Claude Code

hi-ogawa and others added 2 commits January 19, 2026 18:35
Add a new `importAsset` API that allows importing client assets from server
environments (SSR/RSC), returning the asset URL. This provides a more flexible,
specifier-based approach that can eventually replace `loadBootstrapScriptContent`.

API: `importAsset(specifier, options?) => Promise<{ url: string }>`

Features:
- Dev mode with `entry: true`: Uses virtual wrapper with HMR support
- Dev mode with `entry: false`: Returns direct file URL
- Build mode: Returns URL from generated manifest

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Instead of generating a separate __vite_rsc_asset_imports_manifest.js file,
include importAssets in the existing __vite_rsc_assets_manifest.js as
AssetsManifest.importAssets.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
// Build: use existing assets manifest
// Use relative ID for stable builds across different machines
const relativeId = manager.toRelativeId(resolvedId)
replacement = `(async () => (await import("virtual:vite-rsc/assets-manifest")).default.importAssets[${JSON.stringify(relativeId)}])()`

Check warning

Code scanning / CodeQL

Improper code sanitization Medium

Code construction depends on an
improperly sanitized value
.

Copilot Autofix

AI 7 days ago

In general, whenever user-influenced strings are embedded into dynamically constructed JavaScript source, you must escape additional “unsafe” characters beyond what JSON.stringify handles for HTML/script contexts, particularly <, >, /, backslash, control characters, and U+2028/U+2029. The referenced pattern uses a small helper (escapeUnsafeChars) applied after JSON.stringify to ensure the resulting string literal is safe even when inlined into <script>.

The best targeted fix here is to introduce a small local escape helper in packages/plugin-rsc/src/plugins/import-asset.ts and apply it to JSON.stringify(relativeId) in the build-mode replacement expression. We will: (1) define a charMap and escapeUnsafeChars function near the top of this file, using the same mapping as in the background, and (2) change line 188 so that it uses escapeUnsafeChars(JSON.stringify(relativeId)). This preserves all existing behavior (the manifest still uses the same key value from relativeId) while ensuring the generated virtual module source cannot contain problematic raw characters in that location.

No new external imports are needed; we can implement the helper with String.prototype.replace. All edits stay within packages/plugin-rsc/src/plugins/import-asset.ts in the provided regions.

Suggested changeset 1
packages/plugin-rsc/src/plugins/import-asset.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/plugin-rsc/src/plugins/import-asset.ts b/packages/plugin-rsc/src/plugins/import-asset.ts
--- a/packages/plugin-rsc/src/plugins/import-asset.ts
+++ b/packages/plugin-rsc/src/plugins/import-asset.ts
@@ -14,6 +14,28 @@
 const ASSET_IMPORTS_CLIENT_ENTRY_FALLBACK =
   'virtual:vite-rsc/asset-imports-client-entry-fallback'
 
+const __unsafeCharMap: Record<string, string> = {
+  '<': '\\u003C',
+  '>': '\\u003E',
+  '/': '\\u002F',
+  '\\': '\\\\',
+  '\b': '\\b',
+  '\f': '\\f',
+  '\n': '\\n',
+  '\r': '\\r',
+  '\t': '\\t',
+  '\0': '\\0',
+  '\u2028': '\\u2028',
+  '\u2029': '\\u2029',
+}
+
+function escapeUnsafeChars(str: string): string {
+  return str.replace(
+    /[<>/\\\b\f\n\r\t\0\u2028\u2029]/g,
+    (ch) => __unsafeCharMap[ch] ?? ch,
+  )
+}
+
 export type AssetImportMeta = {
   resolvedId: string
   sourceEnv: string
@@ -185,7 +207,9 @@
               // Build: use existing assets manifest
               // Use relative ID for stable builds across different machines
               const relativeId = manager.toRelativeId(resolvedId)
-              replacement = `(async () => (await import("virtual:vite-rsc/assets-manifest")).default.importAssets[${JSON.stringify(relativeId)}])()`
+              replacement = `(async () => (await import("virtual:vite-rsc/assets-manifest")).default.importAssets[${escapeUnsafeChars(
+                JSON.stringify(relativeId),
+              )}])()`
             }
 
             const [start, end] = match.indices![0]!
EOF
@@ -14,6 +14,28 @@
const ASSET_IMPORTS_CLIENT_ENTRY_FALLBACK =
'virtual:vite-rsc/asset-imports-client-entry-fallback'

const __unsafeCharMap: Record<string, string> = {
'<': '\\u003C',
'>': '\\u003E',
'/': '\\u002F',
'\\': '\\\\',
'\b': '\\b',
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'\0': '\\0',
'\u2028': '\\u2028',
'\u2029': '\\u2029',
}

function escapeUnsafeChars(str: string): string {
return str.replace(
/[<>/\\\b\f\n\r\t\0\u2028\u2029]/g,
(ch) => __unsafeCharMap[ch] ?? ch,
)
}

export type AssetImportMeta = {
resolvedId: string
sourceEnv: string
@@ -185,7 +207,9 @@
// Build: use existing assets manifest
// Use relative ID for stable builds across different machines
const relativeId = manager.toRelativeId(resolvedId)
replacement = `(async () => (await import("virtual:vite-rsc/assets-manifest")).default.importAssets[${JSON.stringify(relativeId)}])()`
replacement = `(async () => (await import("virtual:vite-rsc/assets-manifest")).default.importAssets[${escapeUnsafeChars(
JSON.stringify(relativeId),
)}])()`
}

const [start, end] = match.indices![0]!
Copilot is powered by AI and may make mistakes. Always verify output.
hi-ogawa and others added 4 commits January 19, 2026 18:44
Ensure the client environment has at least one entry when no other entries
exist, similar to how import-environment.ts handles non-client environments.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Test that importAsset can replace loadBootstrapScriptContent for loading
client entry URLs in both dev and build modes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants