Skip to content

Commit 6f47bae

Browse files
committed
chore: copy utils from @vitejs/plugin-rsc
1 parent 75db0f7 commit 6f47bae

File tree

6 files changed

+304
-34
lines changed

6 files changed

+304
-34
lines changed

packages/fullstack/examples/basic/src/entry.client.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ function main() {
77
createRoot(domRoot).render(<App />);
88

99
if (import.meta.hot) {
10+
// TODO
1011
import.meta.hot.on("fullstack:update", (e) => {
1112
console.log("[fullstack:update]", e);
1213
window.location.reload();

packages/fullstack/src/plugin.ts

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import assert from "node:assert";
2-
import { createHash } from "node:crypto";
32
import MagicString from "magic-string";
43
import { toNodeHandler } from "srvx/node";
54
import {
65
type Plugin,
7-
type ResolvedConfig,
86
isRunnableDevEnvironment,
97
} from "vite";
108
import type { ImportAssetsOptions, ImportAssetsResult } from "../types/shared";
9+
import { evalValue } from "./plugins/vite-utils";
10+
import { getEntrySource } from "./plugins/utils";
1111

1212
type FullstackPluginOptions = {
1313
serverHandler?: boolean;
@@ -172,35 +172,3 @@ export default function vitePluginFullstack(
172172
},
173173
];
174174
}
175-
176-
function getEntrySource(
177-
config: Pick<ResolvedConfig, "build">,
178-
name: string = "index",
179-
): string {
180-
const input = config.build.rollupOptions.input;
181-
if (typeof input === "string") {
182-
return input;
183-
}
184-
assert(
185-
typeof input === "object" &&
186-
!Array.isArray(input) &&
187-
name in input &&
188-
typeof input[name] === "string",
189-
`[vite-rsc:getEntrySource] expected 'build.rollupOptions.input' to be an object with a '${name}' property that is a string, but got ${JSON.stringify(input)}`,
190-
);
191-
return input[name];
192-
}
193-
194-
// https://github.com/vitejs/vite/blob/ea9aed7ebcb7f4be542bd2a384cbcb5a1e7b31bd/packages/vite/src/node/utils.ts#L1469-L1475
195-
function evalValue<T = any>(rawValue: string): T {
196-
const fn = new Function(`
197-
var console, exports, global, module, process, require
198-
return (\n${rawValue}\n)
199-
`);
200-
return fn();
201-
}
202-
203-
hashString;
204-
function hashString(v: string): string {
205-
return createHash("sha256").update(v).digest().toString("hex").slice(0, 12);
206-
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
type CssVirtual = {
2+
id: string
3+
type: 'ssr' | 'rsc'
4+
}
5+
6+
export function toCssVirtual({ id, type }: CssVirtual) {
7+
// ensure other plugins treat it as a plain js file
8+
// e.g. https://github.com/vitejs/rolldown-vite/issues/372#issuecomment-3193401601
9+
return `virtual:vite-rsc/css?type=${type}&id=${encodeURIComponent(id)}&lang.js`
10+
}
11+
12+
export function parseCssVirtual(id: string): CssVirtual | undefined {
13+
if (id.startsWith('\0virtual:vite-rsc/css?')) {
14+
return parseIdQuery(id).query as any
15+
}
16+
}
17+
18+
// https://github.com/vitejs/vite-plugin-vue/blob/06931b1ea2b9299267374cb8eb4db27c0626774a/packages/plugin-vue/src/utils/query.ts#L13
19+
export function parseIdQuery(id: string): {
20+
filename: string
21+
query: {
22+
[k: string]: string
23+
}
24+
} {
25+
if (!id.includes('?')) return { filename: id, query: {} }
26+
const [filename, rawQuery] = id.split(`?`, 2) as [string, string]
27+
const query = Object.fromEntries(new URLSearchParams(rawQuery))
28+
return { filename, query }
29+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import assert from 'node:assert'
2+
import { createHash } from 'node:crypto'
3+
import {
4+
normalizePath,
5+
type Plugin,
6+
type ResolvedConfig,
7+
type Rollup,
8+
} from 'vite'
9+
10+
export function sortObject<T extends object>(o: T) {
11+
return Object.fromEntries(
12+
Object.entries(o).sort(([a], [b]) => a.localeCompare(b)),
13+
) as T
14+
}
15+
16+
// Rethrow transform error through `this.error` with `error.pos`
17+
export function withRollupError<F extends (...args: any[]) => any>(
18+
ctx: Rollup.TransformPluginContext,
19+
f: F,
20+
): F {
21+
function processError(e: any): never {
22+
if (e && typeof e === 'object' && typeof e.pos === 'number') {
23+
return ctx.error(e, e.pos)
24+
}
25+
throw e
26+
}
27+
return function (this: any, ...args: any[]) {
28+
try {
29+
const result = f.apply(this, args)
30+
if (result instanceof Promise) {
31+
return result.catch((e: any) => processError(e))
32+
}
33+
return result
34+
} catch (e: any) {
35+
processError(e)
36+
}
37+
} as F
38+
}
39+
40+
export function createVirtualPlugin(
41+
name: string,
42+
load: Plugin['load'],
43+
): Plugin {
44+
name = 'virtual:' + name
45+
return {
46+
name: `rsc:virtual-${name}`,
47+
resolveId(source, _importer, _options) {
48+
return source === name ? '\0' + name : undefined
49+
},
50+
load(id, options) {
51+
if (id === '\0' + name) {
52+
return (load as Function).apply(this, [id, options])
53+
}
54+
},
55+
}
56+
}
57+
58+
export function normalizeRelativePath(s: string): string {
59+
s = normalizePath(s)
60+
return s[0] === '.' ? s : './' + s
61+
}
62+
63+
export function getEntrySource(
64+
config: Pick<ResolvedConfig, 'build'>,
65+
name: string = 'index',
66+
): string {
67+
const input = config.build.rollupOptions.input
68+
if (typeof input === 'string') {
69+
return input;
70+
}
71+
assert(
72+
typeof input === 'object' &&
73+
!Array.isArray(input) &&
74+
name in input &&
75+
typeof input[name] === 'string',
76+
`[vite-rsc:getEntrySource] expected 'build.rollupOptions.input' to be an object with a '${name}' property that is a string, but got ${JSON.stringify(input)}`,
77+
)
78+
return input[name]
79+
}
80+
81+
export function hashString(v: string): string {
82+
return createHash('sha256').update(v).digest().toString('hex').slice(0, 12)
83+
}
84+
85+
// normalize server entry exports to align with server runtimes
86+
// https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/
87+
// https://srvx.h3.dev/guide
88+
// https://vercel.com/docs/functions/functions-api-reference?framework=other#fetch-web-standard
89+
// https://github.com/jacob-ebey/rsbuild-rsc-playground/blob/eb1a54afa49cbc5ff93c315744d7754d5ed63498/plugin/fetch-server.ts#L59-L79
90+
export function getFetchHandlerExport(exports: object): any {
91+
if ('default' in exports) {
92+
const default_ = exports.default
93+
if (
94+
default_ &&
95+
typeof default_ === 'object' &&
96+
'fetch' in default_ &&
97+
typeof default_.fetch === 'function'
98+
) {
99+
return default_.fetch
100+
}
101+
if (typeof default_ === 'function') {
102+
return default_
103+
}
104+
}
105+
throw new Error('Invalid server handler entry')
106+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// misc utilities copied from vite
2+
3+
import fs from 'node:fs'
4+
import path from 'node:path'
5+
import type { DevEnvironment, ErrorPayload, Rollup } from 'vite'
6+
import { stripVTControlCharacters as strip } from 'node:util'
7+
8+
export const VALID_ID_PREFIX = `/@id/`
9+
10+
export const NULL_BYTE_PLACEHOLDER = `__x00__`
11+
12+
export const FS_PREFIX = `/@fs/`
13+
14+
export function wrapId(id: string): string {
15+
return id.startsWith(VALID_ID_PREFIX)
16+
? id
17+
: VALID_ID_PREFIX + id.replace('\0', NULL_BYTE_PLACEHOLDER)
18+
}
19+
20+
export function unwrapId(id: string): string {
21+
return id.startsWith(VALID_ID_PREFIX)
22+
? id.slice(VALID_ID_PREFIX.length).replace(NULL_BYTE_PLACEHOLDER, '\0')
23+
: id
24+
}
25+
26+
export function withTrailingSlash(path: string): string {
27+
if (path[path.length - 1] !== '/') {
28+
return `${path}/`
29+
}
30+
return path
31+
}
32+
33+
const postfixRE = /[?#].*$/
34+
export function cleanUrl(url: string): string {
35+
return url.replace(postfixRE, '')
36+
}
37+
38+
export function splitFileAndPostfix(path: string): {
39+
file: string
40+
postfix: string
41+
} {
42+
const file = cleanUrl(path)
43+
return { file, postfix: path.slice(file.length) }
44+
}
45+
46+
const windowsSlashRE = /\\/g
47+
export function slash(p: string): string {
48+
return p.replace(windowsSlashRE, '/')
49+
}
50+
51+
const isWindows = typeof process !== 'undefined' && process.platform === 'win32'
52+
53+
export function injectQuery(url: string, queryToInject: string): string {
54+
const { file, postfix } = splitFileAndPostfix(url)
55+
const normalizedFile = isWindows ? slash(file) : file
56+
return `${normalizedFile}?${queryToInject}${
57+
postfix[0] === '?' ? `&${postfix.slice(1)}` : /* hash only */ postfix
58+
}`
59+
}
60+
61+
export function joinUrlSegments(a: string, b: string): string {
62+
if (!a || !b) {
63+
return a || b || ''
64+
}
65+
if (a.endsWith('/')) {
66+
a = a.substring(0, a.length - 1)
67+
}
68+
if (b[0] !== '/') {
69+
b = '/' + b
70+
}
71+
return a + b
72+
}
73+
74+
export function normalizeResolvedIdToUrl(
75+
environment: DevEnvironment,
76+
url: string,
77+
resolved: Rollup.PartialResolvedId,
78+
): string {
79+
const root = environment.config.root
80+
const depsOptimizer = environment.depsOptimizer
81+
82+
// normalize all imports into resolved URLs
83+
// e.g. `import 'foo'` -> `import '/@fs/.../node_modules/foo/index.js'`
84+
if (resolved.id.startsWith(withTrailingSlash(root))) {
85+
// in root: infer short absolute path from root
86+
url = resolved.id.slice(root.length)
87+
} else if (
88+
depsOptimizer?.isOptimizedDepFile(resolved.id) ||
89+
// vite-plugin-react isn't following the leading \0 virtual module convention.
90+
// This is a temporary hack to avoid expensive fs checks for React apps.
91+
// We'll remove this as soon we're able to fix the react plugins.
92+
(resolved.id !== '/@react-refresh' &&
93+
path.isAbsolute(resolved.id) &&
94+
fs.existsSync(cleanUrl(resolved.id)))
95+
) {
96+
// an optimized deps may not yet exists in the filesystem, or
97+
// a regular file exists but is out of root: rewrite to absolute /@fs/ paths
98+
url = path.posix.join(FS_PREFIX, resolved.id)
99+
} else {
100+
url = resolved.id
101+
}
102+
103+
// if the resolved id is not a valid browser import specifier,
104+
// prefix it to make it valid. We will strip this before feeding it
105+
// back into the transform pipeline
106+
if (url[0] !== '.' && url[0] !== '/') {
107+
url = wrapId(resolved.id)
108+
}
109+
110+
return url
111+
}
112+
113+
export function normalizeViteImportAnalysisUrl(
114+
environment: DevEnvironment,
115+
id: string,
116+
): string {
117+
let url = normalizeResolvedIdToUrl(environment, id, { id })
118+
119+
// https://github.com/vitejs/vite/blob/c18ce868c4d70873406e9f7d1b2d0a03264d2168/packages/vite/src/node/plugins/importAnalysis.ts#L416
120+
if (environment.config.consumer === 'client') {
121+
const mod = environment.moduleGraph.getModuleById(id)
122+
if (mod && mod.lastHMRTimestamp > 0) {
123+
url = injectQuery(url, `t=${mod.lastHMRTimestamp}`)
124+
}
125+
}
126+
127+
return url
128+
}
129+
130+
// error formatting
131+
// https://github.com/vitejs/vite/blob/8033e5bf8d3ff43995d0620490ed8739c59171dd/packages/vite/src/node/server/middlewares/error.ts#L11
132+
133+
type RollupError = Rollup.RollupError
134+
135+
export function prepareError(err: Error | RollupError): ErrorPayload['err'] {
136+
// only copy the information we need and avoid serializing unnecessary
137+
// properties, since some errors may attach full objects (e.g. PostCSS)
138+
return {
139+
message: strip(err.message),
140+
stack: strip(cleanStack(err.stack || '')),
141+
id: (err as RollupError).id,
142+
frame: strip((err as RollupError).frame || ''),
143+
plugin: (err as RollupError).plugin,
144+
pluginCode: (err as RollupError).pluginCode?.toString(),
145+
loc: (err as RollupError).loc,
146+
}
147+
}
148+
149+
function cleanStack(stack: string) {
150+
return stack
151+
.split(/\n/)
152+
.filter((l) => /^\s*at/.test(l))
153+
.join('\n')
154+
}
155+
156+
// https://github.com/vitejs/vite/blob/ea9aed7ebcb7f4be542bd2a384cbcb5a1e7b31bd/packages/vite/src/node/utils.ts#L1469-L1475
157+
export function evalValue<T = any>(rawValue: string): T {
158+
const fn = new Function(`
159+
var console, exports, global, module, process, require
160+
return (\n${rawValue}\n)
161+
`)
162+
return fn()
163+
}
164+
165+
// https://github.com/vitejs/vite/blob/84079a84ad94de4c1ef4f1bdb2ab448ff2c01196/packages/vite/src/node/utils.ts#L321
166+
export const directRequestRE: RegExp = /(\?|&)direct=?(?:&|$)/

packages/fullstack/src/runtime.ts

Whitespace-only changes.

0 commit comments

Comments
 (0)