Skip to content

Commit f2e28b2

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

File tree

6 files changed

+306
-38
lines changed

6 files changed

+306
-38
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: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import assert from "node:assert";
2-
import { createHash } from "node:crypto";
32
import MagicString from "magic-string";
43
import { toNodeHandler } from "srvx/node";
5-
import {
6-
type Plugin,
7-
type ResolvedConfig,
8-
isRunnableDevEnvironment,
9-
} from "vite";
4+
import { type Plugin, isRunnableDevEnvironment } from "vite";
105
import type { ImportAssetsOptions, ImportAssetsResult } from "../types/shared";
6+
import { getEntrySource } from "./plugins/utils";
7+
import { evalValue } from "./plugins/vite-utils";
118

129
type FullstackPluginOptions = {
1310
serverHandler?: boolean;
@@ -172,35 +169,3 @@ export default function vitePluginFullstack(
172169
},
173170
];
174171
}
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+
type Plugin,
5+
type ResolvedConfig,
6+
type Rollup,
7+
normalizePath,
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: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// misc utilities copied from vite
2+
3+
import fs from "node:fs";
4+
import path from "node:path";
5+
import { stripVTControlCharacters as strip } from "node:util";
6+
import type { DevEnvironment, ErrorPayload, Rollup } from "vite";
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 =
52+
typeof process !== "undefined" && process.platform === "win32";
53+
54+
export function injectQuery(url: string, queryToInject: string): string {
55+
const { file, postfix } = splitFileAndPostfix(url);
56+
const normalizedFile = isWindows ? slash(file) : file;
57+
return `${normalizedFile}?${queryToInject}${
58+
postfix[0] === "?" ? `&${postfix.slice(1)}` : /* hash only */ postfix
59+
}`;
60+
}
61+
62+
export function joinUrlSegments(a: string, b: string): string {
63+
if (!a || !b) {
64+
return a || b || "";
65+
}
66+
if (a.endsWith("/")) {
67+
a = a.substring(0, a.length - 1);
68+
}
69+
if (b[0] !== "/") {
70+
b = "/" + b;
71+
}
72+
return a + b;
73+
}
74+
75+
export function normalizeResolvedIdToUrl(
76+
environment: DevEnvironment,
77+
url: string,
78+
resolved: Rollup.PartialResolvedId,
79+
): string {
80+
const root = environment.config.root;
81+
const depsOptimizer = environment.depsOptimizer;
82+
83+
// normalize all imports into resolved URLs
84+
// e.g. `import 'foo'` -> `import '/@fs/.../node_modules/foo/index.js'`
85+
if (resolved.id.startsWith(withTrailingSlash(root))) {
86+
// in root: infer short absolute path from root
87+
url = resolved.id.slice(root.length);
88+
} else if (
89+
depsOptimizer?.isOptimizedDepFile(resolved.id) ||
90+
// vite-plugin-react isn't following the leading \0 virtual module convention.
91+
// This is a temporary hack to avoid expensive fs checks for React apps.
92+
// We'll remove this as soon we're able to fix the react plugins.
93+
(resolved.id !== "/@react-refresh" &&
94+
path.isAbsolute(resolved.id) &&
95+
fs.existsSync(cleanUrl(resolved.id)))
96+
) {
97+
// an optimized deps may not yet exists in the filesystem, or
98+
// a regular file exists but is out of root: rewrite to absolute /@fs/ paths
99+
url = path.posix.join(FS_PREFIX, resolved.id);
100+
} else {
101+
url = resolved.id;
102+
}
103+
104+
// if the resolved id is not a valid browser import specifier,
105+
// prefix it to make it valid. We will strip this before feeding it
106+
// back into the transform pipeline
107+
if (url[0] !== "." && url[0] !== "/") {
108+
url = wrapId(resolved.id);
109+
}
110+
111+
return url;
112+
}
113+
114+
export function normalizeViteImportAnalysisUrl(
115+
environment: DevEnvironment,
116+
id: string,
117+
): string {
118+
let url = normalizeResolvedIdToUrl(environment, id, { id });
119+
120+
// https://github.com/vitejs/vite/blob/c18ce868c4d70873406e9f7d1b2d0a03264d2168/packages/vite/src/node/plugins/importAnalysis.ts#L416
121+
if (environment.config.consumer === "client") {
122+
const mod = environment.moduleGraph.getModuleById(id);
123+
if (mod && mod.lastHMRTimestamp > 0) {
124+
url = injectQuery(url, `t=${mod.lastHMRTimestamp}`);
125+
}
126+
}
127+
128+
return url;
129+
}
130+
131+
// error formatting
132+
// https://github.com/vitejs/vite/blob/8033e5bf8d3ff43995d0620490ed8739c59171dd/packages/vite/src/node/server/middlewares/error.ts#L11
133+
134+
type RollupError = Rollup.RollupError;
135+
136+
export function prepareError(err: Error | RollupError): ErrorPayload["err"] {
137+
// only copy the information we need and avoid serializing unnecessary
138+
// properties, since some errors may attach full objects (e.g. PostCSS)
139+
return {
140+
message: strip(err.message),
141+
stack: strip(cleanStack(err.stack || "")),
142+
id: (err as RollupError).id,
143+
frame: strip((err as RollupError).frame || ""),
144+
plugin: (err as RollupError).plugin,
145+
pluginCode: (err as RollupError).pluginCode?.toString(),
146+
loc: (err as RollupError).loc,
147+
};
148+
}
149+
150+
function cleanStack(stack: string) {
151+
return stack
152+
.split(/\n/)
153+
.filter((l) => /^\s*at/.test(l))
154+
.join("\n");
155+
}
156+
157+
// https://github.com/vitejs/vite/blob/ea9aed7ebcb7f4be542bd2a384cbcb5a1e7b31bd/packages/vite/src/node/utils.ts#L1469-L1475
158+
export function evalValue<T = any>(rawValue: string): T {
159+
const fn = new Function(`
160+
var console, exports, global, module, process, require
161+
return (\n${rawValue}\n)
162+
`);
163+
return fn();
164+
}
165+
166+
// https://github.com/vitejs/vite/blob/84079a84ad94de4c1ef4f1bdb2ab448ff2c01196/packages/vite/src/node/utils.ts#L321
167+
export const directRequestRE: RegExp = /(\?|&)direct=?(?:&|$)/;

packages/fullstack/src/runtime.ts

Whitespace-only changes.

0 commit comments

Comments
 (0)