@reckona/mreact-compiler 0.0.66 → 0.0.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compiler-module-context.js.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/emit-client.js.map +1 -1
- package/dist/emit-compat.js.map +1 -1
- package/dist/emit-escape-helper.js.map +1 -1
- package/dist/emit-server-shared.js.map +1 -1
- package/dist/emit-server-stream.js.map +1 -1
- package/dist/emit-server.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal.js.map +1 -1
- package/dist/ir.js.map +1 -1
- package/dist/oxc-analysis-types.js.map +1 -1
- package/dist/oxc-await-analysis.js.map +1 -1
- package/dist/oxc-await-ids.js.map +1 -1
- package/dist/oxc-await-validation.js.map +1 -1
- package/dist/oxc-bindings.js.map +1 -1
- package/dist/oxc-body-lowering.js.map +1 -1
- package/dist/oxc-child-analysis.js.map +1 -1
- package/dist/oxc-code-utils.js.map +1 -1
- package/dist/oxc-component-detection.js.map +1 -1
- package/dist/oxc-component-props.js.map +1 -1
- package/dist/oxc-component-references.js.map +1 -1
- package/dist/oxc-dom-lowering.js.map +1 -1
- package/dist/oxc-expression-utils.js.map +1 -1
- package/dist/oxc-jsx-attributes.js.map +1 -1
- package/dist/oxc-jsx-text.js.map +1 -1
- package/dist/oxc-nested-lowering.js.map +1 -1
- package/dist/oxc-node-utils.js.map +1 -1
- package/dist/oxc-raw-jsx.js.map +1 -1
- package/dist/oxc-render-values.js.map +1 -1
- package/dist/oxc-runtime-emit.js.map +1 -1
- package/dist/oxc-transform.js.map +1 -1
- package/dist/oxc.js.map +1 -1
- package/dist/transform.js.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +4 -3
- package/src/compiler-module-context.ts +31 -0
- package/src/diagnostics.ts +184 -0
- package/src/emit-client.ts +837 -0
- package/src/emit-compat.ts +567 -0
- package/src/emit-escape-helper.ts +45 -0
- package/src/emit-server-shared.ts +384 -0
- package/src/emit-server-stream.ts +2558 -0
- package/src/emit-server.ts +1827 -0
- package/src/index.ts +44 -0
- package/src/internal.ts +1905 -0
- package/src/ir.ts +151 -0
- package/src/oxc-analysis-types.ts +5 -0
- package/src/oxc-await-analysis.ts +165 -0
- package/src/oxc-await-ids.ts +62 -0
- package/src/oxc-await-validation.ts +117 -0
- package/src/oxc-bindings.ts +70 -0
- package/src/oxc-body-lowering.ts +430 -0
- package/src/oxc-child-analysis.ts +791 -0
- package/src/oxc-code-utils.ts +19 -0
- package/src/oxc-component-detection.ts +459 -0
- package/src/oxc-component-props.ts +170 -0
- package/src/oxc-component-references.ts +613 -0
- package/src/oxc-dom-lowering.ts +127 -0
- package/src/oxc-expression-utils.ts +42 -0
- package/src/oxc-jsx-attributes.ts +110 -0
- package/src/oxc-jsx-text.ts +84 -0
- package/src/oxc-nested-lowering.ts +319 -0
- package/src/oxc-node-utils.ts +65 -0
- package/src/oxc-raw-jsx.ts +239 -0
- package/src/oxc-render-values.ts +620 -0
- package/src/oxc-runtime-emit.ts +212 -0
- package/src/oxc-transform.ts +77 -0
- package/src/oxc.ts +932 -0
- package/src/transform.ts +634 -0
- package/src/types.ts +117 -0
|
@@ -0,0 +1,1827 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AttributeIr,
|
|
3
|
+
ComponentPropIr,
|
|
4
|
+
ComponentIr,
|
|
5
|
+
JsxNodeIr,
|
|
6
|
+
ModuleIr,
|
|
7
|
+
} from "./ir.js";
|
|
8
|
+
import type { RuntimeImport, ServerEscapeOptions } from "./types.js";
|
|
9
|
+
import { emitEscapeHtmlHelper } from "./emit-escape-helper.js";
|
|
10
|
+
import { escapeHtmlAttribute as escapeHtml } from "@reckona/mreact-shared/html-escape";
|
|
11
|
+
import {
|
|
12
|
+
htmlAttributeName,
|
|
13
|
+
isDangerousHtmlAttribute,
|
|
14
|
+
isStaticUrlValueUnsafe,
|
|
15
|
+
isUrlAttribute,
|
|
16
|
+
parseStaticStyleObjectLiteral,
|
|
17
|
+
parseStyleLiteralValue,
|
|
18
|
+
simpleSideEffectFreeExpression,
|
|
19
|
+
} from "./emit-server-shared.js";
|
|
20
|
+
import { oxcServerStringReactNodeRenderHelperPlaceholder } from "./oxc-runtime-emit.js";
|
|
21
|
+
|
|
22
|
+
export interface EmitResult {
|
|
23
|
+
code: string;
|
|
24
|
+
imports: RuntimeImport[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface EmitServerOptions {
|
|
28
|
+
dynamicAttributes?: "drop" | "emit";
|
|
29
|
+
escape?: ServerEscapeOptions | undefined;
|
|
30
|
+
serverHydration?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Module-local handle to the URL-safety helper name for the current emit
|
|
34
|
+
// call. Used by deeply-nested attribute emitters to avoid threading the
|
|
35
|
+
// name through every signature. Reset at the top of `emitServer`.
|
|
36
|
+
let currentUrlSafeHelperName: string = "_urlAttrSafe";
|
|
37
|
+
let currentClientBoundaryHelperName: string | undefined;
|
|
38
|
+
let currentSpreadAttributesHelperName: string = "_renderSpreadAttributes";
|
|
39
|
+
|
|
40
|
+
export function emitServer(
|
|
41
|
+
ir: ModuleIr,
|
|
42
|
+
options: EmitServerOptions = {},
|
|
43
|
+
): EmitResult {
|
|
44
|
+
const escapeHelperName = allocateEscapeHelperName(ir);
|
|
45
|
+
const escapeBatchHelperName = options.escape === undefined
|
|
46
|
+
? undefined
|
|
47
|
+
: allocateHelperName(ir, "_escapeHtmlBatch");
|
|
48
|
+
const contextProviderHelperName = usesContextProvider(ir)
|
|
49
|
+
? allocateHelperName(ir, "_renderContextProviderToString")
|
|
50
|
+
: undefined;
|
|
51
|
+
const contextConsumerHelperName = usesContextConsumer(ir)
|
|
52
|
+
? allocateHelperName(ir, "_renderContextConsumerToString")
|
|
53
|
+
: undefined;
|
|
54
|
+
const reactNodeRenderHelperName = usesReactNodeRender(ir)
|
|
55
|
+
? allocateHelperName(ir, "_renderReactNodeToString")
|
|
56
|
+
: undefined;
|
|
57
|
+
const clientBoundaryHelperName = usesClientBoundary(ir)
|
|
58
|
+
? allocateHelperName(ir, "_renderClientBoundary")
|
|
59
|
+
: undefined;
|
|
60
|
+
const spreadAttributesHelperName = allocateHelperName(ir, "_renderSpreadAttributes");
|
|
61
|
+
const outAccumulatorName = allocateHelperName(ir, "_out");
|
|
62
|
+
const urlSafeHelperName = allocateHelperName(ir, "_urlAttrSafe");
|
|
63
|
+
currentUrlSafeHelperName = urlSafeHelperName;
|
|
64
|
+
currentClientBoundaryHelperName = clientBoundaryHelperName;
|
|
65
|
+
currentSpreadAttributesHelperName = spreadAttributesHelperName;
|
|
66
|
+
const helper = emitEscapeHtmlHelper(escapeHelperName);
|
|
67
|
+
// Inline URL-scheme guard mirroring packages/server/src/url-safety.ts.
|
|
68
|
+
// Returns the original value when safe to emit and undefined when the
|
|
69
|
+
// attribute should be dropped. Inlined so compiler output stays free
|
|
70
|
+
// of cross-package runtime imports.
|
|
71
|
+
// Mirrors packages/server/src/url-safety.ts. Issue 078: in-scheme
|
|
72
|
+
// tab/CR/LF must be stripped anywhere in the value, not just at the
|
|
73
|
+
// start, to match the browser's URL parser.
|
|
74
|
+
const urlSafeHelper = [
|
|
75
|
+
`function ${urlSafeHelperName}(name, value) {`,
|
|
76
|
+
` if (typeof value !== "string") return value;`,
|
|
77
|
+
` const _canonical = value`,
|
|
78
|
+
` .replace(/^[\\x00-\\x20]+/u, "")`,
|
|
79
|
+
` .replace(/[\\t\\r\\n]/g, "");`,
|
|
80
|
+
` const _match = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(_canonical);`,
|
|
81
|
+
` if (_match === null) return value;`,
|
|
82
|
+
` const _scheme = _match[1].toLowerCase();`,
|
|
83
|
+
` if (_scheme !== "javascript" && _scheme !== "vbscript" && _scheme !== "livescript" && _scheme !== "mhtml" && _scheme !== "file" && _scheme !== "data") return value;`,
|
|
84
|
+
` if (_scheme === "data" && (name === "src" || name === "poster") && /^data:image\\/(?!svg\\+xml(?:[;,]|$))/i.test(_canonical)) return value;`,
|
|
85
|
+
` return undefined;`,
|
|
86
|
+
`}`,
|
|
87
|
+
].join("\n");
|
|
88
|
+
const asyncComponentNames = collectAsyncServerComponentNames(ir.components);
|
|
89
|
+
const components = ir.components
|
|
90
|
+
.map((component) =>
|
|
91
|
+
emitComponent(
|
|
92
|
+
component,
|
|
93
|
+
escapeHelperName,
|
|
94
|
+
escapeBatchHelperName,
|
|
95
|
+
outAccumulatorName,
|
|
96
|
+
options,
|
|
97
|
+
asyncComponentNames,
|
|
98
|
+
options.dynamicAttributes ?? "emit",
|
|
99
|
+
contextProviderHelperName,
|
|
100
|
+
contextConsumerHelperName,
|
|
101
|
+
reactNodeRenderHelperName,
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
.join("\n\n");
|
|
105
|
+
// Tree-shake the URL-safety helper when it is not referenced by any
|
|
106
|
+
// component output. Same shape as the existing escapeImport check.
|
|
107
|
+
const needsSpreadAttributesHelper = components.includes(spreadAttributesHelperName);
|
|
108
|
+
const urlSafeBlock =
|
|
109
|
+
components.includes(urlSafeHelperName) || needsSpreadAttributesHelper ? urlSafeHelper : "";
|
|
110
|
+
const clientBoundaryBlock =
|
|
111
|
+
clientBoundaryHelperName === undefined || !components.includes(clientBoundaryHelperName)
|
|
112
|
+
? ""
|
|
113
|
+
: emitClientBoundaryHelper(clientBoundaryHelperName);
|
|
114
|
+
const spreadAttributesBlock = needsSpreadAttributesHelper
|
|
115
|
+
? emitSpreadAttributesHelper(spreadAttributesHelperName, escapeHelperName, urlSafeHelperName)
|
|
116
|
+
: "";
|
|
117
|
+
// Emit batch escape import only when the helper is actually referenced
|
|
118
|
+
// by the generated component code (issue 048: dead-import elimination).
|
|
119
|
+
// Helper names are uniquely allocated, so a literal substring check is
|
|
120
|
+
// both correct and inexpensive.
|
|
121
|
+
const escapeImport =
|
|
122
|
+
options.escape === undefined ||
|
|
123
|
+
escapeBatchHelperName === undefined ||
|
|
124
|
+
!components.includes(escapeBatchHelperName)
|
|
125
|
+
? ""
|
|
126
|
+
: `import { ${options.escape.batchImportName} as ${escapeBatchHelperName} } from ${stringLiteral(options.escape.batchImportSource)};`;
|
|
127
|
+
const userImports = emitUserImports(ir);
|
|
128
|
+
const contextImport = emitContextImport(
|
|
129
|
+
contextProviderHelperName,
|
|
130
|
+
contextConsumerHelperName,
|
|
131
|
+
reactNodeRenderHelperName,
|
|
132
|
+
);
|
|
133
|
+
const moduleStatements = emitModuleStatements(ir);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
code: `${[userImports, escapeImport, contextImport, moduleStatements, helper, urlSafeBlock, clientBoundaryBlock, spreadAttributesBlock].filter(Boolean).join("\n\n")}\n\n${components}\n`,
|
|
137
|
+
imports: collectContextImports(
|
|
138
|
+
contextProviderHelperName,
|
|
139
|
+
contextConsumerHelperName,
|
|
140
|
+
reactNodeRenderHelperName,
|
|
141
|
+
),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function emitContextImport(
|
|
146
|
+
contextProviderHelperName: string | undefined,
|
|
147
|
+
contextConsumerHelperName: string | undefined,
|
|
148
|
+
reactNodeRenderHelperName: string | undefined,
|
|
149
|
+
): string {
|
|
150
|
+
const specifiers = [
|
|
151
|
+
reactNodeRenderHelperName === undefined
|
|
152
|
+
? undefined
|
|
153
|
+
: `renderToString as ${reactNodeRenderHelperName}`,
|
|
154
|
+
contextProviderHelperName === undefined
|
|
155
|
+
? undefined
|
|
156
|
+
: `renderContextProviderToString as ${contextProviderHelperName}`,
|
|
157
|
+
contextConsumerHelperName === undefined
|
|
158
|
+
? undefined
|
|
159
|
+
: `renderContextConsumerToString as ${contextConsumerHelperName}`,
|
|
160
|
+
].filter((specifier): specifier is string => specifier !== undefined);
|
|
161
|
+
|
|
162
|
+
return specifiers.length === 0
|
|
163
|
+
? ""
|
|
164
|
+
: `import { ${specifiers.join(", ")} } from "@reckona/mreact-compat";`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function collectContextImports(
|
|
168
|
+
contextProviderHelperName: string | undefined,
|
|
169
|
+
contextConsumerHelperName: string | undefined,
|
|
170
|
+
reactNodeRenderHelperName?: string,
|
|
171
|
+
): RuntimeImport[] {
|
|
172
|
+
const specifiers = [
|
|
173
|
+
reactNodeRenderHelperName === undefined ? undefined : "renderToString",
|
|
174
|
+
contextProviderHelperName === undefined
|
|
175
|
+
? undefined
|
|
176
|
+
: "renderContextProviderToString",
|
|
177
|
+
contextConsumerHelperName === undefined
|
|
178
|
+
? undefined
|
|
179
|
+
: "renderContextConsumerToString",
|
|
180
|
+
].filter((specifier): specifier is string => specifier !== undefined);
|
|
181
|
+
|
|
182
|
+
return specifiers.length === 0
|
|
183
|
+
? []
|
|
184
|
+
: [{ source: "@reckona/mreact-compat", specifiers }];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function emitUserImports(ir: ModuleIr): string {
|
|
188
|
+
return ir.userImports.join("\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function emitModuleStatements(ir: ModuleIr): string {
|
|
192
|
+
return ir.moduleStatements.join("\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function emitComponent(
|
|
196
|
+
component: ComponentIr,
|
|
197
|
+
escapeHelperName: string,
|
|
198
|
+
escapeBatchHelperName: string | undefined,
|
|
199
|
+
outAccumulatorName: string,
|
|
200
|
+
options: EmitServerOptions,
|
|
201
|
+
asyncComponentNames: ReadonlySet<string>,
|
|
202
|
+
dynamicAttributes: "drop" | "emit",
|
|
203
|
+
contextProviderHelperName?: string,
|
|
204
|
+
contextConsumerHelperName?: string,
|
|
205
|
+
reactNodeRenderHelperName?: string,
|
|
206
|
+
): string {
|
|
207
|
+
const body = component.bodyStatements.map((statement) =>
|
|
208
|
+
` ${replaceOxcServerStringReactNodeRenderHelper(statement, reactNodeRenderHelperName)}`,
|
|
209
|
+
);
|
|
210
|
+
const parameters = component.parameters.join(", ");
|
|
211
|
+
const htmlStatements = collectHtmlStatements(
|
|
212
|
+
component.root,
|
|
213
|
+
outAccumulatorName,
|
|
214
|
+
escapeHelperName,
|
|
215
|
+
escapeBatchHelperName,
|
|
216
|
+
asyncComponentNames,
|
|
217
|
+
dynamicAttributes,
|
|
218
|
+
contextProviderHelperName,
|
|
219
|
+
contextConsumerHelperName,
|
|
220
|
+
reactNodeRenderHelperName,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const markerStart = stringLiteral(`<!--mreact-h:start:${encodeURIComponent(component.name)}-->`);
|
|
224
|
+
const markerEnd = stringLiteral(`<!--mreact-h:end:${encodeURIComponent(component.name)}-->`);
|
|
225
|
+
|
|
226
|
+
const hydrationOpenStatements =
|
|
227
|
+
options.serverHydration === true ? [` ${outAccumulatorName} += ${markerStart};`] : [];
|
|
228
|
+
const hydrationCloseStatements =
|
|
229
|
+
options.serverHydration === true ? [` ${outAccumulatorName} += ${markerEnd};`] : [];
|
|
230
|
+
|
|
231
|
+
const functionKeyword = `${component.exportDefault === true ? "export default " : component.exported === false ? "" : "export "}${
|
|
232
|
+
asyncComponentNames.has(component.name) ? "async " : ""
|
|
233
|
+
}function`;
|
|
234
|
+
|
|
235
|
+
return [
|
|
236
|
+
`${functionKeyword} ${component.name}(${parameters}) {`,
|
|
237
|
+
...body,
|
|
238
|
+
` let ${outAccumulatorName} = "";`,
|
|
239
|
+
...hydrationOpenStatements,
|
|
240
|
+
...htmlStatements.map((statement) => ` ${statement}`),
|
|
241
|
+
...hydrationCloseStatements,
|
|
242
|
+
` return ${outAccumulatorName};`,
|
|
243
|
+
`}`,
|
|
244
|
+
].join("\n");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function emitHtmlExpression(
|
|
248
|
+
node: JsxNodeIr,
|
|
249
|
+
escapeHelperName: string,
|
|
250
|
+
escapeBatchHelperName: string | undefined,
|
|
251
|
+
asyncComponentNames: ReadonlySet<string>,
|
|
252
|
+
dynamicAttributes: "drop" | "emit",
|
|
253
|
+
contextProviderHelperName?: string,
|
|
254
|
+
contextConsumerHelperName?: string,
|
|
255
|
+
reactNodeRenderHelperName?: string,
|
|
256
|
+
): string {
|
|
257
|
+
const parts = collectHtmlParts(
|
|
258
|
+
node,
|
|
259
|
+
escapeHelperName,
|
|
260
|
+
escapeBatchHelperName,
|
|
261
|
+
asyncComponentNames,
|
|
262
|
+
dynamicAttributes,
|
|
263
|
+
contextProviderHelperName,
|
|
264
|
+
contextConsumerHelperName,
|
|
265
|
+
reactNodeRenderHelperName,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
if (parts.length === 0) {
|
|
269
|
+
return "\"\"";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return parts.join(" + ");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Statement-list IR walker (issue 046 followup). Produces a sequence of
|
|
277
|
+
* statements that each append to a shared accumulator variable instead of
|
|
278
|
+
* a single concat expression. Used at the top of component bodies; sub
|
|
279
|
+
* callbacks (`renderContextProviderToString`, async list renderers, etc.)
|
|
280
|
+
* still use the expression form via `emitHtmlExpression`.
|
|
281
|
+
*
|
|
282
|
+
* The benefits over expression mode:
|
|
283
|
+
* - intermediate string allocations from `+ +` chains disappear
|
|
284
|
+
* - conditional branches lower to `if/else` (no ternary expression spaghetti)
|
|
285
|
+
* - sync list rendering inlines the for-loop append without an IIFE wrapper
|
|
286
|
+
* - debugger / source maps step naturally over the generated statements
|
|
287
|
+
*/
|
|
288
|
+
function collectHtmlStatements(
|
|
289
|
+
node: JsxNodeIr,
|
|
290
|
+
outVar: string,
|
|
291
|
+
escapeHelperName: string,
|
|
292
|
+
escapeBatchHelperName: string | undefined,
|
|
293
|
+
asyncComponentNames: ReadonlySet<string>,
|
|
294
|
+
dynamicAttributes: "drop" | "emit",
|
|
295
|
+
contextProviderHelperName?: string,
|
|
296
|
+
contextConsumerHelperName?: string,
|
|
297
|
+
reactNodeRenderHelperName?: string,
|
|
298
|
+
selectedValueCode?: string,
|
|
299
|
+
): string[] {
|
|
300
|
+
if (node.kind === "text") {
|
|
301
|
+
const literal = escapeHtml(node.value);
|
|
302
|
+
if (literal === "") {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
return [`${outVar} += ${stringLiteral(literal)};`];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (node.kind === "expr") {
|
|
309
|
+
if (node.renderMode === "html") {
|
|
310
|
+
return [`${outVar} += ${rawHtmlExpression(node.code)};`];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (node.renderMode === "react-node" && reactNodeRenderHelperName !== undefined) {
|
|
314
|
+
return [`${outVar} += ${reactNodeRenderHelperName}(() => (${node.code}));`];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return [`${outVar} += ${escapeHelperName}(${node.code});`];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (node.kind === "conditional") {
|
|
321
|
+
const whenTrueStatements = node.whenTrue.flatMap((child) =>
|
|
322
|
+
collectHtmlStatements(
|
|
323
|
+
child,
|
|
324
|
+
outVar,
|
|
325
|
+
escapeHelperName,
|
|
326
|
+
escapeBatchHelperName,
|
|
327
|
+
asyncComponentNames,
|
|
328
|
+
dynamicAttributes,
|
|
329
|
+
contextProviderHelperName,
|
|
330
|
+
contextConsumerHelperName,
|
|
331
|
+
reactNodeRenderHelperName,
|
|
332
|
+
),
|
|
333
|
+
);
|
|
334
|
+
const whenFalseStatements = node.whenFalse.flatMap((child) =>
|
|
335
|
+
collectHtmlStatements(
|
|
336
|
+
child,
|
|
337
|
+
outVar,
|
|
338
|
+
escapeHelperName,
|
|
339
|
+
escapeBatchHelperName,
|
|
340
|
+
asyncComponentNames,
|
|
341
|
+
dynamicAttributes,
|
|
342
|
+
contextProviderHelperName,
|
|
343
|
+
contextConsumerHelperName,
|
|
344
|
+
reactNodeRenderHelperName,
|
|
345
|
+
),
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
if (whenTrueStatements.length === 0 && whenFalseStatements.length === 0) {
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (whenFalseStatements.length === 0) {
|
|
353
|
+
return [
|
|
354
|
+
`if (${node.conditionCode}) {`,
|
|
355
|
+
...whenTrueStatements.map((statement) => ` ${statement}`),
|
|
356
|
+
`}`,
|
|
357
|
+
];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (whenTrueStatements.length === 0) {
|
|
361
|
+
return [
|
|
362
|
+
`if (!(${node.conditionCode})) {`,
|
|
363
|
+
...whenFalseStatements.map((statement) => ` ${statement}`),
|
|
364
|
+
`}`,
|
|
365
|
+
];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return [
|
|
369
|
+
`if (${node.conditionCode}) {`,
|
|
370
|
+
...whenTrueStatements.map((statement) => ` ${statement}`),
|
|
371
|
+
`} else {`,
|
|
372
|
+
...whenFalseStatements.map((statement) => ` ${statement}`),
|
|
373
|
+
`}`,
|
|
374
|
+
];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (node.kind === "list") {
|
|
378
|
+
const isAsync = containsAsyncServerOperationInChildren(
|
|
379
|
+
node.children,
|
|
380
|
+
asyncComponentNames,
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
if (isAsync) {
|
|
384
|
+
// Parallel async path keeps the existing renderer + Promise.all + join
|
|
385
|
+
// form to preserve concurrent resolution semantics.
|
|
386
|
+
const parameters =
|
|
387
|
+
node.indexName === undefined
|
|
388
|
+
? node.itemName
|
|
389
|
+
: `${node.itemName}, ${node.indexName}`;
|
|
390
|
+
const renderer = emitListRenderer(
|
|
391
|
+
node,
|
|
392
|
+
parameters,
|
|
393
|
+
escapeHelperName,
|
|
394
|
+
escapeBatchHelperName,
|
|
395
|
+
asyncComponentNames,
|
|
396
|
+
dynamicAttributes,
|
|
397
|
+
contextProviderHelperName,
|
|
398
|
+
contextConsumerHelperName,
|
|
399
|
+
reactNodeRenderHelperName,
|
|
400
|
+
);
|
|
401
|
+
const mapped = `(${node.itemsCode}).map(${renderer})`;
|
|
402
|
+
return [`${outVar} += (await Promise.all(${mapped})).join("");`];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Sync list — inline for-loop appending to the caller's accumulator.
|
|
406
|
+
// No inner IIFE wrapper and no intermediate string concat per iteration.
|
|
407
|
+
const itemBinding = `const ${node.itemName} = _arr[_i];`;
|
|
408
|
+
const indexBinding =
|
|
409
|
+
node.indexName === undefined ? undefined : `const ${node.indexName} = _i;`;
|
|
410
|
+
const bodyStatements = node.bodyStatements ?? [];
|
|
411
|
+
const childStatements = node.children.flatMap((child) =>
|
|
412
|
+
collectHtmlStatements(
|
|
413
|
+
child,
|
|
414
|
+
outVar,
|
|
415
|
+
escapeHelperName,
|
|
416
|
+
escapeBatchHelperName,
|
|
417
|
+
asyncComponentNames,
|
|
418
|
+
dynamicAttributes,
|
|
419
|
+
contextProviderHelperName,
|
|
420
|
+
contextConsumerHelperName,
|
|
421
|
+
reactNodeRenderHelperName,
|
|
422
|
+
),
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
return [
|
|
426
|
+
`{`,
|
|
427
|
+
` const _arr = (${node.itemsCode});`,
|
|
428
|
+
` for (let _i = 0, _len = _arr.length; _i < _len; _i++) {`,
|
|
429
|
+
` ${itemBinding}`,
|
|
430
|
+
...(indexBinding === undefined ? [] : [` ${indexBinding}`]),
|
|
431
|
+
...bodyStatements.map((statement) => ` ${statement}`),
|
|
432
|
+
...childStatements.map((statement) => ` ${statement}`),
|
|
433
|
+
` }`,
|
|
434
|
+
`}`,
|
|
435
|
+
];
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (node.kind === "fragment") {
|
|
439
|
+
return node.children.flatMap((child) =>
|
|
440
|
+
collectHtmlStatements(
|
|
441
|
+
child,
|
|
442
|
+
outVar,
|
|
443
|
+
escapeHelperName,
|
|
444
|
+
escapeBatchHelperName,
|
|
445
|
+
asyncComponentNames,
|
|
446
|
+
dynamicAttributes,
|
|
447
|
+
contextProviderHelperName,
|
|
448
|
+
contextConsumerHelperName,
|
|
449
|
+
reactNodeRenderHelperName,
|
|
450
|
+
),
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (node.kind === "component") {
|
|
455
|
+
if (node.name === "Suspense") {
|
|
456
|
+
return [
|
|
457
|
+
`${outVar} += "<!--$-->";`,
|
|
458
|
+
...node.children.flatMap((child) =>
|
|
459
|
+
collectHtmlStatements(
|
|
460
|
+
child,
|
|
461
|
+
outVar,
|
|
462
|
+
escapeHelperName,
|
|
463
|
+
escapeBatchHelperName,
|
|
464
|
+
asyncComponentNames,
|
|
465
|
+
dynamicAttributes,
|
|
466
|
+
contextProviderHelperName,
|
|
467
|
+
contextConsumerHelperName,
|
|
468
|
+
reactNodeRenderHelperName,
|
|
469
|
+
),
|
|
470
|
+
),
|
|
471
|
+
`${outVar} += "<!--/$-->";`,
|
|
472
|
+
];
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (contextProviderHelperName !== undefined && node.name.endsWith(".Provider")) {
|
|
476
|
+
// Provider helper takes a string-returning callback. Use the
|
|
477
|
+
// expression form inside the callback to preserve the existing
|
|
478
|
+
// helper contract.
|
|
479
|
+
const valueCode = findComponentPropCode(node.props, "value") ?? "undefined";
|
|
480
|
+
return [
|
|
481
|
+
`${outVar} += ${contextProviderHelperName}(${node.name}, ${valueCode}, () => ${emitHtmlExpressionFromChildren(node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)});`,
|
|
482
|
+
];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (contextConsumerHelperName !== undefined && node.name.endsWith(".Consumer")) {
|
|
486
|
+
const renderProp = findComponentRenderProp(node.props, "children");
|
|
487
|
+
|
|
488
|
+
if (renderProp !== undefined) {
|
|
489
|
+
const valueName = renderProp.valueName ?? "_value";
|
|
490
|
+
return [
|
|
491
|
+
`${outVar} += ${contextConsumerHelperName}(${node.name}, (${valueName}) => ${emitHtmlExpressionFromChildren(renderProp.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)});`,
|
|
492
|
+
];
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (isClientBoundaryPlaceholder(node)) {
|
|
497
|
+
const helperName = currentClientBoundaryHelperName;
|
|
498
|
+
if (helperName !== undefined) {
|
|
499
|
+
return [
|
|
500
|
+
`${outVar} += ${helperName}(${stringLiteral(node.name)}, ${emitPropsObject(
|
|
501
|
+
node.props,
|
|
502
|
+
node.children,
|
|
503
|
+
escapeHelperName,
|
|
504
|
+
escapeBatchHelperName,
|
|
505
|
+
asyncComponentNames,
|
|
506
|
+
dynamicAttributes,
|
|
507
|
+
contextProviderHelperName,
|
|
508
|
+
contextConsumerHelperName,
|
|
509
|
+
reactNodeRenderHelperName,
|
|
510
|
+
)});`,
|
|
511
|
+
];
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return [`${outVar} += ${stringLiteral(clientBoundaryPlaceholder(node))};`];
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (node.runtime === "compat" && reactNodeRenderHelperName !== undefined) {
|
|
518
|
+
return [
|
|
519
|
+
`${outVar} += ${reactNodeRenderHelperName}(${node.name}, ${emitPropsObject(
|
|
520
|
+
node.props,
|
|
521
|
+
node.children,
|
|
522
|
+
escapeHelperName,
|
|
523
|
+
escapeBatchHelperName,
|
|
524
|
+
asyncComponentNames,
|
|
525
|
+
dynamicAttributes,
|
|
526
|
+
contextProviderHelperName,
|
|
527
|
+
contextConsumerHelperName,
|
|
528
|
+
reactNodeRenderHelperName,
|
|
529
|
+
)});`,
|
|
530
|
+
];
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return [
|
|
534
|
+
`${outVar} += ${emitComponentCallExpression(
|
|
535
|
+
node.name,
|
|
536
|
+
emitPropsObject(
|
|
537
|
+
node.props,
|
|
538
|
+
node.children,
|
|
539
|
+
escapeHelperName,
|
|
540
|
+
escapeBatchHelperName,
|
|
541
|
+
asyncComponentNames,
|
|
542
|
+
dynamicAttributes,
|
|
543
|
+
contextProviderHelperName,
|
|
544
|
+
contextConsumerHelperName,
|
|
545
|
+
reactNodeRenderHelperName,
|
|
546
|
+
),
|
|
547
|
+
asyncComponentNames,
|
|
548
|
+
)};`,
|
|
549
|
+
];
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (node.kind === "async-boundary") {
|
|
553
|
+
return [];
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// element
|
|
557
|
+
const statements: string[] = [];
|
|
558
|
+
if (node.tagName === "textarea") {
|
|
559
|
+
const attributeScan = scanElementAttributes(node.tagName, node.attributes);
|
|
560
|
+
statements.push(`${outVar} += ${stringLiteral("<textarea")};`);
|
|
561
|
+
for (const attributePart of collectElementAttributeParts(
|
|
562
|
+
node.tagName,
|
|
563
|
+
node.attributes,
|
|
564
|
+
escapeHelperName,
|
|
565
|
+
escapeBatchHelperName,
|
|
566
|
+
dynamicAttributes,
|
|
567
|
+
attributeScan,
|
|
568
|
+
)) {
|
|
569
|
+
statements.push(`${outVar} += ${attributePart};`);
|
|
570
|
+
}
|
|
571
|
+
statements.push(`${outVar} += ">";`);
|
|
572
|
+
for (const valuePart of collectTextareaValueParts(
|
|
573
|
+
node,
|
|
574
|
+
escapeHelperName,
|
|
575
|
+
escapeBatchHelperName,
|
|
576
|
+
asyncComponentNames,
|
|
577
|
+
dynamicAttributes,
|
|
578
|
+
contextProviderHelperName,
|
|
579
|
+
contextConsumerHelperName,
|
|
580
|
+
reactNodeRenderHelperName,
|
|
581
|
+
attributeScan,
|
|
582
|
+
)) {
|
|
583
|
+
statements.push(`${outVar} += ${valuePart};`);
|
|
584
|
+
}
|
|
585
|
+
statements.push(`${outVar} += "</textarea>";`);
|
|
586
|
+
return statements;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
statements.push(`${outVar} += ${stringLiteral(`<${node.tagName}`)};`);
|
|
590
|
+
const attributeScan = scanElementAttributes(node.tagName, node.attributes);
|
|
591
|
+
|
|
592
|
+
for (const attributePart of collectElementAttributeParts(
|
|
593
|
+
node.tagName,
|
|
594
|
+
node.attributes,
|
|
595
|
+
escapeHelperName,
|
|
596
|
+
escapeBatchHelperName,
|
|
597
|
+
dynamicAttributes,
|
|
598
|
+
attributeScan,
|
|
599
|
+
)) {
|
|
600
|
+
statements.push(`${outVar} += ${attributePart};`);
|
|
601
|
+
}
|
|
602
|
+
const selectedAttributePart = collectOptionSelectedAttributePart(node, selectedValueCode);
|
|
603
|
+
if (selectedAttributePart !== undefined) {
|
|
604
|
+
statements.push(`${outVar} += ${selectedAttributePart};`);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
statements.push(`${outVar} += ">";`);
|
|
608
|
+
|
|
609
|
+
const dangerousInnerHtml = emitDangerouslySetInnerHtmlExpression(node.attributes);
|
|
610
|
+
if (dangerousInnerHtml !== undefined) {
|
|
611
|
+
statements.push(`${outVar} += ${dangerousInnerHtml};`);
|
|
612
|
+
statements.push(`${outVar} += ${stringLiteral(`</${node.tagName}>`)};`);
|
|
613
|
+
return statements;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const childrenExpression = emitBatchedSimpleChildrenExpression(
|
|
617
|
+
node.children,
|
|
618
|
+
escapeBatchHelperName,
|
|
619
|
+
);
|
|
620
|
+
const childSelectedValueCode = node.tagName === "select"
|
|
621
|
+
? attributeScan.formValueAttributeCode
|
|
622
|
+
: undefined;
|
|
623
|
+
|
|
624
|
+
if (childrenExpression !== undefined && childSelectedValueCode === undefined) {
|
|
625
|
+
statements.push(`${outVar} += ${childrenExpression};`);
|
|
626
|
+
} else {
|
|
627
|
+
for (const child of node.children) {
|
|
628
|
+
statements.push(
|
|
629
|
+
...collectHtmlStatements(
|
|
630
|
+
child,
|
|
631
|
+
outVar,
|
|
632
|
+
escapeHelperName,
|
|
633
|
+
escapeBatchHelperName,
|
|
634
|
+
asyncComponentNames,
|
|
635
|
+
dynamicAttributes,
|
|
636
|
+
contextProviderHelperName,
|
|
637
|
+
contextConsumerHelperName,
|
|
638
|
+
reactNodeRenderHelperName,
|
|
639
|
+
childSelectedValueCode,
|
|
640
|
+
),
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
statements.push(`${outVar} += ${stringLiteral(`</${node.tagName}>`)};`);
|
|
646
|
+
|
|
647
|
+
return statements;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function collectHtmlParts(
|
|
651
|
+
node: JsxNodeIr,
|
|
652
|
+
escapeHelperName: string,
|
|
653
|
+
escapeBatchHelperName: string | undefined,
|
|
654
|
+
asyncComponentNames: ReadonlySet<string>,
|
|
655
|
+
dynamicAttributes: "drop" | "emit",
|
|
656
|
+
contextProviderHelperName?: string,
|
|
657
|
+
contextConsumerHelperName?: string,
|
|
658
|
+
reactNodeRenderHelperName?: string,
|
|
659
|
+
selectedValueCode?: string,
|
|
660
|
+
): string[] {
|
|
661
|
+
if (node.kind === "text") {
|
|
662
|
+
return [stringLiteral(escapeHtml(node.value))];
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (node.kind === "expr") {
|
|
666
|
+
if (node.renderMode === "html") {
|
|
667
|
+
return [rawHtmlExpression(node.code)];
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (node.renderMode === "react-node" && reactNodeRenderHelperName !== undefined) {
|
|
671
|
+
return [`${reactNodeRenderHelperName}(() => (${node.code}))`];
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return [`${escapeHelperName}(${node.code})`];
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (node.kind === "conditional") {
|
|
678
|
+
return [
|
|
679
|
+
`((${node.conditionCode}) ? ${emitHtmlExpressionFromChildren(node.whenTrue, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)} : ${emitHtmlExpressionFromChildren(node.whenFalse, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)})`,
|
|
680
|
+
];
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (node.kind === "list") {
|
|
684
|
+
const isAsync = containsAsyncServerOperationInChildren(
|
|
685
|
+
node.children,
|
|
686
|
+
asyncComponentNames,
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
if (isAsync) {
|
|
690
|
+
// Async lists rely on Promise.all() for parallel resolution; the
|
|
691
|
+
// callback allocation is amortized across `await` latency, so we keep
|
|
692
|
+
// the `.map().then(...).join("")` form.
|
|
693
|
+
const parameters =
|
|
694
|
+
node.indexName === undefined
|
|
695
|
+
? node.itemName
|
|
696
|
+
: `${node.itemName}, ${node.indexName}`;
|
|
697
|
+
const renderer = emitListRenderer(
|
|
698
|
+
node,
|
|
699
|
+
parameters,
|
|
700
|
+
escapeHelperName,
|
|
701
|
+
escapeBatchHelperName,
|
|
702
|
+
asyncComponentNames,
|
|
703
|
+
dynamicAttributes,
|
|
704
|
+
contextProviderHelperName,
|
|
705
|
+
contextConsumerHelperName,
|
|
706
|
+
reactNodeRenderHelperName,
|
|
707
|
+
);
|
|
708
|
+
const mapped = `(${node.itemsCode}).map(${renderer})`;
|
|
709
|
+
return [`(await Promise.all(${mapped})).join("")`];
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Synchronous list — imperative accumulator avoids the per-render
|
|
713
|
+
// callback allocation, the intermediate `.map()` result array, and the
|
|
714
|
+
// trailing `.join("")` call.
|
|
715
|
+
return [emitSyncListIife(
|
|
716
|
+
node,
|
|
717
|
+
escapeHelperName,
|
|
718
|
+
escapeBatchHelperName,
|
|
719
|
+
asyncComponentNames,
|
|
720
|
+
dynamicAttributes,
|
|
721
|
+
contextProviderHelperName,
|
|
722
|
+
contextConsumerHelperName,
|
|
723
|
+
reactNodeRenderHelperName,
|
|
724
|
+
)];
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (node.kind === "fragment") {
|
|
728
|
+
return node.children.flatMap((child) =>
|
|
729
|
+
collectHtmlParts(
|
|
730
|
+
child,
|
|
731
|
+
escapeHelperName,
|
|
732
|
+
escapeBatchHelperName,
|
|
733
|
+
asyncComponentNames,
|
|
734
|
+
dynamicAttributes,
|
|
735
|
+
contextProviderHelperName,
|
|
736
|
+
contextConsumerHelperName,
|
|
737
|
+
reactNodeRenderHelperName,
|
|
738
|
+
),
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (node.kind === "component") {
|
|
743
|
+
if (node.name === "Suspense") {
|
|
744
|
+
return [
|
|
745
|
+
stringLiteral("<!--$-->"),
|
|
746
|
+
...node.children.flatMap((child) =>
|
|
747
|
+
collectHtmlParts(
|
|
748
|
+
child,
|
|
749
|
+
escapeHelperName,
|
|
750
|
+
escapeBatchHelperName,
|
|
751
|
+
asyncComponentNames,
|
|
752
|
+
dynamicAttributes,
|
|
753
|
+
contextProviderHelperName,
|
|
754
|
+
contextConsumerHelperName,
|
|
755
|
+
reactNodeRenderHelperName,
|
|
756
|
+
),
|
|
757
|
+
),
|
|
758
|
+
stringLiteral("<!--/$-->"),
|
|
759
|
+
];
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (contextProviderHelperName !== undefined && node.name.endsWith(".Provider")) {
|
|
763
|
+
const valueCode = findComponentPropCode(node.props, "value") ?? "undefined";
|
|
764
|
+
return [
|
|
765
|
+
`${contextProviderHelperName}(${node.name}, ${valueCode}, () => ${emitHtmlExpressionFromChildren(node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)})`,
|
|
766
|
+
];
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (contextConsumerHelperName !== undefined && node.name.endsWith(".Consumer")) {
|
|
770
|
+
const renderProp = findComponentRenderProp(node.props, "children");
|
|
771
|
+
|
|
772
|
+
if (renderProp !== undefined) {
|
|
773
|
+
const valueName = renderProp.valueName ?? "_value";
|
|
774
|
+
return [
|
|
775
|
+
`${contextConsumerHelperName}(${node.name}, (${valueName}) => ${emitHtmlExpressionFromChildren(renderProp.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)})`,
|
|
776
|
+
];
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (isClientBoundaryPlaceholder(node)) {
|
|
781
|
+
const helperName = currentClientBoundaryHelperName;
|
|
782
|
+
if (helperName !== undefined) {
|
|
783
|
+
return [
|
|
784
|
+
`${helperName}(${stringLiteral(node.name)}, ${emitPropsObject(
|
|
785
|
+
node.props,
|
|
786
|
+
node.children,
|
|
787
|
+
escapeHelperName,
|
|
788
|
+
escapeBatchHelperName,
|
|
789
|
+
asyncComponentNames,
|
|
790
|
+
dynamicAttributes,
|
|
791
|
+
contextProviderHelperName,
|
|
792
|
+
contextConsumerHelperName,
|
|
793
|
+
reactNodeRenderHelperName,
|
|
794
|
+
)})`,
|
|
795
|
+
];
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return [stringLiteral(clientBoundaryPlaceholder(node))];
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (node.runtime === "compat" && reactNodeRenderHelperName !== undefined) {
|
|
802
|
+
return [
|
|
803
|
+
`${reactNodeRenderHelperName}(${node.name}, ${emitPropsObject(
|
|
804
|
+
node.props,
|
|
805
|
+
node.children,
|
|
806
|
+
escapeHelperName,
|
|
807
|
+
escapeBatchHelperName,
|
|
808
|
+
asyncComponentNames,
|
|
809
|
+
dynamicAttributes,
|
|
810
|
+
contextProviderHelperName,
|
|
811
|
+
contextConsumerHelperName,
|
|
812
|
+
reactNodeRenderHelperName,
|
|
813
|
+
)})`,
|
|
814
|
+
];
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return [
|
|
818
|
+
emitComponentCallExpression(
|
|
819
|
+
node.name,
|
|
820
|
+
emitPropsObject(
|
|
821
|
+
node.props,
|
|
822
|
+
node.children,
|
|
823
|
+
escapeHelperName,
|
|
824
|
+
escapeBatchHelperName,
|
|
825
|
+
asyncComponentNames,
|
|
826
|
+
dynamicAttributes,
|
|
827
|
+
contextProviderHelperName,
|
|
828
|
+
contextConsumerHelperName,
|
|
829
|
+
reactNodeRenderHelperName,
|
|
830
|
+
),
|
|
831
|
+
asyncComponentNames,
|
|
832
|
+
),
|
|
833
|
+
];
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (node.kind === "async-boundary") {
|
|
837
|
+
return [];
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const closeTag = `</${node.tagName}>`;
|
|
841
|
+
|
|
842
|
+
if (node.tagName === "textarea") {
|
|
843
|
+
const attributeScan = scanElementAttributes(node.tagName, node.attributes);
|
|
844
|
+
return [
|
|
845
|
+
stringLiteral("<textarea"),
|
|
846
|
+
...collectElementAttributeParts(
|
|
847
|
+
node.tagName,
|
|
848
|
+
node.attributes,
|
|
849
|
+
escapeHelperName,
|
|
850
|
+
escapeBatchHelperName,
|
|
851
|
+
dynamicAttributes,
|
|
852
|
+
attributeScan,
|
|
853
|
+
),
|
|
854
|
+
stringLiteral(">"),
|
|
855
|
+
...collectTextareaValueParts(
|
|
856
|
+
node,
|
|
857
|
+
escapeHelperName,
|
|
858
|
+
escapeBatchHelperName,
|
|
859
|
+
asyncComponentNames,
|
|
860
|
+
dynamicAttributes,
|
|
861
|
+
contextProviderHelperName,
|
|
862
|
+
contextConsumerHelperName,
|
|
863
|
+
reactNodeRenderHelperName,
|
|
864
|
+
attributeScan,
|
|
865
|
+
),
|
|
866
|
+
stringLiteral(closeTag),
|
|
867
|
+
];
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const childrenExpression = emitBatchedSimpleChildrenExpression(
|
|
871
|
+
node.children,
|
|
872
|
+
escapeBatchHelperName,
|
|
873
|
+
);
|
|
874
|
+
const attributeScan = scanElementAttributes(node.tagName, node.attributes);
|
|
875
|
+
const childSelectedValueCode = node.tagName === "select"
|
|
876
|
+
? attributeScan.formValueAttributeCode
|
|
877
|
+
: undefined;
|
|
878
|
+
const selectedAttributePart = collectOptionSelectedAttributePart(node, selectedValueCode);
|
|
879
|
+
const dangerousInnerHtml = emitDangerouslySetInnerHtmlExpression(node.attributes);
|
|
880
|
+
const childrenParts =
|
|
881
|
+
dangerousInnerHtml !== undefined
|
|
882
|
+
? [dangerousInnerHtml]
|
|
883
|
+
: childrenExpression === undefined || childSelectedValueCode !== undefined
|
|
884
|
+
? node.children.flatMap((child) =>
|
|
885
|
+
collectHtmlParts(
|
|
886
|
+
child,
|
|
887
|
+
escapeHelperName,
|
|
888
|
+
escapeBatchHelperName,
|
|
889
|
+
asyncComponentNames,
|
|
890
|
+
dynamicAttributes,
|
|
891
|
+
contextProviderHelperName,
|
|
892
|
+
contextConsumerHelperName,
|
|
893
|
+
reactNodeRenderHelperName,
|
|
894
|
+
childSelectedValueCode,
|
|
895
|
+
),
|
|
896
|
+
)
|
|
897
|
+
: [childrenExpression];
|
|
898
|
+
|
|
899
|
+
return [
|
|
900
|
+
stringLiteral(`<${node.tagName}`),
|
|
901
|
+
...collectElementAttributeParts(
|
|
902
|
+
node.tagName,
|
|
903
|
+
node.attributes,
|
|
904
|
+
escapeHelperName,
|
|
905
|
+
escapeBatchHelperName,
|
|
906
|
+
dynamicAttributes,
|
|
907
|
+
attributeScan,
|
|
908
|
+
),
|
|
909
|
+
...(selectedAttributePart === undefined ? [] : [selectedAttributePart]),
|
|
910
|
+
stringLiteral(">"),
|
|
911
|
+
...childrenParts,
|
|
912
|
+
stringLiteral(closeTag),
|
|
913
|
+
];
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function emitDangerouslySetInnerHtmlExpression(attrs: readonly AttributeIr[]): string | undefined {
|
|
917
|
+
const attr = attrs.find(
|
|
918
|
+
(candidate): candidate is Extract<AttributeIr, { kind: "dynamic-attr" }> =>
|
|
919
|
+
candidate.kind === "dynamic-attr" && candidate.name === "dangerouslySetInnerHTML",
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
if (attr === undefined) {
|
|
923
|
+
return undefined;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return `(() => { const _value = (${attr.code}); return typeof _value === "object" && _value !== null && typeof _value.__html === "string" ? _value.__html : ""; })()`;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function collectHtmlAttributeParts(
|
|
930
|
+
tagName: string,
|
|
931
|
+
attr: AttributeIr,
|
|
932
|
+
escapeHelperName: string,
|
|
933
|
+
escapeBatchHelperName: string | undefined,
|
|
934
|
+
dynamicAttributes: "drop" | "emit",
|
|
935
|
+
): string[] {
|
|
936
|
+
if (attr.kind === "spread-attr") {
|
|
937
|
+
return dynamicAttributes === "drop"
|
|
938
|
+
? []
|
|
939
|
+
: [`${currentSpreadAttributesHelperName}(${stringLiteral(tagName)}, (${attr.code}))`];
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (attr.kind === "event" || attr.name === "key" || attr.name === "dangerouslySetInnerHTML") {
|
|
943
|
+
return [];
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const htmlName = htmlAttributeNameForElement(tagName, attr.name);
|
|
947
|
+
|
|
948
|
+
if (attr.kind === "static-attr") {
|
|
949
|
+
// Reject literal `javascript:` / `data:` / etc. in JSX source. This
|
|
950
|
+
// never produces a runtime branch because the value is known at
|
|
951
|
+
// compile time -- we just drop the attribute (matching the dynamic
|
|
952
|
+
// path) so a developer cannot statically introduce the same XSS.
|
|
953
|
+
if (isUrlAttribute(htmlName) && isStaticUrlValueUnsafe(htmlName, attr.value)) {
|
|
954
|
+
return [];
|
|
955
|
+
}
|
|
956
|
+
// Issue 077: a literal string value for `srcdoc` etc. can never be
|
|
957
|
+
// the `{ __html: ... }` opt-in shape, so it is dropped at compile
|
|
958
|
+
// time.
|
|
959
|
+
if (isDangerousHtmlAttribute(htmlName)) {
|
|
960
|
+
return [];
|
|
961
|
+
}
|
|
962
|
+
return [`${stringLiteral(` ${htmlName}="${escapeHtml(attr.value)}"`)}`];
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (dynamicAttributes === "drop") {
|
|
966
|
+
return [];
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (attr.name === "style") {
|
|
970
|
+
return [emitDynamicStyleAttributeExpression(attr.code, escapeHelperName, escapeBatchHelperName)];
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (isDangerousHtmlAttribute(htmlName)) {
|
|
974
|
+
// Dynamic srcdoc must arrive as `{ __html: "..." }`. Drop anything
|
|
975
|
+
// else at runtime so a value computed from a loader cannot inject
|
|
976
|
+
// executable HTML into the iframe document.
|
|
977
|
+
return [
|
|
978
|
+
`(() => { const _value = (${attr.code}); if (_value == null || _value === false) return ""; if (typeof _value === "object" && _value !== null && typeof _value.__html === "string") return ${stringLiteral(` ${htmlName}="`)} + ${escapeHelperName}(_value.__html) + ${stringLiteral("\"")}; return ""; })()`,
|
|
979
|
+
];
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
return [emitDynamicAttributeExpression(htmlName, attr.code, escapeHelperName)];
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function collectElementAttributeParts(
|
|
986
|
+
tagName: string,
|
|
987
|
+
attrs: readonly AttributeIr[],
|
|
988
|
+
escapeHelperName: string,
|
|
989
|
+
escapeBatchHelperName: string | undefined,
|
|
990
|
+
dynamicAttributes: "drop" | "emit",
|
|
991
|
+
attributeScan = scanElementAttributes(tagName, attrs),
|
|
992
|
+
): string[] {
|
|
993
|
+
return attrs.flatMap((attr) =>
|
|
994
|
+
attr.kind !== "spread-attr" &&
|
|
995
|
+
((tagName === "input" &&
|
|
996
|
+
((attr.name === "defaultValue" && attributeScan.hasExplicitInputValue) ||
|
|
997
|
+
(attr.name === "defaultChecked" && attributeScan.hasExplicitInputChecked))) ||
|
|
998
|
+
((tagName === "textarea" || tagName === "select") &&
|
|
999
|
+
(attr.name === "value" || attr.name === "defaultValue")))
|
|
1000
|
+
? []
|
|
1001
|
+
: collectHtmlAttributeParts(
|
|
1002
|
+
tagName,
|
|
1003
|
+
attr,
|
|
1004
|
+
escapeHelperName,
|
|
1005
|
+
escapeBatchHelperName,
|
|
1006
|
+
dynamicAttributes,
|
|
1007
|
+
),
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
interface ElementAttributeScan {
|
|
1012
|
+
hasExplicitInputValue: boolean;
|
|
1013
|
+
hasExplicitInputChecked: boolean;
|
|
1014
|
+
formValueAttributeCode: string | undefined;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function scanElementAttributes(
|
|
1018
|
+
tagName: string,
|
|
1019
|
+
attrs: readonly AttributeIr[],
|
|
1020
|
+
): ElementAttributeScan {
|
|
1021
|
+
let hasExplicitInputValue = false;
|
|
1022
|
+
let hasExplicitInputChecked = false;
|
|
1023
|
+
let valueAttributeCode: string | undefined;
|
|
1024
|
+
let defaultValueAttributeCode: string | undefined;
|
|
1025
|
+
|
|
1026
|
+
for (const attr of attrs) {
|
|
1027
|
+
if (attr.kind === "spread-attr") {
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
if (tagName === "input") {
|
|
1032
|
+
if (attr.name === "value") {
|
|
1033
|
+
hasExplicitInputValue = true;
|
|
1034
|
+
} else if (attr.name === "checked") {
|
|
1035
|
+
hasExplicitInputChecked = true;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if ((tagName === "textarea" || tagName === "select") && attr.name === "value") {
|
|
1040
|
+
valueAttributeCode = readFormValueAttributeCode(attr);
|
|
1041
|
+
} else if (
|
|
1042
|
+
(tagName === "textarea" || tagName === "select") &&
|
|
1043
|
+
attr.name === "defaultValue"
|
|
1044
|
+
) {
|
|
1045
|
+
defaultValueAttributeCode = readFormValueAttributeCode(attr);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
return {
|
|
1050
|
+
hasExplicitInputValue,
|
|
1051
|
+
hasExplicitInputChecked,
|
|
1052
|
+
formValueAttributeCode: valueAttributeCode ?? defaultValueAttributeCode,
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function readFormValueAttributeCode(
|
|
1057
|
+
attr: Exclude<AttributeIr, { kind: "spread-attr" }>,
|
|
1058
|
+
): string | undefined {
|
|
1059
|
+
if (attr.kind === "event") {
|
|
1060
|
+
return undefined;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
return attr.kind === "static-attr" ? stringLiteral(attr.value) : `(${attr.code})`;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function emitDynamicAttributeExpression(
|
|
1067
|
+
name: string,
|
|
1068
|
+
code: string,
|
|
1069
|
+
escapeHelperName: string,
|
|
1070
|
+
): string {
|
|
1071
|
+
if (isUrlAttribute(name)) {
|
|
1072
|
+
// Run the value through the inline URL safety helper. The helper
|
|
1073
|
+
// returns the value when safe and `undefined` when the attribute
|
|
1074
|
+
// should be dropped. Using an IIFE here is necessary because we
|
|
1075
|
+
// need to capture the value once and branch on the helper output.
|
|
1076
|
+
return `(() => { const _value = (${code}); if (_value == null || _value === false) return ""; const _checked = ${currentUrlSafeHelperName}(${stringLiteral(name)}, _value === true ? "" : _value); return _checked === undefined ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_checked) + ${stringLiteral("\"")}; })()`;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const inlineExpr = simpleSideEffectFreeExpression(code);
|
|
1080
|
+
|
|
1081
|
+
if (inlineExpr !== undefined) {
|
|
1082
|
+
// Inline 3 evaluations to avoid per-attribute IIFE closure allocation.
|
|
1083
|
+
// Safe because `simpleSideEffectFreeExpression` only matches expressions
|
|
1084
|
+
// whose evaluation has no observable side effects (identifier read,
|
|
1085
|
+
// member chain, literal, this).
|
|
1086
|
+
return `(${inlineExpr} == null || ${inlineExpr} === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(${inlineExpr} === true ? "" : ${inlineExpr}) + ${stringLiteral("\"")})`;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
return `(() => { const _value = (${code}); return _value == null || _value === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_value === true ? "" : _value) + ${stringLiteral("\"")}; })()`;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function emitDynamicStyleAttributeExpression(
|
|
1093
|
+
code: string,
|
|
1094
|
+
escapeHelperName: string,
|
|
1095
|
+
escapeBatchHelperName: string | undefined,
|
|
1096
|
+
): string {
|
|
1097
|
+
const staticStyleExpression = emitStaticStyleObjectAttributeExpression(code, escapeHelperName);
|
|
1098
|
+
|
|
1099
|
+
if (staticStyleExpression !== undefined) {
|
|
1100
|
+
return staticStyleExpression;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const escapedPair = escapeBatchHelperName === undefined
|
|
1104
|
+
? `${escapeHelperName}(_cssName) + ":" + ${escapeHelperName}(_styleValue === true ? "" : _styleValue)`
|
|
1105
|
+
: `(() => { const _escaped = ${escapeBatchHelperName}([_cssName, _styleValue === true ? "" : _styleValue]); return _escaped[0] + ":" + _escaped[1]; })()`;
|
|
1106
|
+
|
|
1107
|
+
return `(() => { const _value = (${code}); if (_value == null || _value === false) return ""; if (typeof _value === "string") { const _style = ${escapeHelperName}(_value); return _style === "" ? "" : ${stringLiteral(" style=\"")} + _style + ${stringLiteral("\"")}; } const _style = Object.entries(_value).filter(([, _styleValue]) => _styleValue != null && _styleValue !== false).map(([_styleName, _styleValue]) => { const _cssName = String(_styleName).startsWith("--") ? String(_styleName) : String(_styleName).replace(/[A-Z]/g, (_char) => "-" + _char.toLowerCase()); return ${escapedPair}; }).join(";"); return _style === "" ? "" : ${stringLiteral(" style=\"")} + _style + ${stringLiteral("\"")}; })()`;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function emitStaticStyleObjectAttributeExpression(
|
|
1111
|
+
code: string,
|
|
1112
|
+
escapeHelperName: string,
|
|
1113
|
+
): string | undefined {
|
|
1114
|
+
const entries = parseStaticStyleObjectLiteral(code);
|
|
1115
|
+
|
|
1116
|
+
if (entries === undefined) {
|
|
1117
|
+
return undefined;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (entries.length === 0) {
|
|
1121
|
+
return `""`;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Stage B — all values are compile-time literals: collapse to a single
|
|
1125
|
+
// constant string. null/false entries are dropped at build time.
|
|
1126
|
+
const literalEntries = entries.map((entry) => ({
|
|
1127
|
+
cssName: entry.cssName,
|
|
1128
|
+
literal: parseStyleLiteralValue(entry.valueCode),
|
|
1129
|
+
}));
|
|
1130
|
+
|
|
1131
|
+
if (literalEntries.every((entry) => entry.literal !== undefined)) {
|
|
1132
|
+
const parts = literalEntries
|
|
1133
|
+
.filter((entry) => entry.literal !== null)
|
|
1134
|
+
.map((entry) => `${entry.cssName}:${escapeHtml(String(entry.literal))}`);
|
|
1135
|
+
|
|
1136
|
+
if (parts.length === 0) {
|
|
1137
|
+
return `""`;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
return stringLiteral(` style="${parts.join(";")}"`);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Stage A — needSep tracking with inline string accumulator, no intermediate
|
|
1144
|
+
// array allocation and no `.join(";")` per render.
|
|
1145
|
+
const statements = entries.map((entry) =>
|
|
1146
|
+
`{ const _v = (${entry.valueCode}); if (_v != null && _v !== false) _style += (_style === "" ? "" : ";") + ${stringLiteral(`${entry.cssName}:`)} + ${escapeHelperName}(_v === true ? "" : _v); }`
|
|
1147
|
+
);
|
|
1148
|
+
|
|
1149
|
+
return `(() => { let _style = ""; ${statements.join(" ")} return _style === "" ? "" : ${stringLiteral(" style=\"")} + _style + ${stringLiteral("\"")}; })()`;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function collectTextareaValueParts(
|
|
1153
|
+
node: Extract<JsxNodeIr, { kind: "element" }>,
|
|
1154
|
+
escapeHelperName: string,
|
|
1155
|
+
escapeBatchHelperName: string | undefined,
|
|
1156
|
+
asyncComponentNames: ReadonlySet<string>,
|
|
1157
|
+
dynamicAttributes: "drop" | "emit",
|
|
1158
|
+
contextProviderHelperName?: string,
|
|
1159
|
+
contextConsumerHelperName?: string,
|
|
1160
|
+
reactNodeRenderHelperName?: string,
|
|
1161
|
+
attributeScan = scanElementAttributes(node.tagName, node.attributes),
|
|
1162
|
+
): string[] {
|
|
1163
|
+
const valueCode = attributeScan.formValueAttributeCode;
|
|
1164
|
+
if (valueCode !== undefined) {
|
|
1165
|
+
return [`${escapeHelperName}(${valueCode})`];
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
return node.children.flatMap((child) =>
|
|
1169
|
+
collectHtmlParts(
|
|
1170
|
+
child,
|
|
1171
|
+
escapeHelperName,
|
|
1172
|
+
escapeBatchHelperName,
|
|
1173
|
+
asyncComponentNames,
|
|
1174
|
+
dynamicAttributes,
|
|
1175
|
+
contextProviderHelperName,
|
|
1176
|
+
contextConsumerHelperName,
|
|
1177
|
+
reactNodeRenderHelperName,
|
|
1178
|
+
)
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function collectOptionSelectedAttributePart(
|
|
1183
|
+
node: Extract<JsxNodeIr, { kind: "element" }>,
|
|
1184
|
+
selectedValueCode: string | undefined,
|
|
1185
|
+
): string | undefined {
|
|
1186
|
+
if (selectedValueCode === undefined || node.tagName !== "option") {
|
|
1187
|
+
return undefined;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const optionValueCode = findOptionValueCode(node);
|
|
1191
|
+
if (optionValueCode === undefined) {
|
|
1192
|
+
return undefined;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
return `(() => { const _selected = (${selectedValueCode}); return _selected == null ? "" : String(_selected) === String(${optionValueCode}) ? ${stringLiteral(' selected=""')} : ""; })()`;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function findOptionValueCode(node: Extract<JsxNodeIr, { kind: "element" }>): string | undefined {
|
|
1199
|
+
const valueAttr = node.attributes.find((attr) => attr.kind !== "spread-attr" && attr.name === "value");
|
|
1200
|
+
if (valueAttr !== undefined && valueAttr.kind !== "event" && valueAttr.kind !== "spread-attr") {
|
|
1201
|
+
return valueAttr.kind === "static-attr" ? stringLiteral(valueAttr.value) : `(${valueAttr.code})`;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
return node.children.every((child) => child.kind === "text")
|
|
1205
|
+
? stringLiteral(node.children.map((child) => child.value).join(""))
|
|
1206
|
+
: undefined;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function htmlAttributeNameForElement(tagName: string, name: string): string {
|
|
1210
|
+
if (tagName === "input") {
|
|
1211
|
+
if (name === "defaultValue") {
|
|
1212
|
+
return "value";
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
if (name === "defaultChecked") {
|
|
1216
|
+
return "checked";
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
return htmlAttributeName(name);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function rawHtmlExpression(code: string): string {
|
|
1224
|
+
return `(() => { const _value = (${code}); return Array.isArray(_value) ? _value.join("") : String(_value ?? ""); })()`;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function emitBatchedSimpleChildrenExpression(
|
|
1228
|
+
children: readonly JsxNodeIr[],
|
|
1229
|
+
escapeBatchHelperName: string | undefined,
|
|
1230
|
+
): string | undefined {
|
|
1231
|
+
if (escapeBatchHelperName === undefined) {
|
|
1232
|
+
return undefined;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
const dynamicChildren = children.filter(
|
|
1236
|
+
(child) => child.kind === "expr" && child.renderMode !== "html" && child.renderMode !== "react-node",
|
|
1237
|
+
) as Array<Extract<JsxNodeIr, { kind: "expr" }>>;
|
|
1238
|
+
|
|
1239
|
+
if (dynamicChildren.length < 2) {
|
|
1240
|
+
return undefined;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
if (
|
|
1244
|
+
children.some(
|
|
1245
|
+
(child) =>
|
|
1246
|
+
child.kind !== "text" &&
|
|
1247
|
+
!(child.kind === "expr" && child.renderMode !== "html" && child.renderMode !== "react-node"),
|
|
1248
|
+
)
|
|
1249
|
+
) {
|
|
1250
|
+
return undefined;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const values = dynamicChildren.map((child) => child.code);
|
|
1254
|
+
let dynamicIndex = 0;
|
|
1255
|
+
const pieces = children.map((child) => {
|
|
1256
|
+
if (child.kind === "text") {
|
|
1257
|
+
return stringLiteral(escapeHtml(child.value));
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const index = dynamicIndex;
|
|
1261
|
+
dynamicIndex += 1;
|
|
1262
|
+
|
|
1263
|
+
return `_escaped[${index}]`;
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
return `(() => { const _escaped = ${escapeBatchHelperName}([${values.join(", ")}]); return ${pieces.join(" + ")}; })()`;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function emitHtmlExpressionFromChildren(
|
|
1270
|
+
children: JsxNodeIr[],
|
|
1271
|
+
escapeHelperName: string,
|
|
1272
|
+
escapeBatchHelperName: string | undefined,
|
|
1273
|
+
asyncComponentNames: ReadonlySet<string>,
|
|
1274
|
+
dynamicAttributes: "drop" | "emit",
|
|
1275
|
+
contextProviderHelperName?: string,
|
|
1276
|
+
contextConsumerHelperName?: string,
|
|
1277
|
+
reactNodeRenderHelperName?: string,
|
|
1278
|
+
): string {
|
|
1279
|
+
if (children.length === 0) {
|
|
1280
|
+
return "\"\"";
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
return emitHtmlExpression(
|
|
1284
|
+
{ kind: "fragment", children },
|
|
1285
|
+
escapeHelperName,
|
|
1286
|
+
escapeBatchHelperName,
|
|
1287
|
+
asyncComponentNames,
|
|
1288
|
+
dynamicAttributes,
|
|
1289
|
+
contextProviderHelperName,
|
|
1290
|
+
contextConsumerHelperName,
|
|
1291
|
+
reactNodeRenderHelperName,
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function emitSyncListIife(
|
|
1296
|
+
node: Extract<JsxNodeIr, { kind: "list" }>,
|
|
1297
|
+
escapeHelperName: string,
|
|
1298
|
+
escapeBatchHelperName: string | undefined,
|
|
1299
|
+
asyncComponentNames: ReadonlySet<string>,
|
|
1300
|
+
dynamicAttributes: "drop" | "emit",
|
|
1301
|
+
contextProviderHelperName?: string,
|
|
1302
|
+
contextConsumerHelperName?: string,
|
|
1303
|
+
reactNodeRenderHelperName?: string,
|
|
1304
|
+
): string {
|
|
1305
|
+
const valueExpression = emitHtmlExpressionFromChildren(
|
|
1306
|
+
node.children,
|
|
1307
|
+
escapeHelperName,
|
|
1308
|
+
escapeBatchHelperName,
|
|
1309
|
+
asyncComponentNames,
|
|
1310
|
+
dynamicAttributes,
|
|
1311
|
+
contextProviderHelperName,
|
|
1312
|
+
contextConsumerHelperName,
|
|
1313
|
+
reactNodeRenderHelperName,
|
|
1314
|
+
);
|
|
1315
|
+
const itemBinding = `const ${node.itemName} = _arr[_i];`;
|
|
1316
|
+
const indexBinding =
|
|
1317
|
+
node.indexName === undefined ? "" : ` const ${node.indexName} = _i;`;
|
|
1318
|
+
const bodyStatements =
|
|
1319
|
+
node.bodyStatements === undefined || node.bodyStatements.length === 0
|
|
1320
|
+
? ""
|
|
1321
|
+
: ` ${node.bodyStatements.join(" ")}`;
|
|
1322
|
+
|
|
1323
|
+
return `(() => { let _o = ""; const _arr = (${node.itemsCode}); for (let _i = 0, _len = _arr.length; _i < _len; _i++) { ${itemBinding}${indexBinding}${bodyStatements} _o += ${valueExpression}; } return _o; })()`;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function emitListRenderer(
|
|
1327
|
+
node: Extract<JsxNodeIr, { kind: "list" }>,
|
|
1328
|
+
parameters: string,
|
|
1329
|
+
escapeHelperName: string,
|
|
1330
|
+
escapeBatchHelperName: string | undefined,
|
|
1331
|
+
asyncComponentNames: ReadonlySet<string>,
|
|
1332
|
+
dynamicAttributes: "drop" | "emit",
|
|
1333
|
+
contextProviderHelperName?: string,
|
|
1334
|
+
contextConsumerHelperName?: string,
|
|
1335
|
+
reactNodeRenderHelperName?: string,
|
|
1336
|
+
): string {
|
|
1337
|
+
const valueExpression = emitHtmlExpressionFromChildren(
|
|
1338
|
+
node.children,
|
|
1339
|
+
escapeHelperName,
|
|
1340
|
+
escapeBatchHelperName,
|
|
1341
|
+
asyncComponentNames,
|
|
1342
|
+
dynamicAttributes,
|
|
1343
|
+
contextProviderHelperName,
|
|
1344
|
+
contextConsumerHelperName,
|
|
1345
|
+
reactNodeRenderHelperName,
|
|
1346
|
+
);
|
|
1347
|
+
const asyncKeyword = containsAsyncServerOperationInChildren(
|
|
1348
|
+
node.children,
|
|
1349
|
+
asyncComponentNames,
|
|
1350
|
+
)
|
|
1351
|
+
? "async "
|
|
1352
|
+
: "";
|
|
1353
|
+
|
|
1354
|
+
if (node.bodyStatements === undefined || node.bodyStatements.length === 0) {
|
|
1355
|
+
return `${asyncKeyword}(${parameters}) => ${valueExpression}`;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
return `${asyncKeyword}(${parameters}) => {\n${node.bodyStatements.map((statement) => ` ${statement}`).join("\n")}\n return ${valueExpression};\n }`;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function emitPropsObject(
|
|
1362
|
+
props: ComponentPropIr[],
|
|
1363
|
+
children: JsxNodeIr[] = [],
|
|
1364
|
+
escapeHelperName: string,
|
|
1365
|
+
escapeBatchHelperName: string | undefined,
|
|
1366
|
+
asyncComponentNames: ReadonlySet<string>,
|
|
1367
|
+
dynamicAttributes: "drop" | "emit",
|
|
1368
|
+
contextProviderHelperName?: string,
|
|
1369
|
+
contextConsumerHelperName?: string,
|
|
1370
|
+
reactNodeRenderHelperName?: string,
|
|
1371
|
+
): string {
|
|
1372
|
+
const entries = props.map((prop) => {
|
|
1373
|
+
if (prop.kind === "spread-prop") {
|
|
1374
|
+
return `...(${prop.code})`;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
if (prop.kind === "render-prop") {
|
|
1378
|
+
return `${emitPropName(prop.name)}: ${emitHtmlExpressionFromChildren(prop.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)}`;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
return `${emitPropName(prop.name)}: (${prop.code})`;
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
if (children.length > 0) {
|
|
1385
|
+
entries.push(
|
|
1386
|
+
`children: ${emitHtmlExpressionFromChildren(children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)}`,
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
return `{ ${entries.join(", ")} }`;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
function emitComponentCallExpression(
|
|
1394
|
+
name: string,
|
|
1395
|
+
propsCode: string,
|
|
1396
|
+
asyncComponentNames: ReadonlySet<string>,
|
|
1397
|
+
): string {
|
|
1398
|
+
const call = `${name}(${propsCode})`;
|
|
1399
|
+
return asyncComponentNames.has(name) ? `(await ${call})` : call;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function isClientBoundaryPlaceholder(node: Extract<JsxNodeIr, { kind: "component" }>): boolean {
|
|
1403
|
+
return node.clientReference !== undefined && !isCompatClientReference(node);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function isCompatClientReference(node: Extract<JsxNodeIr, { kind: "component" }>): boolean {
|
|
1407
|
+
return node.clientReference !== undefined && /\.(?:compat)\.[cm]?[jt]sx?$/.test(node.clientReference.moduleId);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function clientBoundaryPlaceholder(node: Extract<JsxNodeIr, { kind: "component" }>): string {
|
|
1411
|
+
return `<!--mreact-client-boundary:${escapeHtml(node.name)}-->`;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function collectAsyncServerComponentNames(components: readonly ComponentIr[]): Set<string> {
|
|
1415
|
+
const names = new Set(
|
|
1416
|
+
components
|
|
1417
|
+
.filter((component) => component.async === true)
|
|
1418
|
+
.map((component) => component.name),
|
|
1419
|
+
);
|
|
1420
|
+
|
|
1421
|
+
let changed = true;
|
|
1422
|
+
|
|
1423
|
+
while (changed) {
|
|
1424
|
+
changed = false;
|
|
1425
|
+
|
|
1426
|
+
for (const component of components) {
|
|
1427
|
+
if (
|
|
1428
|
+
!names.has(component.name) &&
|
|
1429
|
+
containsAsyncServerOperation(component.root, names)
|
|
1430
|
+
) {
|
|
1431
|
+
names.add(component.name);
|
|
1432
|
+
changed = true;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
return names;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function containsAsyncServerOperationInChildren(
|
|
1441
|
+
children: readonly JsxNodeIr[],
|
|
1442
|
+
asyncComponentNames: ReadonlySet<string>,
|
|
1443
|
+
): boolean {
|
|
1444
|
+
return children.some((child) => containsAsyncServerOperation(child, asyncComponentNames));
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function containsAsyncServerOperation(
|
|
1448
|
+
node: JsxNodeIr,
|
|
1449
|
+
asyncComponentNames: ReadonlySet<string>,
|
|
1450
|
+
): boolean {
|
|
1451
|
+
if (node.kind === "async-boundary") {
|
|
1452
|
+
return true;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
if (node.kind === "component") {
|
|
1456
|
+
return (
|
|
1457
|
+
asyncComponentNames.has(node.name) ||
|
|
1458
|
+
containsAsyncServerOperationInChildren(node.children, asyncComponentNames) ||
|
|
1459
|
+
node.props.some(
|
|
1460
|
+
(prop) =>
|
|
1461
|
+
prop.kind === "render-prop" &&
|
|
1462
|
+
containsAsyncServerOperationInChildren(prop.children, asyncComponentNames),
|
|
1463
|
+
)
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
if (node.kind === "conditional") {
|
|
1468
|
+
return containsAsyncServerOperationInChildren(
|
|
1469
|
+
[...node.whenTrue, ...node.whenFalse],
|
|
1470
|
+
asyncComponentNames,
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
if (node.kind === "list") {
|
|
1475
|
+
return containsAsyncServerOperationInChildren(node.children, asyncComponentNames);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
if (node.kind === "element" || node.kind === "fragment") {
|
|
1479
|
+
return containsAsyncServerOperationInChildren(node.children, asyncComponentNames);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
return false;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function emitPropName(name: string): string {
|
|
1486
|
+
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function allocateEscapeHelperName(ir: ModuleIr): string {
|
|
1490
|
+
return allocateHelperName(ir, "_escapeHtml");
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function allocateHelperName(ir: ModuleIr, baseName: string): string {
|
|
1494
|
+
const reservedNames = new Set<string>(ir.moduleBindingNames);
|
|
1495
|
+
|
|
1496
|
+
for (const component of ir.components) {
|
|
1497
|
+
reservedNames.add(component.name);
|
|
1498
|
+
|
|
1499
|
+
for (const bindingName of component.bindingNames) {
|
|
1500
|
+
reservedNames.add(bindingName);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
let name = baseName;
|
|
1505
|
+
let index = 1;
|
|
1506
|
+
|
|
1507
|
+
while (reservedNames.has(name)) {
|
|
1508
|
+
name = `${baseName}$${index}`;
|
|
1509
|
+
index += 1;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
return name;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
function usesContextProvider(ir: ModuleIr): boolean {
|
|
1516
|
+
return ir.components.some((component) => containsContextProvider(component.root));
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function usesContextConsumer(ir: ModuleIr): boolean {
|
|
1520
|
+
return ir.components.some((component) => containsContextConsumer(component.root));
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function usesReactNodeRender(ir: ModuleIr): boolean {
|
|
1524
|
+
return ir.components.some(
|
|
1525
|
+
(component) =>
|
|
1526
|
+
containsReactNodeRender(component.root) ||
|
|
1527
|
+
component.bodyStatements.some((statement) =>
|
|
1528
|
+
statement.includes(oxcServerStringReactNodeRenderHelperPlaceholder),
|
|
1529
|
+
),
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function replaceOxcServerStringReactNodeRenderHelper(
|
|
1534
|
+
code: string,
|
|
1535
|
+
helperName: string | undefined,
|
|
1536
|
+
): string {
|
|
1537
|
+
return helperName === undefined
|
|
1538
|
+
? code
|
|
1539
|
+
: code.replaceAll(oxcServerStringReactNodeRenderHelperPlaceholder, helperName);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function usesClientBoundary(ir: ModuleIr): boolean {
|
|
1543
|
+
return ir.components.some((component) => containsClientBoundary(component.root));
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
function emitClientBoundaryHelper(name: string): string {
|
|
1547
|
+
const propsHelperName = `${name}$hasNonSerializableProps`;
|
|
1548
|
+
|
|
1549
|
+
return [
|
|
1550
|
+
`function ${propsHelperName}(value) {`,
|
|
1551
|
+
` if (typeof value === "function" || typeof value === "symbol") return true;`,
|
|
1552
|
+
` if (value === null || typeof value !== "object") return false;`,
|
|
1553
|
+
` if (Array.isArray(value)) return value.some(${propsHelperName});`,
|
|
1554
|
+
` for (const key of Object.keys(value)) {`,
|
|
1555
|
+
` if (${propsHelperName}(value[key])) return true;`,
|
|
1556
|
+
` }`,
|
|
1557
|
+
` return false;`,
|
|
1558
|
+
`}`,
|
|
1559
|
+
`function ${name}(name, props) {`,
|
|
1560
|
+
` const _name = String(name);`,
|
|
1561
|
+
` const _escapedName = _name.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");`,
|
|
1562
|
+
` const _props = props ?? {};`,
|
|
1563
|
+
` const _nonSerializable = ${propsHelperName}(_props);`,
|
|
1564
|
+
` const _nonSerializableAttr = _nonSerializable ? ' data-mreact-client-boundary-nonserializable="true"' : "";`,
|
|
1565
|
+
` const _json = (JSON.stringify(_props) ?? "{}").replaceAll("<", "\\\\u003c");`,
|
|
1566
|
+
` return \`<template data-mreact-client-boundary="\${_escapedName}"\${_nonSerializableAttr}></template><script type="application/json" data-mreact-client-boundary-props="\${_escapedName}">\${_json}</script>\`;`,
|
|
1567
|
+
`}`,
|
|
1568
|
+
].join("\n");
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function emitSpreadAttributesHelper(
|
|
1572
|
+
name: string,
|
|
1573
|
+
escapeHelperName: string,
|
|
1574
|
+
urlSafeHelperName: string,
|
|
1575
|
+
): string {
|
|
1576
|
+
const aliases = JSON.stringify({
|
|
1577
|
+
acceptCharset: "accept-charset",
|
|
1578
|
+
autoFocus: "autofocus",
|
|
1579
|
+
autoPlay: "autoplay",
|
|
1580
|
+
charSet: "charset",
|
|
1581
|
+
className: "class",
|
|
1582
|
+
colSpan: "colspan",
|
|
1583
|
+
contentEditable: "contenteditable",
|
|
1584
|
+
crossOrigin: "crossorigin",
|
|
1585
|
+
encType: "enctype",
|
|
1586
|
+
formAction: "formaction",
|
|
1587
|
+
frameBorder: "frameborder",
|
|
1588
|
+
htmlFor: "for",
|
|
1589
|
+
httpEquiv: "http-equiv",
|
|
1590
|
+
maxLength: "maxlength",
|
|
1591
|
+
minLength: "minlength",
|
|
1592
|
+
noValidate: "novalidate",
|
|
1593
|
+
playsInline: "playsinline",
|
|
1594
|
+
readOnly: "readonly",
|
|
1595
|
+
rowSpan: "rowspan",
|
|
1596
|
+
spellCheck: "spellcheck",
|
|
1597
|
+
srcDoc: "srcdoc",
|
|
1598
|
+
srcSet: "srcset",
|
|
1599
|
+
tabIndex: "tabindex",
|
|
1600
|
+
useMap: "usemap",
|
|
1601
|
+
});
|
|
1602
|
+
const urlAttributes = JSON.stringify([
|
|
1603
|
+
"href",
|
|
1604
|
+
"src",
|
|
1605
|
+
"action",
|
|
1606
|
+
"formaction",
|
|
1607
|
+
"xlink:href",
|
|
1608
|
+
"ping",
|
|
1609
|
+
"poster",
|
|
1610
|
+
"background",
|
|
1611
|
+
"manifest",
|
|
1612
|
+
]);
|
|
1613
|
+
const dangerousAttributes = JSON.stringify(["srcdoc"]);
|
|
1614
|
+
|
|
1615
|
+
return [
|
|
1616
|
+
`const ${name}$aliases = ${aliases};`,
|
|
1617
|
+
`const ${name}$urlAttributes = new Set(${urlAttributes});`,
|
|
1618
|
+
`const ${name}$dangerousAttributes = new Set(${dangerousAttributes});`,
|
|
1619
|
+
`function ${name}$style(value) {`,
|
|
1620
|
+
` if (value == null || value === false) return "";`,
|
|
1621
|
+
` if (typeof value === "string") return value;`,
|
|
1622
|
+
` let _style = "";`,
|
|
1623
|
+
` for (const _styleName of Object.keys(value)) {`,
|
|
1624
|
+
` const _styleValue = value[_styleName];`,
|
|
1625
|
+
` if (_styleValue == null || _styleValue === false) continue;`,
|
|
1626
|
+
` const _cssName = String(_styleName).startsWith("--") ? String(_styleName) : String(_styleName).replace(/[A-Z]/g, (_char) => "-" + _char.toLowerCase());`,
|
|
1627
|
+
` _style += (_style === "" ? "" : ";") + _cssName + ":" + (_styleValue === true ? "" : String(_styleValue));`,
|
|
1628
|
+
` }`,
|
|
1629
|
+
` return _style;`,
|
|
1630
|
+
`}`,
|
|
1631
|
+
`function ${name}(tagName, props) {`,
|
|
1632
|
+
` if (props == null || props === false) return "";`,
|
|
1633
|
+
` let _out = "";`,
|
|
1634
|
+
` for (const _rawName of Object.keys(props)) {`,
|
|
1635
|
+
` let _value = props[_rawName];`,
|
|
1636
|
+
` if (_value == null || _value === false) continue;`,
|
|
1637
|
+
` if (_rawName === "key" || _rawName === "ref" || _rawName === "children") continue;`,
|
|
1638
|
+
` if (/^on[A-Za-z]/.test(_rawName)) continue;`,
|
|
1639
|
+
` let _name = tagName === "input" && _rawName === "defaultValue" ? "value" : tagName === "input" && _rawName === "defaultChecked" ? "checked" : (${name}$aliases[_rawName] ?? _rawName);`,
|
|
1640
|
+
` if (!/^[A-Za-z_:][A-Za-z0-9:_.-]*$/.test(_name)) continue;`,
|
|
1641
|
+
` if (_name === "style") {`,
|
|
1642
|
+
` const _style = ${name}$style(_value);`,
|
|
1643
|
+
` if (_style !== "") _out += " style=\\"" + ${escapeHelperName}(_style) + "\\"";`,
|
|
1644
|
+
` continue;`,
|
|
1645
|
+
` }`,
|
|
1646
|
+
` if (${name}$dangerousAttributes.has(_name)) {`,
|
|
1647
|
+
` if (typeof _value === "object" && _value !== null && typeof _value.__html === "string") {`,
|
|
1648
|
+
` _out += " " + _name + "=\\"" + ${escapeHelperName}(_value.__html) + "\\"";`,
|
|
1649
|
+
` }`,
|
|
1650
|
+
` continue;`,
|
|
1651
|
+
` }`,
|
|
1652
|
+
` if (${name}$urlAttributes.has(_name)) {`,
|
|
1653
|
+
` _value = ${urlSafeHelperName}(_name, _value === true ? "" : _value);`,
|
|
1654
|
+
` if (_value === undefined) continue;`,
|
|
1655
|
+
` }`,
|
|
1656
|
+
` _out += " " + _name + "=\\"" + ${escapeHelperName}(_value === true ? "" : _value) + "\\"";`,
|
|
1657
|
+
` }`,
|
|
1658
|
+
` return _out;`,
|
|
1659
|
+
`}`,
|
|
1660
|
+
].join("\n");
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
function containsClientBoundary(node: JsxNodeIr): boolean {
|
|
1664
|
+
if (node.kind === "component" && isClientBoundaryPlaceholder(node)) {
|
|
1665
|
+
return true;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
if (node.kind === "conditional") {
|
|
1669
|
+
return [...node.whenTrue, ...node.whenFalse].some(containsClientBoundary);
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
if (node.kind === "list") {
|
|
1673
|
+
return node.children.some(containsClientBoundary);
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
if (node.kind === "fragment" || node.kind === "element") {
|
|
1677
|
+
return node.children.some(containsClientBoundary);
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
if (node.kind === "component") {
|
|
1681
|
+
return (
|
|
1682
|
+
node.children.some(containsClientBoundary) ||
|
|
1683
|
+
node.props.some(
|
|
1684
|
+
(prop) => prop.kind === "render-prop" && prop.children.some(containsClientBoundary),
|
|
1685
|
+
)
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
if (node.kind === "async-boundary") {
|
|
1690
|
+
return (
|
|
1691
|
+
node.children.some(containsClientBoundary) ||
|
|
1692
|
+
node.placeholderChildren?.some(containsClientBoundary) === true ||
|
|
1693
|
+
node.catchChildren?.some(containsClientBoundary) === true
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
return false;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function containsReactNodeRender(node: JsxNodeIr): boolean {
|
|
1701
|
+
if (node.kind === "expr") {
|
|
1702
|
+
return node.renderMode === "react-node";
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
if (node.kind === "conditional") {
|
|
1706
|
+
return [...node.whenTrue, ...node.whenFalse].some(containsReactNodeRender);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
if (node.kind === "list") {
|
|
1710
|
+
return node.children.some(containsReactNodeRender);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
if (node.kind === "fragment" || node.kind === "element") {
|
|
1714
|
+
return node.children.some(containsReactNodeRender);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
if (node.kind === "component") {
|
|
1718
|
+
if (node.runtime === "compat" && !isClientBoundaryPlaceholder(node)) {
|
|
1719
|
+
return true;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
return (
|
|
1723
|
+
node.children.some(containsReactNodeRender) ||
|
|
1724
|
+
node.props.some(
|
|
1725
|
+
(prop) => prop.kind === "render-prop" && prop.children.some(containsReactNodeRender),
|
|
1726
|
+
)
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
if (node.kind === "async-boundary") {
|
|
1731
|
+
return (
|
|
1732
|
+
node.children.some(containsReactNodeRender) ||
|
|
1733
|
+
node.placeholderChildren?.some(containsReactNodeRender) === true ||
|
|
1734
|
+
node.catchChildren?.some(containsReactNodeRender) === true
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
return false;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
function containsContextProvider(node: JsxNodeIr): boolean {
|
|
1742
|
+
if (node.kind === "component" && node.name.endsWith(".Provider")) {
|
|
1743
|
+
return true;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
if (node.kind === "conditional") {
|
|
1747
|
+
return [...node.whenTrue, ...node.whenFalse].some(containsContextProvider);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
if (node.kind === "list") {
|
|
1751
|
+
return node.children.some(containsContextProvider);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
if (node.kind === "fragment" || node.kind === "element") {
|
|
1755
|
+
return node.children.some(containsContextProvider);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
if (node.kind === "component") {
|
|
1759
|
+
return (
|
|
1760
|
+
node.children.some(containsContextProvider) ||
|
|
1761
|
+
node.props.some(
|
|
1762
|
+
(prop) => prop.kind === "render-prop" && prop.children.some(containsContextProvider),
|
|
1763
|
+
)
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
return false;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function containsContextConsumer(node: JsxNodeIr): boolean {
|
|
1771
|
+
if (node.kind === "component" && node.name.endsWith(".Consumer")) {
|
|
1772
|
+
return true;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
if (node.kind === "conditional") {
|
|
1776
|
+
return [...node.whenTrue, ...node.whenFalse].some(containsContextConsumer);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
if (node.kind === "list") {
|
|
1780
|
+
return node.children.some(containsContextConsumer);
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
if (node.kind === "fragment" || node.kind === "element") {
|
|
1784
|
+
return node.children.some(containsContextConsumer);
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
if (node.kind === "component") {
|
|
1788
|
+
return (
|
|
1789
|
+
node.children.some(containsContextConsumer) ||
|
|
1790
|
+
node.props.some(
|
|
1791
|
+
(prop) => prop.kind === "render-prop" && prop.children.some(containsContextConsumer),
|
|
1792
|
+
)
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
return false;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function findComponentPropCode(
|
|
1800
|
+
props: readonly ComponentPropIr[],
|
|
1801
|
+
name: string,
|
|
1802
|
+
): string | undefined {
|
|
1803
|
+
for (const prop of props) {
|
|
1804
|
+
if (prop.kind === "prop" && prop.name === name) {
|
|
1805
|
+
return prop.code;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
return undefined;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function findComponentRenderProp(
|
|
1813
|
+
props: readonly ComponentPropIr[],
|
|
1814
|
+
name: string,
|
|
1815
|
+
): Extract<ComponentPropIr, { kind: "render-prop" }> | undefined {
|
|
1816
|
+
for (const prop of props) {
|
|
1817
|
+
if (prop.kind === "render-prop" && prop.name === name) {
|
|
1818
|
+
return prop;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
return undefined;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
function stringLiteral(value: string): string {
|
|
1826
|
+
return JSON.stringify(value);
|
|
1827
|
+
}
|