Skip to content

Commit 72b4a22

Browse files
move utils
1 parent 3861515 commit 72b4a22

File tree

2 files changed

+319
-315
lines changed

2 files changed

+319
-315
lines changed

src/ssr-plugin-utils.ts

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import { dirname, relative, resolve } from "node:path";
2+
import type { Component } from "@builder.io/qwik";
3+
import type {
4+
BindingIdentifier,
5+
CallExpression,
6+
ExpressionStatement,
7+
FunctionType,
8+
ImportDeclaration,
9+
ImportDefaultSpecifier,
10+
ImportSpecifier,
11+
JSXAttribute,
12+
JSXAttributeItem,
13+
JSXElement,
14+
JSXExpressionContainer,
15+
Node,
16+
Function as OxcFunction,
17+
Span,
18+
VariableDeclarator,
19+
} from "@oxc-project/types";
20+
import type { BrowserCommandContext } from "vitest/node";
21+
22+
// Type guards for better type safety
23+
export function isFunction(node: Node): node is OxcFunction {
24+
const functionTypes: FunctionType[] = [
25+
"FunctionDeclaration",
26+
"FunctionExpression",
27+
"TSDeclareFunction",
28+
"TSEmptyBodyFunctionExpression",
29+
];
30+
return functionTypes.includes(node.type as FunctionType);
31+
}
32+
33+
export function isCallExpression(node: Node): node is CallExpression {
34+
return node.type === "CallExpression";
35+
}
36+
37+
export function isImportDeclaration(node: Node): node is ImportDeclaration {
38+
return node.type === "ImportDeclaration";
39+
}
40+
41+
export function isVariableDeclarator(node: Node): node is VariableDeclarator {
42+
return node.type === "VariableDeclarator";
43+
}
44+
45+
export function isExpressionStatement(node: Node): node is ExpressionStatement {
46+
return node.type === "ExpressionStatement";
47+
}
48+
49+
export function isJSXElement(node: Node): node is JSXElement {
50+
return node.type === "JSXElement";
51+
}
52+
53+
export function isJSXExpressionContainer(
54+
node: Node,
55+
): node is JSXExpressionContainer {
56+
return node.type === "JSXExpressionContainer";
57+
}
58+
59+
export function traverseChildren(
60+
node: Node,
61+
callback: (child: Node) => boolean | undefined,
62+
): boolean {
63+
for (const key in node) {
64+
const child = (node as unknown as Record<string, unknown>)[key];
65+
if (Array.isArray(child)) {
66+
for (const item of child) {
67+
if (item && typeof item === "object" && callback(item as Node)) {
68+
return true;
69+
}
70+
}
71+
} else if (child && typeof child === "object") {
72+
if (callback(child as Node)) return true;
73+
}
74+
}
75+
return false;
76+
}
77+
78+
export async function hasRenderSSRCall(
79+
code: string,
80+
filename: string,
81+
): Promise<boolean> {
82+
try {
83+
const { parseSync } = await import("oxc-parser");
84+
const ast = parseSync(filename, code);
85+
const renderSSRIdentifiers = new Set<string>(["renderSSR"]);
86+
let hasRenderSSRCallInCode = false;
87+
88+
function walkForDetection(node: Node): boolean {
89+
if (!node || typeof node !== "object") return false;
90+
91+
// Track renderSSR imports and aliases
92+
if (isImportDeclaration(node)) {
93+
if (!node.source?.value || !node.specifiers) return false;
94+
95+
for (const spec of node.specifiers) {
96+
if (spec.type === "ImportSpecifier") {
97+
const importSpec = spec as ImportSpecifier;
98+
if (importSpec.imported.type !== "Identifier") continue;
99+
if (importSpec.imported.name === "renderSSR") {
100+
renderSSRIdentifiers.add(importSpec.local.name);
101+
}
102+
} else if (spec.type === "ImportDefaultSpecifier") {
103+
const defaultSpec = spec as ImportDefaultSpecifier;
104+
if (defaultSpec.local.name.toLowerCase().includes("renderssr")) {
105+
renderSSRIdentifiers.add(defaultSpec.local.name);
106+
}
107+
}
108+
}
109+
}
110+
111+
// Track declared renderSSR functions (including TypeScript declares)
112+
if (isFunction(node)) {
113+
if (node.id?.name === "renderSSR") {
114+
renderSSRIdentifiers.add("renderSSR");
115+
}
116+
}
117+
118+
// Track variable aliases
119+
if (isVariableDeclarator(node)) {
120+
if (node.id.type !== "Identifier") return false;
121+
if (node.init?.type !== "Identifier") return false;
122+
if (!renderSSRIdentifiers.has(node.init.name)) return false;
123+
124+
const bindingId = node.id as BindingIdentifier;
125+
renderSSRIdentifiers.add(bindingId.name);
126+
}
127+
128+
// Check for renderSSR calls
129+
if (isCallExpression(node)) {
130+
if (node.callee.type === "Identifier") {
131+
if (renderSSRIdentifiers.has(node.callee.name)) {
132+
hasRenderSSRCallInCode = true;
133+
return true;
134+
}
135+
}
136+
}
137+
138+
// Recursively check children
139+
return traverseChildren(node, walkForDetection);
140+
}
141+
142+
walkForDetection(ast as unknown as Node);
143+
144+
// If we have renderSSR calls, transform the code
145+
// This handles both cases:
146+
// 1. Explicit imports/declares with calls
147+
// 2. Direct renderSSR calls (common in tests)
148+
const hasCallsInString = code.includes("renderSSR(");
149+
const result = hasRenderSSRCallInCode || hasCallsInString;
150+
151+
return result;
152+
} catch (error) {
153+
console.warn(
154+
`Failed to parse ${filename} for renderSSR detection, falling back to string check:`,
155+
error,
156+
);
157+
return code.includes("renderSSR");
158+
}
159+
}
160+
161+
export function resolveComponentPath(
162+
importPath: string,
163+
testFileId: string,
164+
): string {
165+
if (!importPath.startsWith(".")) {
166+
// Absolute import, add extension if needed
167+
return importPath.endsWith(".tsx") || importPath.endsWith(".ts")
168+
? importPath
169+
: `${importPath}.tsx`;
170+
}
171+
172+
// Relative import - resolve relative to test file
173+
const testFileDir = dirname(testFileId);
174+
const resolvedPath = resolve(testFileDir, importPath);
175+
const projectRoot = process.cwd();
176+
let componentPath = `./${relative(projectRoot, resolvedPath)}`;
177+
178+
// Add extension if needed
179+
if (!componentPath.endsWith(".tsx") && !componentPath.endsWith(".ts")) {
180+
componentPath += ".tsx";
181+
}
182+
183+
return componentPath;
184+
}
185+
186+
export function extractPropsFromJSX(
187+
attributes: JSXAttributeItem[],
188+
sourceCode: string,
189+
): Record<string, string> {
190+
const props: Record<string, string> = {};
191+
192+
for (const attr of attributes) {
193+
if (attr.type !== "JSXAttribute") continue;
194+
195+
const jsxAttr = attr as JSXAttribute;
196+
if (jsxAttr.name.type !== "JSXIdentifier") continue;
197+
198+
const propName = jsxAttr.name.name;
199+
if (!jsxAttr.value) continue;
200+
201+
if (isJSXExpressionContainer(jsxAttr.value)) {
202+
// Extract the raw source code of the expression
203+
if (jsxAttr.value.expression.type !== "JSXEmptyExpression") {
204+
const exprSpan = jsxAttr.value.expression as Node & Span;
205+
const expressionCode = sourceCode.slice(exprSpan.start, exprSpan.end);
206+
props[propName] = expressionCode;
207+
}
208+
} else if (jsxAttr.value.type === "Literal") {
209+
// For string literals, use the actual value
210+
const literal = jsxAttr.value as { value: unknown };
211+
props[propName] = JSON.stringify(literal.value);
212+
}
213+
}
214+
215+
return props;
216+
}
217+
218+
export function isTestFile(id: string): boolean {
219+
return id.includes(".test.") || id.includes(".spec.");
220+
}
221+
222+
export function hasCommandsImport(node: Node): boolean {
223+
if (!isImportDeclaration(node)) return false;
224+
225+
if (node.source?.value !== "@vitest/browser/context") return false;
226+
if (!node.specifiers) return false;
227+
228+
return node.specifiers.some(
229+
(spec) =>
230+
spec.type === "ImportSpecifier" &&
231+
spec.imported.type === "Identifier" &&
232+
spec.imported.name === "commands",
233+
);
234+
}
235+
236+
// Shared SSR rendering logic
237+
export async function renderComponentToSSR(
238+
ctx: BrowserCommandContext,
239+
Component: Component,
240+
props: Record<string, unknown> = {},
241+
): Promise<{ html: string }> {
242+
const viteServer = ctx.project.vite;
243+
244+
const qwikModule = await viteServer.ssrLoadModule("@builder.io/qwik");
245+
const { jsx } = qwikModule;
246+
const jsxElement = jsx(Component, props);
247+
248+
const serverModule = await viteServer.ssrLoadModule(
249+
"@builder.io/qwik/server",
250+
);
251+
const { renderToString } = serverModule;
252+
253+
const result = await renderToString(jsxElement, {
254+
containerTagName: "div",
255+
base: "/",
256+
qwikLoader: { include: "always" },
257+
symbolMapper: globalThis.qwikSymbolMapper,
258+
});
259+
260+
return { html: result.html };
261+
}

0 commit comments

Comments
 (0)