@reckona/mreact-compiler 0.0.65 → 0.0.67
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/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,2558 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AsyncBoundaryIr,
|
|
3
|
+
AttributeIr,
|
|
4
|
+
ComponentPropIr,
|
|
5
|
+
ComponentIr,
|
|
6
|
+
JsxNodeIr,
|
|
7
|
+
ModuleIr,
|
|
8
|
+
} from "./ir.js";
|
|
9
|
+
import type { RuntimeImport, ServerBootstrapMode, ServerEscapeOptions } from "./types.js";
|
|
10
|
+
import { emitEscapeHtmlHelper } from "./emit-escape-helper.js";
|
|
11
|
+
import { escapeHtmlAttribute as escapeHtml } from "@reckona/mreact-shared/html-escape";
|
|
12
|
+
import {
|
|
13
|
+
htmlAttributeName,
|
|
14
|
+
isDangerousHtmlAttribute,
|
|
15
|
+
isStaticUrlValueUnsafe,
|
|
16
|
+
isUrlAttribute,
|
|
17
|
+
parseStaticStyleObjectLiteral,
|
|
18
|
+
parseStyleLiteralValue,
|
|
19
|
+
simpleSideEffectFreeExpression,
|
|
20
|
+
} from "./emit-server-shared.js";
|
|
21
|
+
import { oxcServerStringReactNodeRenderHelperPlaceholder } from "./oxc-runtime-emit.js";
|
|
22
|
+
|
|
23
|
+
export interface EmitServerStreamResult {
|
|
24
|
+
code: string;
|
|
25
|
+
imports: RuntimeImport[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface EmitServerStreamOptions {
|
|
29
|
+
dynamicAttributes?: "drop" | "emit";
|
|
30
|
+
serverBootstrap?: ServerBootstrapMode;
|
|
31
|
+
serverBootstrapNonce?: string;
|
|
32
|
+
serverBootstrapSrc?: string;
|
|
33
|
+
serverHydration?: boolean;
|
|
34
|
+
serverAwaitHydration?: boolean;
|
|
35
|
+
escape?: ServerEscapeOptions | undefined;
|
|
36
|
+
reactSuspenseRevealScriptSrc?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let currentUrlSafeHelperName: string = "_urlAttrSafe";
|
|
40
|
+
let currentClientBoundaryHelperName: string | undefined;
|
|
41
|
+
let currentSpreadAttributesHelperName: string = "_renderSpreadAttributes";
|
|
42
|
+
let currentStreamNodeHelperName: string = "_renderStreamNode";
|
|
43
|
+
let currentAsyncBoundaryHelperName: string = "_renderAsyncBoundary";
|
|
44
|
+
let currentOutOfOrderBoundaryHelperName: string = "_renderOutOfOrderBoundary";
|
|
45
|
+
let currentReactSuspenseBoundaryHelperName: string = "_renderReactSuspenseBoundary";
|
|
46
|
+
let currentReactSuspenseOutOfOrderBoundaryHelperName: string =
|
|
47
|
+
"_renderReactSuspenseOutOfOrderBoundary";
|
|
48
|
+
let currentCompatRenderToStringHelperName: string = "_renderCompatToString";
|
|
49
|
+
let currentPropChildrenCollectState: CollectHtmlState | undefined;
|
|
50
|
+
|
|
51
|
+
export function emitServerStream(
|
|
52
|
+
ir: ModuleIr,
|
|
53
|
+
options: EmitServerStreamOptions = {},
|
|
54
|
+
): EmitServerStreamResult {
|
|
55
|
+
const serverBootstrap = options.serverBootstrap ?? "none";
|
|
56
|
+
const escapeHelperName = allocateHelperName(ir, "_escapeHtml");
|
|
57
|
+
const escapeBatchHelperName = options.escape === undefined
|
|
58
|
+
? undefined
|
|
59
|
+
: allocateHelperName(ir, "_escapeHtmlBatch");
|
|
60
|
+
const asyncBoundaryHelperName = allocateHelperName(ir, "_renderAsyncBoundary");
|
|
61
|
+
const outOfOrderBoundaryHelperName = allocateHelperName(ir, "_renderOutOfOrderBoundary");
|
|
62
|
+
const reorderScriptHelperName = allocateHelperName(ir, "_renderOutOfOrderReorderScript");
|
|
63
|
+
const reactSuspenseBoundaryHelperName = allocateHelperName(ir, "_renderReactSuspenseBoundary");
|
|
64
|
+
const reactSuspenseOutOfOrderBoundaryHelperName = allocateHelperName(
|
|
65
|
+
ir,
|
|
66
|
+
"_renderReactSuspenseOutOfOrderBoundary",
|
|
67
|
+
);
|
|
68
|
+
const compatRenderToStringHelperName = allocateHelperName(ir, "_renderCompatToString");
|
|
69
|
+
const streamNodeHelperName = allocateHelperName(ir, "_renderStreamNode");
|
|
70
|
+
const clientBoundaryHelperName = usesClientBoundary(ir)
|
|
71
|
+
? allocateHelperName(ir, "_renderClientBoundary")
|
|
72
|
+
: undefined;
|
|
73
|
+
const spreadAttributesHelperName = allocateHelperName(ir, "_renderSpreadAttributes");
|
|
74
|
+
const urlSafeHelperName = allocateHelperName(ir, "_urlAttrSafe");
|
|
75
|
+
currentUrlSafeHelperName = urlSafeHelperName;
|
|
76
|
+
currentClientBoundaryHelperName = clientBoundaryHelperName;
|
|
77
|
+
currentSpreadAttributesHelperName = spreadAttributesHelperName;
|
|
78
|
+
currentStreamNodeHelperName = streamNodeHelperName;
|
|
79
|
+
currentAsyncBoundaryHelperName = asyncBoundaryHelperName;
|
|
80
|
+
currentOutOfOrderBoundaryHelperName = outOfOrderBoundaryHelperName;
|
|
81
|
+
currentReactSuspenseBoundaryHelperName = reactSuspenseBoundaryHelperName;
|
|
82
|
+
currentReactSuspenseOutOfOrderBoundaryHelperName = reactSuspenseOutOfOrderBoundaryHelperName;
|
|
83
|
+
currentCompatRenderToStringHelperName = compatRenderToStringHelperName;
|
|
84
|
+
const helper = emitEscapeHtmlHelper(escapeHelperName);
|
|
85
|
+
const urlSafeHelper = [
|
|
86
|
+
`function ${urlSafeHelperName}(name, value) {`,
|
|
87
|
+
` if (typeof value !== "string") return value;`,
|
|
88
|
+
` const _canonical = value`,
|
|
89
|
+
` .replace(/^[\\x00-\\x20]+/u, "")`,
|
|
90
|
+
` .replace(/[\\t\\r\\n]/g, "");`,
|
|
91
|
+
` const _match = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(_canonical);`,
|
|
92
|
+
` if (_match === null) return value;`,
|
|
93
|
+
` const _scheme = _match[1].toLowerCase();`,
|
|
94
|
+
` if (_scheme !== "javascript" && _scheme !== "vbscript" && _scheme !== "livescript" && _scheme !== "mhtml" && _scheme !== "file" && _scheme !== "data") return value;`,
|
|
95
|
+
` if (_scheme === "data" && (name === "src" || name === "poster") && /^data:image\\/(?!svg\\+xml(?:[;,]|$))/i.test(_canonical)) return value;`,
|
|
96
|
+
` return undefined;`,
|
|
97
|
+
`}`,
|
|
98
|
+
].join("\n");
|
|
99
|
+
const components = ir.components
|
|
100
|
+
.map((component) =>
|
|
101
|
+
emitComponent(
|
|
102
|
+
component,
|
|
103
|
+
escapeHelperName,
|
|
104
|
+
asyncBoundaryHelperName,
|
|
105
|
+
outOfOrderBoundaryHelperName,
|
|
106
|
+
reorderScriptHelperName,
|
|
107
|
+
reactSuspenseBoundaryHelperName,
|
|
108
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
109
|
+
compatRenderToStringHelperName,
|
|
110
|
+
{
|
|
111
|
+
serverBootstrap,
|
|
112
|
+
...(options.serverBootstrapNonce === undefined
|
|
113
|
+
? {}
|
|
114
|
+
: { serverBootstrapNonce: options.serverBootstrapNonce }),
|
|
115
|
+
...(options.serverBootstrapSrc === undefined
|
|
116
|
+
? {}
|
|
117
|
+
: { serverBootstrapSrc: options.serverBootstrapSrc }),
|
|
118
|
+
...(options.serverHydration === undefined
|
|
119
|
+
? {}
|
|
120
|
+
: { serverHydration: options.serverHydration }),
|
|
121
|
+
...(options.serverAwaitHydration === undefined
|
|
122
|
+
? {}
|
|
123
|
+
: { serverAwaitHydration: options.serverAwaitHydration }),
|
|
124
|
+
...(options.reactSuspenseRevealScriptSrc === undefined
|
|
125
|
+
? {}
|
|
126
|
+
: { reactSuspenseRevealScriptSrc: options.reactSuspenseRevealScriptSrc }),
|
|
127
|
+
dynamicAttributes: options.dynamicAttributes ?? "emit",
|
|
128
|
+
...(escapeBatchHelperName === undefined ? {} : { escapeBatchHelperName }),
|
|
129
|
+
},
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
.join("\n\n");
|
|
133
|
+
// Emit batch escape import only when the helper is actually referenced
|
|
134
|
+
// (issue 048: dead-import elimination).
|
|
135
|
+
const escapeImport =
|
|
136
|
+
options.escape === undefined ||
|
|
137
|
+
escapeBatchHelperName === undefined ||
|
|
138
|
+
!components.includes(escapeBatchHelperName)
|
|
139
|
+
? ""
|
|
140
|
+
: `import { ${options.escape.batchImportName} as ${escapeBatchHelperName} } from ${stringLiteral(options.escape.batchImportSource)};`;
|
|
141
|
+
const imports = collectImports(ir, serverBootstrap);
|
|
142
|
+
const importAliases: Record<string, string> = {
|
|
143
|
+
renderAsyncBoundary: asyncBoundaryHelperName,
|
|
144
|
+
renderOutOfOrderBoundary: outOfOrderBoundaryHelperName,
|
|
145
|
+
renderOutOfOrderReorderScript: reorderScriptHelperName,
|
|
146
|
+
renderReactSuspenseBoundary: reactSuspenseBoundaryHelperName,
|
|
147
|
+
renderReactSuspenseOutOfOrderBoundary: reactSuspenseOutOfOrderBoundaryHelperName,
|
|
148
|
+
renderToString: compatRenderToStringHelperName,
|
|
149
|
+
};
|
|
150
|
+
const importLine = imports
|
|
151
|
+
.map(
|
|
152
|
+
(runtimeImport) =>
|
|
153
|
+
`import { ${runtimeImport.specifiers
|
|
154
|
+
.map((specifier) => `${specifier} as ${importAliases[specifier]}`)
|
|
155
|
+
.join(", ")} } from "${runtimeImport.source}";`,
|
|
156
|
+
)
|
|
157
|
+
.join("\n");
|
|
158
|
+
const userImports = emitUserImports(ir);
|
|
159
|
+
const moduleStatements = emitModuleStatements(ir);
|
|
160
|
+
const importsBlock = [importLine, escapeImport, userImports, moduleStatements].filter(Boolean).join("\n");
|
|
161
|
+
const needsSpreadAttributesHelper = components.includes(spreadAttributesHelperName);
|
|
162
|
+
const urlSafeBlock =
|
|
163
|
+
components.includes(urlSafeHelperName) || needsSpreadAttributesHelper
|
|
164
|
+
? `\n\n${urlSafeHelper}`
|
|
165
|
+
: "";
|
|
166
|
+
const clientBoundaryBlock =
|
|
167
|
+
clientBoundaryHelperName === undefined || !components.includes(clientBoundaryHelperName)
|
|
168
|
+
? ""
|
|
169
|
+
: `\n\n${emitClientBoundaryHelper(clientBoundaryHelperName)}`;
|
|
170
|
+
const spreadAttributesBlock = needsSpreadAttributesHelper
|
|
171
|
+
? `\n\n${emitSpreadAttributesHelper(spreadAttributesHelperName, escapeHelperName, urlSafeHelperName)}`
|
|
172
|
+
: "";
|
|
173
|
+
const streamNodeBlock = components.includes(streamNodeHelperName)
|
|
174
|
+
? `\n\n${emitStreamNodeHelper(streamNodeHelperName)}`
|
|
175
|
+
: "";
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
code: `${importsBlock === "" ? "" : `${importsBlock}\n\n`}${helper}${urlSafeBlock}${clientBoundaryBlock}${spreadAttributesBlock}${streamNodeBlock}\n\n${components}\n`,
|
|
179
|
+
imports,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function emitUserImports(ir: ModuleIr): string {
|
|
184
|
+
return ir.components.length === 0 ? "" : ir.userImports.join("\n");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function emitModuleStatements(ir: ModuleIr): string {
|
|
188
|
+
return ir.components.length === 0 ? "" : ir.moduleStatements.join("\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function collectImports(ir: ModuleIr, serverBootstrap: ServerBootstrapMode): RuntimeImport[] {
|
|
192
|
+
const serverSpecifiers = [
|
|
193
|
+
...(hasInOrderAsyncBoundary(ir) ? ["renderAsyncBoundary"] : []),
|
|
194
|
+
...(hasOutOfOrderAsyncBoundary(ir) ? ["renderOutOfOrderBoundary"] : []),
|
|
195
|
+
...(serverBootstrap === "out-of-order-reorder" && hasOutOfOrderAsyncBoundary(ir)
|
|
196
|
+
? ["renderOutOfOrderReorderScript"]
|
|
197
|
+
: []),
|
|
198
|
+
...(hasReactSuspenseBoundary(ir) ? ["renderReactSuspenseBoundary"] : []),
|
|
199
|
+
...(hasReactSuspenseOutOfOrderBoundary(ir) ? ["renderReactSuspenseOutOfOrderBoundary"] : []),
|
|
200
|
+
];
|
|
201
|
+
const imports: RuntimeImport[] = [];
|
|
202
|
+
|
|
203
|
+
if (serverSpecifiers.length > 0) {
|
|
204
|
+
imports.push({
|
|
205
|
+
source: "@reckona/mreact-server",
|
|
206
|
+
specifiers: serverSpecifiers,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (hasCompatComponentReference(ir) || hasReactNodeRender(ir) || hasRawJsxDynamicRender(ir)) {
|
|
211
|
+
imports.push({
|
|
212
|
+
source: "@reckona/mreact-compat",
|
|
213
|
+
specifiers: ["renderToString"],
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return imports;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function hasInOrderAsyncBoundary(ir: ModuleIr): boolean {
|
|
221
|
+
return ir.components.some((component) => containsAsyncBoundary(component.root, false));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function hasOutOfOrderAsyncBoundary(ir: ModuleIr): boolean {
|
|
225
|
+
return ir.components.some((component) => containsAsyncBoundary(component.root, true));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function hasReactSuspenseBoundary(ir: ModuleIr): boolean {
|
|
229
|
+
return ir.components.some((component) => containsReactSuspense(component.root, false));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function hasReactSuspenseOutOfOrderBoundary(ir: ModuleIr): boolean {
|
|
233
|
+
return ir.components.some((component) => containsReactSuspense(component.root, true));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function hasCompatComponentReference(ir: ModuleIr): boolean {
|
|
237
|
+
return ir.components.some((component) => containsCompatComponent(component.root));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function hasReactNodeRender(ir: ModuleIr): boolean {
|
|
241
|
+
return ir.components.some((component) => containsReactNodeRender(component.root));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function hasRawJsxDynamicRender(ir: ModuleIr): boolean {
|
|
245
|
+
return ir.components.some((component) => containsRawJsxDynamicRender(component.root));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function usesClientBoundary(ir: ModuleIr): boolean {
|
|
249
|
+
return ir.components.some((component) => containsClientBoundary(component.root));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function emitClientBoundaryHelper(name: string): string {
|
|
253
|
+
const propsHelperName = `${name}$hasNonSerializableProps`;
|
|
254
|
+
|
|
255
|
+
return [
|
|
256
|
+
`function ${propsHelperName}(value) {`,
|
|
257
|
+
` if (typeof value === "function" || typeof value === "symbol") return true;`,
|
|
258
|
+
` if (value === null || typeof value !== "object") return false;`,
|
|
259
|
+
` if (Array.isArray(value)) return value.some(${propsHelperName});`,
|
|
260
|
+
` for (const key of Object.keys(value)) {`,
|
|
261
|
+
` if (${propsHelperName}(value[key])) return true;`,
|
|
262
|
+
` }`,
|
|
263
|
+
` return false;`,
|
|
264
|
+
`}`,
|
|
265
|
+
`function ${name}(name, props) {`,
|
|
266
|
+
` const _name = String(name);`,
|
|
267
|
+
` const _escapedName = _name.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");`,
|
|
268
|
+
` const _props = props ?? {};`,
|
|
269
|
+
` const _nonSerializable = ${propsHelperName}(_props);`,
|
|
270
|
+
` const _nonSerializableAttr = _nonSerializable ? ' data-mreact-client-boundary-nonserializable="true"' : "";`,
|
|
271
|
+
` const _json = (JSON.stringify(_props) ?? "{}").replaceAll("<", "\\\\u003c");`,
|
|
272
|
+
` return \`<template data-mreact-client-boundary="\${_escapedName}"\${_nonSerializableAttr}></template><script type="application/json" data-mreact-client-boundary-props="\${_escapedName}">\${_json}</script>\`;`,
|
|
273
|
+
`}`,
|
|
274
|
+
].join("\n");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function emitSpreadAttributesHelper(
|
|
278
|
+
name: string,
|
|
279
|
+
escapeHelperName: string,
|
|
280
|
+
urlSafeHelperName: string,
|
|
281
|
+
): string {
|
|
282
|
+
const aliases = JSON.stringify({
|
|
283
|
+
acceptCharset: "accept-charset",
|
|
284
|
+
autoFocus: "autofocus",
|
|
285
|
+
autoPlay: "autoplay",
|
|
286
|
+
charSet: "charset",
|
|
287
|
+
className: "class",
|
|
288
|
+
colSpan: "colspan",
|
|
289
|
+
contentEditable: "contenteditable",
|
|
290
|
+
crossOrigin: "crossorigin",
|
|
291
|
+
encType: "enctype",
|
|
292
|
+
formAction: "formaction",
|
|
293
|
+
frameBorder: "frameborder",
|
|
294
|
+
htmlFor: "for",
|
|
295
|
+
httpEquiv: "http-equiv",
|
|
296
|
+
maxLength: "maxlength",
|
|
297
|
+
minLength: "minlength",
|
|
298
|
+
noValidate: "novalidate",
|
|
299
|
+
playsInline: "playsinline",
|
|
300
|
+
readOnly: "readonly",
|
|
301
|
+
rowSpan: "rowspan",
|
|
302
|
+
spellCheck: "spellcheck",
|
|
303
|
+
srcDoc: "srcdoc",
|
|
304
|
+
srcSet: "srcset",
|
|
305
|
+
tabIndex: "tabindex",
|
|
306
|
+
useMap: "usemap",
|
|
307
|
+
});
|
|
308
|
+
const urlAttributes = JSON.stringify([
|
|
309
|
+
"href",
|
|
310
|
+
"src",
|
|
311
|
+
"action",
|
|
312
|
+
"formaction",
|
|
313
|
+
"xlink:href",
|
|
314
|
+
"ping",
|
|
315
|
+
"poster",
|
|
316
|
+
"background",
|
|
317
|
+
"manifest",
|
|
318
|
+
]);
|
|
319
|
+
const dangerousAttributes = JSON.stringify(["srcdoc"]);
|
|
320
|
+
|
|
321
|
+
return [
|
|
322
|
+
`const ${name}$aliases = ${aliases};`,
|
|
323
|
+
`const ${name}$urlAttributes = new Set(${urlAttributes});`,
|
|
324
|
+
`const ${name}$dangerousAttributes = new Set(${dangerousAttributes});`,
|
|
325
|
+
`function ${name}$style(value) {`,
|
|
326
|
+
` if (value == null || value === false) return "";`,
|
|
327
|
+
` if (typeof value === "string") return value;`,
|
|
328
|
+
` let _style = "";`,
|
|
329
|
+
` for (const _styleName of Object.keys(value)) {`,
|
|
330
|
+
` const _styleValue = value[_styleName];`,
|
|
331
|
+
` if (_styleValue == null || _styleValue === false) continue;`,
|
|
332
|
+
` const _cssName = String(_styleName).startsWith("--") ? String(_styleName) : String(_styleName).replace(/[A-Z]/g, (_char) => "-" + _char.toLowerCase());`,
|
|
333
|
+
` _style += (_style === "" ? "" : ";") + _cssName + ":" + (_styleValue === true ? "" : String(_styleValue));`,
|
|
334
|
+
` }`,
|
|
335
|
+
` return _style;`,
|
|
336
|
+
`}`,
|
|
337
|
+
`function ${name}(tagName, props) {`,
|
|
338
|
+
` if (props == null || props === false) return "";`,
|
|
339
|
+
` let _out = "";`,
|
|
340
|
+
` for (const _rawName of Object.keys(props)) {`,
|
|
341
|
+
` let _value = props[_rawName];`,
|
|
342
|
+
` if (_value == null || _value === false) continue;`,
|
|
343
|
+
` if (_rawName === "key" || _rawName === "ref" || _rawName === "children") continue;`,
|
|
344
|
+
` if (/^on[A-Za-z]/.test(_rawName)) continue;`,
|
|
345
|
+
` let _name = tagName === "input" && _rawName === "defaultValue" ? "value" : tagName === "input" && _rawName === "defaultChecked" ? "checked" : (${name}$aliases[_rawName] ?? _rawName);`,
|
|
346
|
+
` if (!/^[A-Za-z_:][A-Za-z0-9:_.-]*$/.test(_name)) continue;`,
|
|
347
|
+
` if (_name === "style") {`,
|
|
348
|
+
` const _style = ${name}$style(_value);`,
|
|
349
|
+
` if (_style !== "") _out += " style=\\"" + ${escapeHelperName}(_style) + "\\"";`,
|
|
350
|
+
` continue;`,
|
|
351
|
+
` }`,
|
|
352
|
+
` if (${name}$dangerousAttributes.has(_name)) {`,
|
|
353
|
+
` if (typeof _value === "object" && _value !== null && typeof _value.__html === "string") {`,
|
|
354
|
+
` _out += " " + _name + "=\\"" + ${escapeHelperName}(_value.__html) + "\\"";`,
|
|
355
|
+
` }`,
|
|
356
|
+
` continue;`,
|
|
357
|
+
` }`,
|
|
358
|
+
` if (${name}$urlAttributes.has(_name)) {`,
|
|
359
|
+
` _value = ${urlSafeHelperName}(_name, _value === true ? "" : _value);`,
|
|
360
|
+
` if (_value === undefined) continue;`,
|
|
361
|
+
` }`,
|
|
362
|
+
` _out += " " + _name + "=\\"" + ${escapeHelperName}(_value === true ? "" : _value) + "\\"";`,
|
|
363
|
+
` }`,
|
|
364
|
+
` return _out;`,
|
|
365
|
+
`}`,
|
|
366
|
+
].join("\n");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function emitStreamNodeHelper(name: string): string {
|
|
370
|
+
return [
|
|
371
|
+
`async function ${name}($sink, value, escapeHtml) {`,
|
|
372
|
+
` if (value == null || value === false) return;`,
|
|
373
|
+
` if (typeof value === "function") { await value($sink); return; }`,
|
|
374
|
+
` if (Array.isArray(value)) { for (const item of value) await ${name}($sink, item, escapeHtml); return; }`,
|
|
375
|
+
` if (typeof value === "string") { $sink.append(value); return; }`,
|
|
376
|
+
` $sink.append(escapeHtml(value === true ? "" : value));`,
|
|
377
|
+
`}`,
|
|
378
|
+
].join("\n");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function emitComponent(
|
|
382
|
+
component: ComponentIr,
|
|
383
|
+
escapeHelperName: string,
|
|
384
|
+
asyncBoundaryHelperName: string,
|
|
385
|
+
outOfOrderBoundaryHelperName: string,
|
|
386
|
+
reorderScriptHelperName: string,
|
|
387
|
+
reactSuspenseBoundaryHelperName: string,
|
|
388
|
+
reactSuspenseOutOfOrderBoundaryHelperName: string,
|
|
389
|
+
compatRenderToStringHelperName: string,
|
|
390
|
+
options: Required<Pick<EmitServerStreamOptions, "serverBootstrap">> &
|
|
391
|
+
Omit<EmitServerStreamOptions, "serverBootstrap"> & {
|
|
392
|
+
dynamicAttributes: "drop" | "emit";
|
|
393
|
+
escapeBatchHelperName?: string;
|
|
394
|
+
},
|
|
395
|
+
): string {
|
|
396
|
+
const { serverBootstrap, serverBootstrapNonce, serverBootstrapSrc } = options;
|
|
397
|
+
const sinkName = allocateComponentSinkName(component);
|
|
398
|
+
const parameters = [sinkName, ...component.parameters].join(", ");
|
|
399
|
+
const body = component.bodyStatements.map((statement) =>
|
|
400
|
+
` ${statement.replaceAll(
|
|
401
|
+
oxcServerStringReactNodeRenderHelperPlaceholder,
|
|
402
|
+
compatRenderToStringHelperName,
|
|
403
|
+
)}`,
|
|
404
|
+
);
|
|
405
|
+
const markerId = encodeURIComponent(component.name);
|
|
406
|
+
const hydrationStartStatements =
|
|
407
|
+
options.serverHydration === true
|
|
408
|
+
? [` ${sinkName}.append(${stringLiteral(`<!--mreact-h:start:${markerId}-->`)});`]
|
|
409
|
+
: [];
|
|
410
|
+
const appendStatements = emitAppendStatements(
|
|
411
|
+
component.root,
|
|
412
|
+
sinkName,
|
|
413
|
+
escapeHelperName,
|
|
414
|
+
asyncBoundaryHelperName,
|
|
415
|
+
outOfOrderBoundaryHelperName,
|
|
416
|
+
reactSuspenseBoundaryHelperName,
|
|
417
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
418
|
+
compatRenderToStringHelperName,
|
|
419
|
+
options.serverBootstrapNonce,
|
|
420
|
+
options.reactSuspenseRevealScriptSrc,
|
|
421
|
+
options.serverHydration === true,
|
|
422
|
+
options.serverAwaitHydration === true,
|
|
423
|
+
options.dynamicAttributes,
|
|
424
|
+
options.escapeBatchHelperName,
|
|
425
|
+
);
|
|
426
|
+
const bootstrapStatements =
|
|
427
|
+
serverBootstrap === "out-of-order-reorder" && containsAsyncBoundary(component.root, true)
|
|
428
|
+
? [
|
|
429
|
+
` ${reorderScriptHelperName}(${sinkName}${emitBootstrapOptions(serverBootstrapNonce, serverBootstrapSrc)});`,
|
|
430
|
+
]
|
|
431
|
+
: [];
|
|
432
|
+
const hydrationEndStatements =
|
|
433
|
+
options.serverHydration === true
|
|
434
|
+
? [` ${sinkName}.append(${stringLiteral(`<!--mreact-h:end:${markerId}-->`)});`]
|
|
435
|
+
: [];
|
|
436
|
+
const exportPrefix =
|
|
437
|
+
component.exportDefault === true ? "export default " : component.exported === false ? "" : "export ";
|
|
438
|
+
const asyncPrefix = component.async === true || containsAnyAsyncBoundary(component.root) ? "async " : "";
|
|
439
|
+
const functionKeyword = `${exportPrefix}${asyncPrefix}function`;
|
|
440
|
+
|
|
441
|
+
return [
|
|
442
|
+
`${functionKeyword} ${component.name}(${parameters}) {`,
|
|
443
|
+
...body,
|
|
444
|
+
...hydrationStartStatements,
|
|
445
|
+
...appendStatements,
|
|
446
|
+
...hydrationEndStatements,
|
|
447
|
+
...bootstrapStatements,
|
|
448
|
+
`}`,
|
|
449
|
+
].join("\n");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function emitBootstrapOptions(nonce?: string, src?: string): string {
|
|
453
|
+
const entries = [
|
|
454
|
+
...(nonce === undefined ? [] : [`nonce: ${stringLiteral(nonce)}`]),
|
|
455
|
+
...(src === undefined ? [] : [`src: ${stringLiteral(src)}`]),
|
|
456
|
+
];
|
|
457
|
+
|
|
458
|
+
return entries.length === 0 ? "" : `, { ${entries.join(", ")} }`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function emitAppendStatements(
|
|
462
|
+
node: JsxNodeIr,
|
|
463
|
+
sinkName: string,
|
|
464
|
+
escapeHelperName: string,
|
|
465
|
+
asyncBoundaryHelperName: string,
|
|
466
|
+
outOfOrderBoundaryHelperName: string,
|
|
467
|
+
reactSuspenseBoundaryHelperName: string,
|
|
468
|
+
reactSuspenseOutOfOrderBoundaryHelperName: string,
|
|
469
|
+
compatRenderToStringHelperName: string,
|
|
470
|
+
reactSuspenseRevealScriptNonce: string | undefined,
|
|
471
|
+
reactSuspenseRevealScriptSrc: string | undefined,
|
|
472
|
+
hydration: boolean,
|
|
473
|
+
awaitHydration: boolean,
|
|
474
|
+
dynamicAttributes: "drop" | "emit",
|
|
475
|
+
escapeBatchHelperName: string | undefined,
|
|
476
|
+
): string[] {
|
|
477
|
+
if (node.kind === "conditional") {
|
|
478
|
+
const emitBranch = (children: readonly JsxNodeIr[]): string[] =>
|
|
479
|
+
children.flatMap((child) =>
|
|
480
|
+
emitAppendStatements(
|
|
481
|
+
child,
|
|
482
|
+
sinkName,
|
|
483
|
+
escapeHelperName,
|
|
484
|
+
asyncBoundaryHelperName,
|
|
485
|
+
outOfOrderBoundaryHelperName,
|
|
486
|
+
reactSuspenseBoundaryHelperName,
|
|
487
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
488
|
+
compatRenderToStringHelperName,
|
|
489
|
+
reactSuspenseRevealScriptNonce,
|
|
490
|
+
reactSuspenseRevealScriptSrc,
|
|
491
|
+
hydration,
|
|
492
|
+
awaitHydration,
|
|
493
|
+
dynamicAttributes,
|
|
494
|
+
escapeBatchHelperName,
|
|
495
|
+
),
|
|
496
|
+
);
|
|
497
|
+
const indentBranch = (line: string) => ` ${line}`;
|
|
498
|
+
const whenTrue = emitBranch(node.whenTrue).map(indentBranch);
|
|
499
|
+
const whenFalse = emitBranch(node.whenFalse).map(indentBranch);
|
|
500
|
+
|
|
501
|
+
if (whenTrue.length === 0 && whenFalse.length === 0) {
|
|
502
|
+
return [];
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (whenFalse.length === 0) {
|
|
506
|
+
return [` if (${node.conditionCode}) {`, ...whenTrue, ` }`];
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return [` if (${node.conditionCode}) {`, ...whenTrue, ` } else {`, ...whenFalse, ` }`];
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const collectState: CollectHtmlState = {
|
|
513
|
+
dynamicAttributes,
|
|
514
|
+
...(escapeBatchHelperName === undefined ? {} : { escapeBatchHelperName }),
|
|
515
|
+
hydration,
|
|
516
|
+
awaitHydration,
|
|
517
|
+
nextFragmentId: 0,
|
|
518
|
+
...(reactSuspenseRevealScriptNonce === undefined ? {} : { reactSuspenseRevealScriptNonce }),
|
|
519
|
+
...(reactSuspenseRevealScriptSrc === undefined ? {} : { reactSuspenseRevealScriptSrc }),
|
|
520
|
+
};
|
|
521
|
+
const collected = collectHtmlParts(
|
|
522
|
+
node,
|
|
523
|
+
escapeHelperName,
|
|
524
|
+
asyncBoundaryHelperName,
|
|
525
|
+
outOfOrderBoundaryHelperName,
|
|
526
|
+
reactSuspenseBoundaryHelperName,
|
|
527
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
528
|
+
collectState,
|
|
529
|
+
);
|
|
530
|
+
const previousPropChildrenCollectState = currentPropChildrenCollectState;
|
|
531
|
+
currentPropChildrenCollectState = collectState;
|
|
532
|
+
try {
|
|
533
|
+
return coalesceAdjacentStaticParts(collected).map((part) => {
|
|
534
|
+
if (part.kind === "async-boundary") {
|
|
535
|
+
return emitAsyncBoundary(
|
|
536
|
+
part,
|
|
537
|
+
sinkName,
|
|
538
|
+
asyncBoundaryHelperName,
|
|
539
|
+
compatRenderToStringHelperName,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (part.kind === "out-of-order-boundary") {
|
|
544
|
+
return emitOutOfOrderBoundary(
|
|
545
|
+
part,
|
|
546
|
+
sinkName,
|
|
547
|
+
outOfOrderBoundaryHelperName,
|
|
548
|
+
compatRenderToStringHelperName,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (part.kind === "react-suspense-boundary") {
|
|
553
|
+
return emitReactSuspenseBoundary(
|
|
554
|
+
part,
|
|
555
|
+
sinkName,
|
|
556
|
+
reactSuspenseBoundaryHelperName,
|
|
557
|
+
compatRenderToStringHelperName,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (part.kind === "react-suspense-out-of-order-boundary") {
|
|
562
|
+
return emitReactSuspenseOutOfOrderBoundary(
|
|
563
|
+
part,
|
|
564
|
+
sinkName,
|
|
565
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
566
|
+
compatRenderToStringHelperName,
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (part.kind === "component") {
|
|
571
|
+
if (part.runtime === "compat") {
|
|
572
|
+
return emitCompatComponentAppendStatements(
|
|
573
|
+
part,
|
|
574
|
+
sinkName,
|
|
575
|
+
compatRenderToStringHelperName,
|
|
576
|
+
" ",
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return ` await ${part.name}(${sinkName}, ${emitPropsObject(part.props, part.children, part.escapeHelperName)});`;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (part.kind === "react-node") {
|
|
584
|
+
return ` ${sinkName}.append(${compatRenderToStringHelperName}(() => (${part.code})));`;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (part.kind === "stream-node") {
|
|
588
|
+
return ` await ${currentStreamNodeHelperName}(${sinkName}, (${part.code}), ${part.escapeHelperName});`;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (part.kind === "list") {
|
|
592
|
+
return emitListPart(part, sinkName, compatRenderToStringHelperName, " ");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (part.kind === "dynamic" && looksLikeRawJsxExpression(part.code)) {
|
|
596
|
+
return emitDynamicHtmlAppendStatement(
|
|
597
|
+
part.code,
|
|
598
|
+
sinkName,
|
|
599
|
+
escapeHelperName,
|
|
600
|
+
compatRenderToStringHelperName,
|
|
601
|
+
" ",
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const expression =
|
|
606
|
+
part.kind === "static"
|
|
607
|
+
? stringLiteral(part.value)
|
|
608
|
+
: part.kind === "dynamic"
|
|
609
|
+
? `${escapeHelperName}(${part.code})`
|
|
610
|
+
: part.code;
|
|
611
|
+
|
|
612
|
+
return ` ${sinkName}.append(${expression});`;
|
|
613
|
+
});
|
|
614
|
+
} finally {
|
|
615
|
+
currentPropChildrenCollectState = previousPropChildrenCollectState;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function isHtmlSyncPart(part: HtmlPart): part is HtmlSyncPart {
|
|
620
|
+
return (
|
|
621
|
+
part.kind !== "async-boundary" &&
|
|
622
|
+
part.kind !== "out-of-order-boundary" &&
|
|
623
|
+
part.kind !== "react-suspense-boundary" &&
|
|
624
|
+
part.kind !== "react-suspense-out-of-order-boundary"
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Issue 085: collapse runs of adjacent `static` parts into a single
|
|
629
|
+
// `static` part. Each part becomes one `sink.append(...)` call at emit
|
|
630
|
+
// time and `sink.append` goes through 2-3 function frames, so merging
|
|
631
|
+
// `["<span", ">"]` into `"<span>"` halves the per-iteration call count
|
|
632
|
+
// for tag-heavy lists.
|
|
633
|
+
//
|
|
634
|
+
// Only adjacent static-kind parts are merged; dynamic / boundary /
|
|
635
|
+
// component / list / react-node parts stay where they are.
|
|
636
|
+
function coalesceAdjacentStaticParts<T extends HtmlPart>(parts: T[]): T[] {
|
|
637
|
+
if (parts.length < 2) return parts;
|
|
638
|
+
const result: T[] = [];
|
|
639
|
+
let pending: { kind: "static"; value: string } | undefined;
|
|
640
|
+
for (const part of parts) {
|
|
641
|
+
if (part.kind === "static") {
|
|
642
|
+
pending =
|
|
643
|
+
pending === undefined
|
|
644
|
+
? { kind: "static", value: part.value }
|
|
645
|
+
: { kind: "static", value: pending.value + part.value };
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (pending !== undefined) {
|
|
649
|
+
result.push(pending as T);
|
|
650
|
+
pending = undefined;
|
|
651
|
+
}
|
|
652
|
+
result.push(part);
|
|
653
|
+
}
|
|
654
|
+
if (pending !== undefined) {
|
|
655
|
+
result.push(pending as T);
|
|
656
|
+
}
|
|
657
|
+
return result;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function emitSyncPartAsAppendStatement(
|
|
661
|
+
part: HtmlSyncPart,
|
|
662
|
+
sinkName: string,
|
|
663
|
+
compatRenderToStringHelperName: string,
|
|
664
|
+
indent: string,
|
|
665
|
+
): string {
|
|
666
|
+
if (part.kind === "component") {
|
|
667
|
+
if (part.runtime === "compat") {
|
|
668
|
+
return emitCompatComponentAppendStatements(
|
|
669
|
+
part,
|
|
670
|
+
sinkName,
|
|
671
|
+
compatRenderToStringHelperName,
|
|
672
|
+
indent,
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return `${indent}await ${part.name}(${sinkName}, ${emitPropsObject(part.props, part.children, part.escapeHelperName)});`;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (part.kind === "react-node") {
|
|
680
|
+
return `${indent}${sinkName}.append(${compatRenderToStringHelperName}(() => (${part.code})));`;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (part.kind === "stream-node") {
|
|
684
|
+
return `${indent}await ${currentStreamNodeHelperName}(${sinkName}, (${part.code}), ${part.escapeHelperName});`;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (part.kind === "list") {
|
|
688
|
+
return emitListPart(part, sinkName, compatRenderToStringHelperName, indent);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (part.kind === "dynamic" && looksLikeRawJsxExpression(part.code)) {
|
|
692
|
+
return emitDynamicHtmlAppendStatement(
|
|
693
|
+
part.code,
|
|
694
|
+
sinkName,
|
|
695
|
+
part.escapeHelperName,
|
|
696
|
+
compatRenderToStringHelperName,
|
|
697
|
+
indent,
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const expression =
|
|
702
|
+
part.kind === "static"
|
|
703
|
+
? stringLiteral(part.value)
|
|
704
|
+
: part.kind === "dynamic"
|
|
705
|
+
? `${part.escapeHelperName}(${part.code})`
|
|
706
|
+
: part.code;
|
|
707
|
+
|
|
708
|
+
return `${indent}${sinkName}.append(${expression});`;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function emitListPart(
|
|
712
|
+
part: Extract<HtmlPart, { kind: "list" }>,
|
|
713
|
+
sinkName: string,
|
|
714
|
+
compatRenderToStringHelperName: string,
|
|
715
|
+
indent: string,
|
|
716
|
+
): string {
|
|
717
|
+
const innerIndent = indent + " ";
|
|
718
|
+
const itemBinding = `${innerIndent}const ${part.itemName} = _arr[_i];`;
|
|
719
|
+
const indexBinding =
|
|
720
|
+
part.indexName === undefined ? undefined : `${innerIndent}const ${part.indexName} = _i;`;
|
|
721
|
+
const bodyLines = part.bodyStatements.map(
|
|
722
|
+
(statement) => `${innerIndent}${statement}`,
|
|
723
|
+
);
|
|
724
|
+
const coalescedParts = coalesceAdjacentStaticParts(part.parts);
|
|
725
|
+
|
|
726
|
+
// Issue 085 follow-up: if every child part can be expressed as a
|
|
727
|
+
// pure string expression (no `sink.append`/`await` required), build
|
|
728
|
+
// up a local ConsString accumulator and emit a single
|
|
729
|
+
// `sink.append(_listOut)` at the end of the iteration. This matches
|
|
730
|
+
// the string backend's `_out +=` pattern, which V8 turns into a
|
|
731
|
+
// shallow cons-string tree (~3 ns per append). Otherwise (the list
|
|
732
|
+
// contains components / nested lists with components / etc) fall
|
|
733
|
+
// back to per-part `sink.append` inside the loop.
|
|
734
|
+
const syncCoalescedParts = coalescedParts.every(isHtmlSyncPart) ? coalescedParts : undefined;
|
|
735
|
+
const stringExpressions =
|
|
736
|
+
syncCoalescedParts?.map((child) =>
|
|
737
|
+
tryEmitPartAsStringExpression(child, compatRenderToStringHelperName),
|
|
738
|
+
) ?? [];
|
|
739
|
+
const allStringSafe =
|
|
740
|
+
syncCoalescedParts !== undefined && stringExpressions.every((expr) => expr !== undefined);
|
|
741
|
+
|
|
742
|
+
if (allStringSafe) {
|
|
743
|
+
const accumulatorName = "_listOut";
|
|
744
|
+
const concatLines = stringExpressions.map(
|
|
745
|
+
(expr) => `${innerIndent}${accumulatorName} += ${expr};`,
|
|
746
|
+
);
|
|
747
|
+
return [
|
|
748
|
+
`${indent}{`,
|
|
749
|
+
`${indent} const _arr = (${part.itemsCode});`,
|
|
750
|
+
`${indent} let ${accumulatorName} = "";`,
|
|
751
|
+
`${indent} for (let _i = 0, _len = _arr.length; _i < _len; _i++) {`,
|
|
752
|
+
itemBinding,
|
|
753
|
+
...(indexBinding === undefined ? [] : [indexBinding]),
|
|
754
|
+
...bodyLines,
|
|
755
|
+
...concatLines,
|
|
756
|
+
`${indent} }`,
|
|
757
|
+
`${indent} ${sinkName}.append(${accumulatorName});`,
|
|
758
|
+
`${indent}}`,
|
|
759
|
+
].join("\n");
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const childLines =
|
|
763
|
+
syncCoalescedParts === undefined
|
|
764
|
+
? [
|
|
765
|
+
emitNestedStreamAppendStatements(
|
|
766
|
+
coalescedParts,
|
|
767
|
+
sinkName,
|
|
768
|
+
compatRenderToStringHelperName,
|
|
769
|
+
),
|
|
770
|
+
]
|
|
771
|
+
: syncCoalescedParts.map((child) =>
|
|
772
|
+
emitSyncPartAsAppendStatement(
|
|
773
|
+
child,
|
|
774
|
+
sinkName,
|
|
775
|
+
compatRenderToStringHelperName,
|
|
776
|
+
innerIndent,
|
|
777
|
+
),
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
return [
|
|
781
|
+
`${indent}{`,
|
|
782
|
+
`${indent} const _arr = (${part.itemsCode});`,
|
|
783
|
+
`${indent} for (let _i = 0, _len = _arr.length; _i < _len; _i++) {`,
|
|
784
|
+
itemBinding,
|
|
785
|
+
...(indexBinding === undefined ? [] : [indexBinding]),
|
|
786
|
+
...bodyLines,
|
|
787
|
+
...childLines,
|
|
788
|
+
`${indent} }`,
|
|
789
|
+
`${indent}}`,
|
|
790
|
+
].join("\n");
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Returns a string-typed expression for `part` if it can be evaluated
|
|
794
|
+
// synchronously without writing to the sink, otherwise undefined.
|
|
795
|
+
// Used by `emitListPart` to choose between the cons-string accumulator
|
|
796
|
+
// path and the per-part `sink.append` path.
|
|
797
|
+
function tryEmitPartAsStringExpression(
|
|
798
|
+
part: HtmlSyncPart,
|
|
799
|
+
compatRenderToStringHelperName: string,
|
|
800
|
+
): string | undefined {
|
|
801
|
+
if (part.kind === "static") return stringLiteral(part.value);
|
|
802
|
+
if (part.kind === "dynamic") {
|
|
803
|
+
return looksLikeRawJsxExpression(part.code)
|
|
804
|
+
? undefined
|
|
805
|
+
: `${part.escapeHelperName}(${part.code})`;
|
|
806
|
+
}
|
|
807
|
+
if (part.kind === "raw-dynamic") return `(${part.code})`;
|
|
808
|
+
if (part.kind === "react-node") {
|
|
809
|
+
return `${compatRenderToStringHelperName}(() => (${part.code}))`;
|
|
810
|
+
}
|
|
811
|
+
if (part.kind === "stream-node") {
|
|
812
|
+
return undefined;
|
|
813
|
+
}
|
|
814
|
+
if (part.kind === "list" && part.parts.every(isHtmlSyncPart)) {
|
|
815
|
+
return emitListPartAsStringExpression(part, compatRenderToStringHelperName);
|
|
816
|
+
}
|
|
817
|
+
if (part.kind === "component" && part.runtime === "compat") {
|
|
818
|
+
const rendered = `${compatRenderToStringHelperName}(${part.name}, ${emitPropsObject(part.props, part.children, part.escapeHelperName)})`;
|
|
819
|
+
if (part.hydrationId === undefined) {
|
|
820
|
+
return rendered;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
return `${stringLiteral(`<!--mreact-h:start:${encodeURIComponent(part.hydrationId)}-->`)} + ${rendered} + ${stringLiteral(`<!--mreact-h:end:${encodeURIComponent(part.hydrationId)}-->`)}`;
|
|
824
|
+
}
|
|
825
|
+
// Non-compat component parts require `await sink-write`; lists with
|
|
826
|
+
// sink-needing children also can't collapse. Signal fallback.
|
|
827
|
+
return undefined;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function emitListPartAsStringExpression(
|
|
831
|
+
part: Extract<HtmlPart, { kind: "list" }>,
|
|
832
|
+
compatRenderToStringHelperName: string,
|
|
833
|
+
): string | undefined {
|
|
834
|
+
const coalescedParts = coalesceAdjacentStaticParts(part.parts);
|
|
835
|
+
if (!coalescedParts.every(isHtmlSyncPart)) {
|
|
836
|
+
return undefined;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const stringExpressions = coalescedParts.map((child) =>
|
|
840
|
+
tryEmitPartAsStringExpression(child, compatRenderToStringHelperName),
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
if (stringExpressions.some((expr) => expr === undefined)) {
|
|
844
|
+
return undefined;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const concatLines = stringExpressions.map((expr) => `_listOut += ${expr};`);
|
|
848
|
+
return `(() => { const _arr = (${part.itemsCode}); let _listOut = ""; for (let _i = 0, _len = _arr.length; _i < _len; _i++) { const ${part.itemName} = _arr[_i];${part.indexName === undefined ? "" : ` const ${part.indexName} = _i;`}${part.bodyStatements.length === 0 ? "" : ` ${part.bodyStatements.join(" ")}`} ${concatLines.join(" ")} } return _listOut; })()`;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function emitAsyncBoundary(
|
|
852
|
+
part: Extract<HtmlPart, { kind: "async-boundary" }>,
|
|
853
|
+
sinkName: string,
|
|
854
|
+
asyncBoundaryHelperName: string,
|
|
855
|
+
compatRenderToStringHelperName: string,
|
|
856
|
+
): string {
|
|
857
|
+
const optionFields: string[] = [];
|
|
858
|
+
|
|
859
|
+
if (part.catchName !== undefined && part.catchParts !== undefined) {
|
|
860
|
+
optionFields.push(
|
|
861
|
+
`catch: (${sinkName}, ${part.catchName}) => {\n${emitNestedAppendStatements(part.catchParts, sinkName, compatRenderToStringHelperName)}\n }`,
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (part.awaitId !== undefined) {
|
|
866
|
+
optionFields.push(`hydrationAwaitId: ${JSON.stringify(part.awaitId)}`);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const optionsExpression = optionFields.length === 0
|
|
870
|
+
? ""
|
|
871
|
+
: `, { ${optionFields.join(", ")} }`;
|
|
872
|
+
|
|
873
|
+
return [
|
|
874
|
+
` await ${asyncBoundaryHelperName}(${sinkName}, (${part.valueCode}), async (${sinkName}, ${part.valueName}) => {`,
|
|
875
|
+
emitNestedAppendStatements(part.parts, sinkName, compatRenderToStringHelperName),
|
|
876
|
+
` }${optionsExpression});`,
|
|
877
|
+
].join("\n");
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function emitOutOfOrderBoundary(
|
|
881
|
+
part: Extract<HtmlPart, { kind: "out-of-order-boundary" }>,
|
|
882
|
+
sinkName: string,
|
|
883
|
+
outOfOrderBoundaryHelperName: string,
|
|
884
|
+
compatRenderToStringHelperName: string,
|
|
885
|
+
): string {
|
|
886
|
+
const catchOption =
|
|
887
|
+
part.catchName === undefined || part.catchParts === undefined
|
|
888
|
+
? ""
|
|
889
|
+
: `,\n catch: (${sinkName}, ${part.catchName}) => {\n${emitNestedAppendStatements(part.catchParts, sinkName, compatRenderToStringHelperName)}\n }`;
|
|
890
|
+
|
|
891
|
+
const hydrationAwaitIdOption =
|
|
892
|
+
part.awaitId === undefined
|
|
893
|
+
? ""
|
|
894
|
+
: `,\n hydrationAwaitId: ${JSON.stringify(part.awaitId)}`;
|
|
895
|
+
const placeholderTagOption =
|
|
896
|
+
part.placeholderTagCode === undefined
|
|
897
|
+
? ""
|
|
898
|
+
: `,\n placeholderTag: (${part.placeholderTagCode})`;
|
|
899
|
+
|
|
900
|
+
return [
|
|
901
|
+
` ${outOfOrderBoundaryHelperName}(${sinkName}, ${JSON.stringify(part.id)}, (${part.valueCode}), async (${sinkName}, ${part.valueName}) => {`,
|
|
902
|
+
emitNestedAppendStatements(part.parts, sinkName, compatRenderToStringHelperName),
|
|
903
|
+
` }, {`,
|
|
904
|
+
...(part.hydration ? [` hydration: true,`] : []),
|
|
905
|
+
` placeholder: (${sinkName}) => {`,
|
|
906
|
+
emitNestedAppendStatements(part.placeholderParts, sinkName, compatRenderToStringHelperName),
|
|
907
|
+
` }${catchOption}${hydrationAwaitIdOption}${placeholderTagOption}`,
|
|
908
|
+
` });`,
|
|
909
|
+
].join("\n");
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function emitReactSuspenseBoundary(
|
|
913
|
+
part: Extract<HtmlPart, { kind: "react-suspense-boundary" }>,
|
|
914
|
+
sinkName: string,
|
|
915
|
+
reactSuspenseBoundaryHelperName: string,
|
|
916
|
+
compatRenderToStringHelperName: string,
|
|
917
|
+
): string {
|
|
918
|
+
return [
|
|
919
|
+
` await ${reactSuspenseBoundaryHelperName}(${sinkName}, async (${sinkName}) => {`,
|
|
920
|
+
emitNestedAppendStatements(part.parts, sinkName, compatRenderToStringHelperName),
|
|
921
|
+
` });`,
|
|
922
|
+
].join("\n");
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function emitReactSuspenseOutOfOrderBoundary(
|
|
926
|
+
part: Extract<HtmlPart, { kind: "react-suspense-out-of-order-boundary" }>,
|
|
927
|
+
sinkName: string,
|
|
928
|
+
reactSuspenseOutOfOrderBoundaryHelperName: string,
|
|
929
|
+
compatRenderToStringHelperName: string,
|
|
930
|
+
): string {
|
|
931
|
+
const options = [
|
|
932
|
+
` fallback: (${sinkName}) => {`,
|
|
933
|
+
emitNestedAppendStatements(part.fallbackParts, sinkName, compatRenderToStringHelperName),
|
|
934
|
+
` },`,
|
|
935
|
+
...(part.catchName === undefined || part.catchParts === undefined
|
|
936
|
+
? []
|
|
937
|
+
: [
|
|
938
|
+
` catch: (${sinkName}, ${part.catchName}) => {`,
|
|
939
|
+
emitNestedAppendStatements(part.catchParts, sinkName, compatRenderToStringHelperName),
|
|
940
|
+
` },`,
|
|
941
|
+
]),
|
|
942
|
+
...(part.nonce === undefined ? [] : [` nonce: ${stringLiteral(part.nonce)},`]),
|
|
943
|
+
...(part.scriptSrc === undefined ? [] : [` src: ${stringLiteral(part.scriptSrc)},`]),
|
|
944
|
+
];
|
|
945
|
+
|
|
946
|
+
return [
|
|
947
|
+
` ${reactSuspenseOutOfOrderBoundaryHelperName}(${sinkName}, ${JSON.stringify(part.boundaryId)}, ${JSON.stringify(part.segmentId)}, (${part.valueCode}), async (${sinkName}, ${part.valueName}) => {`,
|
|
948
|
+
emitNestedAppendStatements(part.parts, sinkName, compatRenderToStringHelperName),
|
|
949
|
+
` }, {`,
|
|
950
|
+
...options,
|
|
951
|
+
` });`,
|
|
952
|
+
].join("\n");
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function emitNestedAppendStatements(
|
|
956
|
+
parts: HtmlSyncPart[],
|
|
957
|
+
sinkName: string,
|
|
958
|
+
compatRenderToStringHelperName: string,
|
|
959
|
+
): string {
|
|
960
|
+
return coalesceAdjacentStaticParts(parts)
|
|
961
|
+
.map((part) => emitSyncPartAsAppendStatement(part, sinkName, compatRenderToStringHelperName, " "))
|
|
962
|
+
.join("\n");
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function emitNestedStreamAppendStatements(
|
|
966
|
+
parts: HtmlPart[],
|
|
967
|
+
sinkName: string,
|
|
968
|
+
compatRenderToStringHelperName: string,
|
|
969
|
+
): string {
|
|
970
|
+
return coalesceAdjacentStaticParts(parts)
|
|
971
|
+
.map((part) => {
|
|
972
|
+
if (part.kind === "async-boundary") {
|
|
973
|
+
return emitAsyncBoundary(
|
|
974
|
+
part,
|
|
975
|
+
sinkName,
|
|
976
|
+
currentAsyncBoundaryHelperName,
|
|
977
|
+
compatRenderToStringHelperName,
|
|
978
|
+
).replace(/^/gm, " ");
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (part.kind === "out-of-order-boundary") {
|
|
982
|
+
return emitOutOfOrderBoundary(
|
|
983
|
+
part,
|
|
984
|
+
sinkName,
|
|
985
|
+
currentOutOfOrderBoundaryHelperName,
|
|
986
|
+
compatRenderToStringHelperName,
|
|
987
|
+
).replace(/^/gm, " ");
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (part.kind === "react-suspense-boundary") {
|
|
991
|
+
return emitReactSuspenseBoundary(
|
|
992
|
+
part,
|
|
993
|
+
sinkName,
|
|
994
|
+
currentReactSuspenseBoundaryHelperName,
|
|
995
|
+
compatRenderToStringHelperName,
|
|
996
|
+
).replace(/^/gm, " ");
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (part.kind === "react-suspense-out-of-order-boundary") {
|
|
1000
|
+
return emitReactSuspenseOutOfOrderBoundary(
|
|
1001
|
+
part,
|
|
1002
|
+
sinkName,
|
|
1003
|
+
currentReactSuspenseOutOfOrderBoundaryHelperName,
|
|
1004
|
+
compatRenderToStringHelperName,
|
|
1005
|
+
).replace(/^/gm, " ");
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
return emitSyncPartAsAppendStatement(
|
|
1009
|
+
part,
|
|
1010
|
+
sinkName,
|
|
1011
|
+
compatRenderToStringHelperName,
|
|
1012
|
+
" ",
|
|
1013
|
+
);
|
|
1014
|
+
})
|
|
1015
|
+
.join("\n");
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function emitCompatComponentAppendStatements(
|
|
1019
|
+
part: Extract<HtmlPart, { kind: "component" }>,
|
|
1020
|
+
sinkName: string,
|
|
1021
|
+
compatRenderToStringHelperName: string,
|
|
1022
|
+
indent: string,
|
|
1023
|
+
): string {
|
|
1024
|
+
const rendered = `${compatRenderToStringHelperName}(${part.name}, ${emitPropsObject(part.props, part.children, part.escapeHelperName)})`;
|
|
1025
|
+
const statements =
|
|
1026
|
+
part.hydrationId === undefined
|
|
1027
|
+
? [`${sinkName}.append(${rendered});`]
|
|
1028
|
+
: [
|
|
1029
|
+
`${sinkName}.append(${stringLiteral(`<!--mreact-h:start:${encodeURIComponent(part.hydrationId)}-->`)});`,
|
|
1030
|
+
`${sinkName}.append(${rendered});`,
|
|
1031
|
+
`${sinkName}.append(${stringLiteral(`<!--mreact-h:end:${encodeURIComponent(part.hydrationId)}-->`)});`,
|
|
1032
|
+
];
|
|
1033
|
+
|
|
1034
|
+
return statements.map((statement) => `${indent}${statement}`).join("\n");
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
type HtmlPart =
|
|
1038
|
+
| {
|
|
1039
|
+
kind: "static";
|
|
1040
|
+
value: string;
|
|
1041
|
+
}
|
|
1042
|
+
| {
|
|
1043
|
+
kind: "dynamic";
|
|
1044
|
+
code: string;
|
|
1045
|
+
escapeHelperName: string;
|
|
1046
|
+
}
|
|
1047
|
+
| {
|
|
1048
|
+
kind: "raw-dynamic";
|
|
1049
|
+
code: string;
|
|
1050
|
+
}
|
|
1051
|
+
| {
|
|
1052
|
+
kind: "react-node";
|
|
1053
|
+
code: string;
|
|
1054
|
+
}
|
|
1055
|
+
| {
|
|
1056
|
+
kind: "stream-node";
|
|
1057
|
+
code: string;
|
|
1058
|
+
escapeHelperName: string;
|
|
1059
|
+
}
|
|
1060
|
+
| {
|
|
1061
|
+
kind: "async-boundary";
|
|
1062
|
+
valueCode: string;
|
|
1063
|
+
valueName: string;
|
|
1064
|
+
parts: HtmlSyncPart[];
|
|
1065
|
+
catchName?: string;
|
|
1066
|
+
catchParts?: HtmlSyncPart[];
|
|
1067
|
+
awaitId?: string;
|
|
1068
|
+
}
|
|
1069
|
+
| {
|
|
1070
|
+
kind: "out-of-order-boundary";
|
|
1071
|
+
id: string;
|
|
1072
|
+
hydration: boolean;
|
|
1073
|
+
valueCode: string;
|
|
1074
|
+
valueName: string;
|
|
1075
|
+
parts: HtmlSyncPart[];
|
|
1076
|
+
placeholderParts: HtmlSyncPart[];
|
|
1077
|
+
placeholderTagCode?: string;
|
|
1078
|
+
catchName?: string;
|
|
1079
|
+
catchParts?: HtmlSyncPart[];
|
|
1080
|
+
awaitId?: string;
|
|
1081
|
+
}
|
|
1082
|
+
| {
|
|
1083
|
+
kind: "react-suspense-boundary";
|
|
1084
|
+
parts: HtmlSyncPart[];
|
|
1085
|
+
}
|
|
1086
|
+
| {
|
|
1087
|
+
kind: "react-suspense-out-of-order-boundary";
|
|
1088
|
+
boundaryId: string;
|
|
1089
|
+
segmentId: string;
|
|
1090
|
+
valueCode: string;
|
|
1091
|
+
valueName: string;
|
|
1092
|
+
parts: HtmlSyncPart[];
|
|
1093
|
+
fallbackParts: HtmlSyncPart[];
|
|
1094
|
+
catchName?: string;
|
|
1095
|
+
catchParts?: HtmlSyncPart[];
|
|
1096
|
+
nonce?: string;
|
|
1097
|
+
scriptSrc?: string;
|
|
1098
|
+
}
|
|
1099
|
+
| {
|
|
1100
|
+
kind: "component";
|
|
1101
|
+
name: string;
|
|
1102
|
+
runtime?: "compat";
|
|
1103
|
+
async?: boolean;
|
|
1104
|
+
hydrationId?: string;
|
|
1105
|
+
props: ComponentPropIr[];
|
|
1106
|
+
children: JsxNodeIr[];
|
|
1107
|
+
escapeHelperName: string;
|
|
1108
|
+
}
|
|
1109
|
+
| {
|
|
1110
|
+
// Issue 085: list direct streaming. The list iterates
|
|
1111
|
+
// `itemsCode`, runs `bodyStatements` and then emits each inner
|
|
1112
|
+
// part per iteration. Sync-only lists still use the string
|
|
1113
|
+
// accumulator fast path; lists that contain async/oob/Suspense
|
|
1114
|
+
// boundaries keep those boundary parts visible to the stream
|
|
1115
|
+
// emitter instead of falling back to a raw `.map().join("")`.
|
|
1116
|
+
kind: "list";
|
|
1117
|
+
itemsCode: string;
|
|
1118
|
+
itemName: string;
|
|
1119
|
+
indexName?: string;
|
|
1120
|
+
bodyStatements: string[];
|
|
1121
|
+
parts: HtmlPart[];
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
type HtmlSyncPart = Exclude<
|
|
1125
|
+
HtmlPart,
|
|
1126
|
+
{
|
|
1127
|
+
kind:
|
|
1128
|
+
| "async-boundary"
|
|
1129
|
+
| "out-of-order-boundary"
|
|
1130
|
+
| "react-suspense-boundary"
|
|
1131
|
+
| "react-suspense-out-of-order-boundary";
|
|
1132
|
+
}
|
|
1133
|
+
>;
|
|
1134
|
+
|
|
1135
|
+
interface CollectHtmlState {
|
|
1136
|
+
dynamicAttributes: "drop" | "emit";
|
|
1137
|
+
escapeBatchHelperName?: string;
|
|
1138
|
+
hydration: boolean;
|
|
1139
|
+
awaitHydration: boolean;
|
|
1140
|
+
nextFragmentId: number;
|
|
1141
|
+
reactSuspenseRevealScriptNonce?: string;
|
|
1142
|
+
reactSuspenseRevealScriptSrc?: string;
|
|
1143
|
+
selectedValueCode?: string;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function collectHtmlParts(
|
|
1147
|
+
node: JsxNodeIr,
|
|
1148
|
+
escapeHelperName: string,
|
|
1149
|
+
asyncBoundaryHelperName: string,
|
|
1150
|
+
outOfOrderBoundaryHelperName: string,
|
|
1151
|
+
reactSuspenseBoundaryHelperName: string,
|
|
1152
|
+
reactSuspenseOutOfOrderBoundaryHelperName: string,
|
|
1153
|
+
state: CollectHtmlState,
|
|
1154
|
+
): HtmlPart[] {
|
|
1155
|
+
void asyncBoundaryHelperName;
|
|
1156
|
+
void outOfOrderBoundaryHelperName;
|
|
1157
|
+
void reactSuspenseBoundaryHelperName;
|
|
1158
|
+
void reactSuspenseOutOfOrderBoundaryHelperName;
|
|
1159
|
+
|
|
1160
|
+
if (node.kind === "text") {
|
|
1161
|
+
return [{ kind: "static", value: escapeHtml(node.value) }];
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
if (node.kind === "expr") {
|
|
1165
|
+
if (node.renderMode === "html" && isChildrenExpressionCode(node.code)) {
|
|
1166
|
+
return [{ kind: "stream-node", code: node.code, escapeHelperName }];
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (node.renderMode === "html") {
|
|
1170
|
+
return [{ kind: "raw-dynamic", code: rawHtmlExpression(node.code) }];
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
if (node.renderMode === "react-node") {
|
|
1174
|
+
return [{ kind: "react-node", code: node.code }];
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (node.renderMode === "stream-node") {
|
|
1178
|
+
return [{ kind: "stream-node", code: node.code, escapeHelperName }];
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
return [{ kind: "dynamic", code: node.code, escapeHelperName }];
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
if (node.kind === "conditional") {
|
|
1185
|
+
return [
|
|
1186
|
+
{
|
|
1187
|
+
kind: "raw-dynamic",
|
|
1188
|
+
code: `((${node.conditionCode}) ? ${emitHtmlExpressionFromChildren(node.whenTrue, escapeHelperName)} : ${emitHtmlExpressionFromChildren(node.whenFalse, escapeHelperName)})`,
|
|
1189
|
+
},
|
|
1190
|
+
];
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
if (node.kind === "list") {
|
|
1194
|
+
// Keep mapped children in the stream emitter so direct `<Await>`
|
|
1195
|
+
// boundaries inside list renderers stay visible to out-of-order
|
|
1196
|
+
// lowering.
|
|
1197
|
+
const collectedChildParts: HtmlPart[] = node.children.flatMap((child) =>
|
|
1198
|
+
collectHtmlParts(
|
|
1199
|
+
child,
|
|
1200
|
+
escapeHelperName,
|
|
1201
|
+
asyncBoundaryHelperName,
|
|
1202
|
+
outOfOrderBoundaryHelperName,
|
|
1203
|
+
reactSuspenseBoundaryHelperName,
|
|
1204
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
1205
|
+
state,
|
|
1206
|
+
),
|
|
1207
|
+
);
|
|
1208
|
+
|
|
1209
|
+
return [
|
|
1210
|
+
{
|
|
1211
|
+
kind: "list",
|
|
1212
|
+
itemsCode: node.itemsCode,
|
|
1213
|
+
itemName: node.itemName,
|
|
1214
|
+
...(node.indexName === undefined ? {} : { indexName: node.indexName }),
|
|
1215
|
+
bodyStatements: node.bodyStatements ?? [],
|
|
1216
|
+
parts: collectedChildParts,
|
|
1217
|
+
},
|
|
1218
|
+
];
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
if (node.kind === "async-boundary") {
|
|
1222
|
+
if (node.placeholderChildren !== undefined) {
|
|
1223
|
+
const id = `mreact-${state.nextFragmentId}`;
|
|
1224
|
+
state.nextFragmentId += 1;
|
|
1225
|
+
|
|
1226
|
+
return [
|
|
1227
|
+
{
|
|
1228
|
+
kind: "out-of-order-boundary",
|
|
1229
|
+
id,
|
|
1230
|
+
hydration: state.hydration,
|
|
1231
|
+
valueCode: node.valueCode,
|
|
1232
|
+
valueName: node.valueName,
|
|
1233
|
+
parts: node.children.flatMap((child) =>
|
|
1234
|
+
collectHtmlParts(
|
|
1235
|
+
child,
|
|
1236
|
+
escapeHelperName,
|
|
1237
|
+
asyncBoundaryHelperName,
|
|
1238
|
+
outOfOrderBoundaryHelperName,
|
|
1239
|
+
reactSuspenseBoundaryHelperName,
|
|
1240
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
1241
|
+
state,
|
|
1242
|
+
),
|
|
1243
|
+
) as HtmlSyncPart[],
|
|
1244
|
+
placeholderParts: node.placeholderChildren.flatMap((child) =>
|
|
1245
|
+
collectHtmlParts(
|
|
1246
|
+
child,
|
|
1247
|
+
escapeHelperName,
|
|
1248
|
+
asyncBoundaryHelperName,
|
|
1249
|
+
outOfOrderBoundaryHelperName,
|
|
1250
|
+
reactSuspenseBoundaryHelperName,
|
|
1251
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
1252
|
+
state,
|
|
1253
|
+
),
|
|
1254
|
+
) as HtmlSyncPart[],
|
|
1255
|
+
...(node.placeholderTagCode === undefined
|
|
1256
|
+
? {}
|
|
1257
|
+
: { placeholderTagCode: node.placeholderTagCode }),
|
|
1258
|
+
...(state.awaitHydration && node.awaitId !== undefined
|
|
1259
|
+
? { awaitId: node.awaitId }
|
|
1260
|
+
: {}),
|
|
1261
|
+
...(node.catchName === undefined || node.catchChildren === undefined
|
|
1262
|
+
? {}
|
|
1263
|
+
: {
|
|
1264
|
+
catchName: node.catchName,
|
|
1265
|
+
catchParts: node.catchChildren.flatMap((child) =>
|
|
1266
|
+
collectHtmlParts(
|
|
1267
|
+
child,
|
|
1268
|
+
escapeHelperName,
|
|
1269
|
+
asyncBoundaryHelperName,
|
|
1270
|
+
outOfOrderBoundaryHelperName,
|
|
1271
|
+
reactSuspenseBoundaryHelperName,
|
|
1272
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
1273
|
+
state,
|
|
1274
|
+
),
|
|
1275
|
+
) as HtmlSyncPart[],
|
|
1276
|
+
}),
|
|
1277
|
+
},
|
|
1278
|
+
];
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
return [
|
|
1282
|
+
{
|
|
1283
|
+
kind: "async-boundary",
|
|
1284
|
+
valueCode: node.valueCode,
|
|
1285
|
+
valueName: node.valueName,
|
|
1286
|
+
parts: node.children.flatMap((child) =>
|
|
1287
|
+
collectHtmlParts(
|
|
1288
|
+
child,
|
|
1289
|
+
escapeHelperName,
|
|
1290
|
+
asyncBoundaryHelperName,
|
|
1291
|
+
outOfOrderBoundaryHelperName,
|
|
1292
|
+
reactSuspenseBoundaryHelperName,
|
|
1293
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
1294
|
+
state,
|
|
1295
|
+
),
|
|
1296
|
+
) as HtmlSyncPart[],
|
|
1297
|
+
...(state.awaitHydration && node.awaitId !== undefined
|
|
1298
|
+
? { awaitId: node.awaitId }
|
|
1299
|
+
: {}),
|
|
1300
|
+
...(node.catchName === undefined || node.catchChildren === undefined
|
|
1301
|
+
? {}
|
|
1302
|
+
: {
|
|
1303
|
+
catchName: node.catchName,
|
|
1304
|
+
catchParts: node.catchChildren.flatMap((child) =>
|
|
1305
|
+
collectHtmlParts(
|
|
1306
|
+
child,
|
|
1307
|
+
escapeHelperName,
|
|
1308
|
+
asyncBoundaryHelperName,
|
|
1309
|
+
outOfOrderBoundaryHelperName,
|
|
1310
|
+
reactSuspenseBoundaryHelperName,
|
|
1311
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
1312
|
+
state,
|
|
1313
|
+
),
|
|
1314
|
+
) as HtmlSyncPart[],
|
|
1315
|
+
}),
|
|
1316
|
+
},
|
|
1317
|
+
];
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
if (node.kind === "fragment") {
|
|
1321
|
+
return node.children.flatMap((child) =>
|
|
1322
|
+
collectHtmlParts(
|
|
1323
|
+
child,
|
|
1324
|
+
escapeHelperName,
|
|
1325
|
+
asyncBoundaryHelperName,
|
|
1326
|
+
outOfOrderBoundaryHelperName,
|
|
1327
|
+
reactSuspenseBoundaryHelperName,
|
|
1328
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
1329
|
+
state,
|
|
1330
|
+
),
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
if (node.kind === "component") {
|
|
1335
|
+
if (node.name === "Suspense") {
|
|
1336
|
+
const asyncBoundary = findSuspenseAsyncBoundary(node.children);
|
|
1337
|
+
|
|
1338
|
+
if (asyncBoundary !== undefined) {
|
|
1339
|
+
const id = state.nextFragmentId;
|
|
1340
|
+
state.nextFragmentId += 1;
|
|
1341
|
+
|
|
1342
|
+
return [
|
|
1343
|
+
{
|
|
1344
|
+
kind: "react-suspense-out-of-order-boundary",
|
|
1345
|
+
boundaryId: `B:${id}`,
|
|
1346
|
+
segmentId: `S:${id}`,
|
|
1347
|
+
valueCode: asyncBoundary.valueCode,
|
|
1348
|
+
valueName: asyncBoundary.valueName,
|
|
1349
|
+
parts: replaceSuspenseAsyncBoundary(
|
|
1350
|
+
node.children,
|
|
1351
|
+
asyncBoundary,
|
|
1352
|
+
asyncBoundary.children,
|
|
1353
|
+
).flatMap((child) =>
|
|
1354
|
+
collectHtmlParts(
|
|
1355
|
+
child,
|
|
1356
|
+
escapeHelperName,
|
|
1357
|
+
asyncBoundaryHelperName,
|
|
1358
|
+
outOfOrderBoundaryHelperName,
|
|
1359
|
+
reactSuspenseBoundaryHelperName,
|
|
1360
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
1361
|
+
state,
|
|
1362
|
+
),
|
|
1363
|
+
) as HtmlSyncPart[],
|
|
1364
|
+
fallbackParts: collectSuspenseFallbackParts(
|
|
1365
|
+
node.props,
|
|
1366
|
+
escapeHelperName,
|
|
1367
|
+
asyncBoundaryHelperName,
|
|
1368
|
+
outOfOrderBoundaryHelperName,
|
|
1369
|
+
reactSuspenseBoundaryHelperName,
|
|
1370
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
1371
|
+
state,
|
|
1372
|
+
),
|
|
1373
|
+
...(state.reactSuspenseRevealScriptNonce === undefined
|
|
1374
|
+
? {}
|
|
1375
|
+
: { nonce: state.reactSuspenseRevealScriptNonce }),
|
|
1376
|
+
...(state.reactSuspenseRevealScriptSrc === undefined
|
|
1377
|
+
? {}
|
|
1378
|
+
: { scriptSrc: state.reactSuspenseRevealScriptSrc }),
|
|
1379
|
+
...(asyncBoundary.catchName === undefined || asyncBoundary.catchChildren === undefined
|
|
1380
|
+
? {}
|
|
1381
|
+
: {
|
|
1382
|
+
catchName: asyncBoundary.catchName,
|
|
1383
|
+
catchParts: replaceSuspenseAsyncBoundary(
|
|
1384
|
+
node.children,
|
|
1385
|
+
asyncBoundary,
|
|
1386
|
+
asyncBoundary.catchChildren,
|
|
1387
|
+
).flatMap((child) =>
|
|
1388
|
+
collectHtmlParts(
|
|
1389
|
+
child,
|
|
1390
|
+
escapeHelperName,
|
|
1391
|
+
asyncBoundaryHelperName,
|
|
1392
|
+
outOfOrderBoundaryHelperName,
|
|
1393
|
+
reactSuspenseBoundaryHelperName,
|
|
1394
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
1395
|
+
state,
|
|
1396
|
+
),
|
|
1397
|
+
) as HtmlSyncPart[],
|
|
1398
|
+
}),
|
|
1399
|
+
},
|
|
1400
|
+
];
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
if (containsAsyncComponent(node.children)) {
|
|
1404
|
+
const id = state.nextFragmentId;
|
|
1405
|
+
state.nextFragmentId += 1;
|
|
1406
|
+
|
|
1407
|
+
return [
|
|
1408
|
+
{
|
|
1409
|
+
kind: "react-suspense-out-of-order-boundary",
|
|
1410
|
+
boundaryId: `B:${id}`,
|
|
1411
|
+
segmentId: `S:${id}`,
|
|
1412
|
+
valueCode: "undefined",
|
|
1413
|
+
valueName: "_",
|
|
1414
|
+
parts: node.children.flatMap((child) =>
|
|
1415
|
+
collectHtmlParts(
|
|
1416
|
+
child,
|
|
1417
|
+
escapeHelperName,
|
|
1418
|
+
asyncBoundaryHelperName,
|
|
1419
|
+
outOfOrderBoundaryHelperName,
|
|
1420
|
+
reactSuspenseBoundaryHelperName,
|
|
1421
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
1422
|
+
state,
|
|
1423
|
+
),
|
|
1424
|
+
) as HtmlSyncPart[],
|
|
1425
|
+
fallbackParts: collectSuspenseFallbackParts(
|
|
1426
|
+
node.props,
|
|
1427
|
+
escapeHelperName,
|
|
1428
|
+
asyncBoundaryHelperName,
|
|
1429
|
+
outOfOrderBoundaryHelperName,
|
|
1430
|
+
reactSuspenseBoundaryHelperName,
|
|
1431
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
1432
|
+
state,
|
|
1433
|
+
),
|
|
1434
|
+
...(state.reactSuspenseRevealScriptNonce === undefined
|
|
1435
|
+
? {}
|
|
1436
|
+
: { nonce: state.reactSuspenseRevealScriptNonce }),
|
|
1437
|
+
...(state.reactSuspenseRevealScriptSrc === undefined
|
|
1438
|
+
? {}
|
|
1439
|
+
: { scriptSrc: state.reactSuspenseRevealScriptSrc }),
|
|
1440
|
+
},
|
|
1441
|
+
];
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
return [
|
|
1445
|
+
{
|
|
1446
|
+
kind: "react-suspense-boundary",
|
|
1447
|
+
parts: node.children.flatMap((child) =>
|
|
1448
|
+
collectHtmlParts(
|
|
1449
|
+
child,
|
|
1450
|
+
escapeHelperName,
|
|
1451
|
+
asyncBoundaryHelperName,
|
|
1452
|
+
outOfOrderBoundaryHelperName,
|
|
1453
|
+
reactSuspenseBoundaryHelperName,
|
|
1454
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
1455
|
+
state,
|
|
1456
|
+
),
|
|
1457
|
+
) as HtmlSyncPart[],
|
|
1458
|
+
},
|
|
1459
|
+
];
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
if (isClientBoundaryPlaceholder(node)) {
|
|
1463
|
+
const helperName = currentClientBoundaryHelperName;
|
|
1464
|
+
if (helperName !== undefined) {
|
|
1465
|
+
return [
|
|
1466
|
+
{
|
|
1467
|
+
kind: "raw-dynamic",
|
|
1468
|
+
code: `${helperName}(${stringLiteral(node.name)}, ${emitPropsObject(
|
|
1469
|
+
node.props,
|
|
1470
|
+
node.children,
|
|
1471
|
+
escapeHelperName,
|
|
1472
|
+
)})`,
|
|
1473
|
+
},
|
|
1474
|
+
];
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
return [{ kind: "static", value: clientBoundaryPlaceholder(node) }];
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
return [
|
|
1481
|
+
{
|
|
1482
|
+
kind: "component",
|
|
1483
|
+
name: node.name,
|
|
1484
|
+
...(node.runtime === undefined ? {} : { runtime: node.runtime }),
|
|
1485
|
+
...(node.async === undefined ? {} : { async: node.async }),
|
|
1486
|
+
...(node.runtime === "compat" && state.hydration
|
|
1487
|
+
? { hydrationId: `mreact-${state.nextFragmentId++}` }
|
|
1488
|
+
: {}),
|
|
1489
|
+
props: node.props,
|
|
1490
|
+
children: node.children,
|
|
1491
|
+
escapeHelperName,
|
|
1492
|
+
},
|
|
1493
|
+
];
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
const closeTag = `</${node.tagName}>`;
|
|
1497
|
+
if (node.tagName === "textarea") {
|
|
1498
|
+
const attributeScan = scanElementAttributes(node.tagName, node.attributes);
|
|
1499
|
+
return [
|
|
1500
|
+
{ kind: "static", value: "<textarea" },
|
|
1501
|
+
...collectElementAttributeParts(
|
|
1502
|
+
node.tagName,
|
|
1503
|
+
node.attributes,
|
|
1504
|
+
escapeHelperName,
|
|
1505
|
+
state,
|
|
1506
|
+
attributeScan,
|
|
1507
|
+
),
|
|
1508
|
+
{ kind: "static", value: ">" },
|
|
1509
|
+
...collectTextareaValueParts(
|
|
1510
|
+
node,
|
|
1511
|
+
escapeHelperName,
|
|
1512
|
+
asyncBoundaryHelperName,
|
|
1513
|
+
outOfOrderBoundaryHelperName,
|
|
1514
|
+
reactSuspenseBoundaryHelperName,
|
|
1515
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
1516
|
+
state,
|
|
1517
|
+
attributeScan,
|
|
1518
|
+
),
|
|
1519
|
+
{ kind: "static", value: closeTag },
|
|
1520
|
+
];
|
|
1521
|
+
}
|
|
1522
|
+
const attributeScan = scanElementAttributes(node.tagName, node.attributes);
|
|
1523
|
+
const childSelectedValueCode = node.tagName === "select"
|
|
1524
|
+
? attributeScan.formValueAttributeCode
|
|
1525
|
+
: undefined;
|
|
1526
|
+
const childState = childSelectedValueCode === undefined
|
|
1527
|
+
? state
|
|
1528
|
+
: { ...state, selectedValueCode: childSelectedValueCode };
|
|
1529
|
+
const selectedAttributePart = collectOptionSelectedAttributePart(node, state.selectedValueCode);
|
|
1530
|
+
const dangerousInnerHtml = emitDangerouslySetInnerHtmlPart(node.attributes);
|
|
1531
|
+
const childrenParts =
|
|
1532
|
+
dangerousInnerHtml !== undefined
|
|
1533
|
+
? [dangerousInnerHtml]
|
|
1534
|
+
: (childState.selectedValueCode === undefined
|
|
1535
|
+
? collectBatchedSimpleChildrenParts(node.children, state.escapeBatchHelperName)
|
|
1536
|
+
: undefined) ??
|
|
1537
|
+
node.children.flatMap((child) =>
|
|
1538
|
+
collectHtmlParts(
|
|
1539
|
+
child,
|
|
1540
|
+
escapeHelperName,
|
|
1541
|
+
asyncBoundaryHelperName,
|
|
1542
|
+
outOfOrderBoundaryHelperName,
|
|
1543
|
+
reactSuspenseBoundaryHelperName,
|
|
1544
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
1545
|
+
childState,
|
|
1546
|
+
),
|
|
1547
|
+
);
|
|
1548
|
+
|
|
1549
|
+
return [
|
|
1550
|
+
{ kind: "static", value: `<${node.tagName}` },
|
|
1551
|
+
...collectElementAttributeParts(
|
|
1552
|
+
node.tagName,
|
|
1553
|
+
node.attributes,
|
|
1554
|
+
escapeHelperName,
|
|
1555
|
+
state,
|
|
1556
|
+
attributeScan,
|
|
1557
|
+
),
|
|
1558
|
+
...(selectedAttributePart === undefined ? [] : [selectedAttributePart]),
|
|
1559
|
+
{ kind: "static", value: ">" },
|
|
1560
|
+
...childrenParts,
|
|
1561
|
+
{ kind: "static", value: closeTag },
|
|
1562
|
+
];
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
function emitDangerouslySetInnerHtmlPart(attrs: readonly AttributeIr[]): HtmlSyncPart | undefined {
|
|
1566
|
+
const attr = attrs.find(
|
|
1567
|
+
(candidate): candidate is Extract<AttributeIr, { kind: "dynamic-attr" }> =>
|
|
1568
|
+
candidate.kind === "dynamic-attr" && candidate.name === "dangerouslySetInnerHTML",
|
|
1569
|
+
);
|
|
1570
|
+
|
|
1571
|
+
if (attr === undefined) {
|
|
1572
|
+
return undefined;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
return {
|
|
1576
|
+
kind: "raw-dynamic",
|
|
1577
|
+
code: `(() => { const _value = (${attr.code}); return typeof _value === "object" && _value !== null && typeof _value.__html === "string" ? _value.__html : ""; })()`,
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function isChildrenExpressionCode(code: string): boolean {
|
|
1582
|
+
const trimmed = code.trim();
|
|
1583
|
+
return (
|
|
1584
|
+
trimmed === "children" ||
|
|
1585
|
+
endsWithChildrenMemberAccess(trimmed) ||
|
|
1586
|
+
endsWithChildrenStringIndex(trimmed)
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function endsWithChildrenMemberAccess(code: string): boolean {
|
|
1591
|
+
const propertyName = "children";
|
|
1592
|
+
if (!code.endsWith(propertyName)) {
|
|
1593
|
+
return false;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
return code[code.length - propertyName.length - 1] === ".";
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
function endsWithChildrenStringIndex(code: string): boolean {
|
|
1600
|
+
return code.endsWith('["children"]') || code.endsWith("['children']");
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
function collectHtmlAttributeParts(
|
|
1604
|
+
tagName: string,
|
|
1605
|
+
attr: AttributeIr,
|
|
1606
|
+
escapeHelperName: string,
|
|
1607
|
+
escapeBatchHelperName: string | undefined,
|
|
1608
|
+
dynamicAttributes: "drop" | "emit",
|
|
1609
|
+
): HtmlSyncPart[] {
|
|
1610
|
+
if (attr.kind === "spread-attr") {
|
|
1611
|
+
return dynamicAttributes === "drop"
|
|
1612
|
+
? []
|
|
1613
|
+
: [
|
|
1614
|
+
{
|
|
1615
|
+
kind: "raw-dynamic",
|
|
1616
|
+
code: `${currentSpreadAttributesHelperName}(${stringLiteral(tagName)}, (${attr.code}))`,
|
|
1617
|
+
},
|
|
1618
|
+
];
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
if (attr.kind === "event" || attr.name === "key" || attr.name === "dangerouslySetInnerHTML") {
|
|
1622
|
+
return [];
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
if (attr.kind === "static-attr") {
|
|
1626
|
+
const htmlName = htmlAttributeNameForElement(tagName, attr.name);
|
|
1627
|
+
if (isUrlAttribute(htmlName) && isStaticUrlValueUnsafe(htmlName, attr.value)) {
|
|
1628
|
+
return [];
|
|
1629
|
+
}
|
|
1630
|
+
if (isDangerousHtmlAttribute(htmlName)) {
|
|
1631
|
+
// Issue 077: literal srcdoc strings cannot match the opt-in shape.
|
|
1632
|
+
return [];
|
|
1633
|
+
}
|
|
1634
|
+
return [
|
|
1635
|
+
{
|
|
1636
|
+
kind: "static",
|
|
1637
|
+
value: ` ${htmlName}="${escapeHtml(attr.value)}"`,
|
|
1638
|
+
},
|
|
1639
|
+
];
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
if (dynamicAttributes === "drop") {
|
|
1643
|
+
return [];
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
if (attr.name === "style") {
|
|
1647
|
+
return [{ kind: "raw-dynamic", code: emitDynamicStyleAttributeExpression(attr.code, escapeHelperName, escapeBatchHelperName) }];
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
const dynamicHtmlName = htmlAttributeNameForElement(tagName, attr.name);
|
|
1651
|
+
if (isDangerousHtmlAttribute(dynamicHtmlName)) {
|
|
1652
|
+
return [
|
|
1653
|
+
{
|
|
1654
|
+
kind: "raw-dynamic",
|
|
1655
|
+
code: `(() => { const _value = (${attr.code}); if (_value == null || _value === false) return ""; if (typeof _value === "object" && _value !== null && typeof _value.__html === "string") return ${stringLiteral(` ${dynamicHtmlName}="`)} + ${escapeHelperName}(_value.__html) + ${stringLiteral("\"")}; return ""; })()`,
|
|
1656
|
+
},
|
|
1657
|
+
];
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
return [
|
|
1661
|
+
{
|
|
1662
|
+
kind: "raw-dynamic",
|
|
1663
|
+
code: emitDynamicAttributeExpression(dynamicHtmlName, attr.code, escapeHelperName),
|
|
1664
|
+
},
|
|
1665
|
+
];
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function collectElementAttributeParts(
|
|
1669
|
+
tagName: string,
|
|
1670
|
+
attrs: readonly AttributeIr[],
|
|
1671
|
+
escapeHelperName: string,
|
|
1672
|
+
state: CollectHtmlState,
|
|
1673
|
+
attributeScan = scanElementAttributes(tagName, attrs),
|
|
1674
|
+
): HtmlSyncPart[] {
|
|
1675
|
+
const escapeBatchHelperName = state.escapeBatchHelperName;
|
|
1676
|
+
|
|
1677
|
+
return attrs.flatMap((attr) =>
|
|
1678
|
+
attr.kind !== "spread-attr" &&
|
|
1679
|
+
((tagName === "input" &&
|
|
1680
|
+
((attr.name === "defaultValue" && attributeScan.hasExplicitInputValue) ||
|
|
1681
|
+
(attr.name === "defaultChecked" && attributeScan.hasExplicitInputChecked))) ||
|
|
1682
|
+
((tagName === "textarea" || tagName === "select") &&
|
|
1683
|
+
(attr.name === "value" || attr.name === "defaultValue")))
|
|
1684
|
+
? []
|
|
1685
|
+
: collectHtmlAttributeParts(
|
|
1686
|
+
tagName,
|
|
1687
|
+
attr,
|
|
1688
|
+
escapeHelperName,
|
|
1689
|
+
escapeBatchHelperName,
|
|
1690
|
+
state.dynamicAttributes,
|
|
1691
|
+
),
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
interface ElementAttributeScan {
|
|
1696
|
+
hasExplicitInputValue: boolean;
|
|
1697
|
+
hasExplicitInputChecked: boolean;
|
|
1698
|
+
formValueAttributeCode: string | undefined;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
function scanElementAttributes(
|
|
1702
|
+
tagName: string,
|
|
1703
|
+
attrs: readonly AttributeIr[],
|
|
1704
|
+
): ElementAttributeScan {
|
|
1705
|
+
let hasExplicitInputValue = false;
|
|
1706
|
+
let hasExplicitInputChecked = false;
|
|
1707
|
+
let valueAttributeCode: string | undefined;
|
|
1708
|
+
let defaultValueAttributeCode: string | undefined;
|
|
1709
|
+
|
|
1710
|
+
for (const attr of attrs) {
|
|
1711
|
+
if (attr.kind === "spread-attr") {
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
if (tagName === "input") {
|
|
1716
|
+
if (attr.name === "value") {
|
|
1717
|
+
hasExplicitInputValue = true;
|
|
1718
|
+
} else if (attr.name === "checked") {
|
|
1719
|
+
hasExplicitInputChecked = true;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
if ((tagName === "textarea" || tagName === "select") && attr.name === "value") {
|
|
1724
|
+
valueAttributeCode = readFormValueAttributeCode(attr);
|
|
1725
|
+
} else if (
|
|
1726
|
+
(tagName === "textarea" || tagName === "select") &&
|
|
1727
|
+
attr.name === "defaultValue"
|
|
1728
|
+
) {
|
|
1729
|
+
defaultValueAttributeCode = readFormValueAttributeCode(attr);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
return {
|
|
1734
|
+
hasExplicitInputValue,
|
|
1735
|
+
hasExplicitInputChecked,
|
|
1736
|
+
formValueAttributeCode: valueAttributeCode ?? defaultValueAttributeCode,
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function readFormValueAttributeCode(
|
|
1741
|
+
attr: Exclude<AttributeIr, { kind: "spread-attr" }>,
|
|
1742
|
+
): string | undefined {
|
|
1743
|
+
if (attr.kind === "event") {
|
|
1744
|
+
return undefined;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
return attr.kind === "static-attr" ? stringLiteral(attr.value) : `(${attr.code})`;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
function emitDynamicAttributeExpression(
|
|
1751
|
+
name: string,
|
|
1752
|
+
code: string,
|
|
1753
|
+
escapeHelperName: string,
|
|
1754
|
+
): string {
|
|
1755
|
+
if (isUrlAttribute(name)) {
|
|
1756
|
+
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("\"")}; })()`;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
const inlineExpr = simpleSideEffectFreeExpression(code);
|
|
1760
|
+
|
|
1761
|
+
if (inlineExpr !== undefined) {
|
|
1762
|
+
// Inline 3 evaluations to avoid per-attribute IIFE closure allocation.
|
|
1763
|
+
return `(${inlineExpr} == null || ${inlineExpr} === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(${inlineExpr} === true ? "" : ${inlineExpr}) + ${stringLiteral("\"")})`;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
return `(() => { const _value = (${code}); return _value == null || _value === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_value === true ? "" : _value) + ${stringLiteral("\"")}; })()`;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
function emitDynamicStyleAttributeExpression(
|
|
1770
|
+
code: string,
|
|
1771
|
+
escapeHelperName: string,
|
|
1772
|
+
escapeBatchHelperName: string | undefined,
|
|
1773
|
+
): string {
|
|
1774
|
+
const staticStyleExpression = emitStaticStyleObjectAttributeExpression(code, escapeHelperName);
|
|
1775
|
+
|
|
1776
|
+
if (staticStyleExpression !== undefined) {
|
|
1777
|
+
return staticStyleExpression;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
const escapedPair = escapeBatchHelperName === undefined
|
|
1781
|
+
? `${escapeHelperName}(_cssName) + ":" + ${escapeHelperName}(_styleValue === true ? "" : _styleValue)`
|
|
1782
|
+
: `(() => { const _escaped = ${escapeBatchHelperName}([_cssName, _styleValue === true ? "" : _styleValue]); return _escaped[0] + ":" + _escaped[1]; })()`;
|
|
1783
|
+
|
|
1784
|
+
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("\"")}; })()`;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
function emitStaticStyleObjectAttributeExpression(
|
|
1788
|
+
code: string,
|
|
1789
|
+
escapeHelperName: string,
|
|
1790
|
+
): string | undefined {
|
|
1791
|
+
const entries = parseStaticStyleObjectLiteral(code);
|
|
1792
|
+
|
|
1793
|
+
if (entries === undefined) {
|
|
1794
|
+
return undefined;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
if (entries.length === 0) {
|
|
1798
|
+
return `""`;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
const literalEntries = entries.map((entry) => ({
|
|
1802
|
+
cssName: entry.cssName,
|
|
1803
|
+
literal: parseStyleLiteralValue(entry.valueCode),
|
|
1804
|
+
}));
|
|
1805
|
+
|
|
1806
|
+
if (literalEntries.every((entry) => entry.literal !== undefined)) {
|
|
1807
|
+
const parts = literalEntries
|
|
1808
|
+
.filter((entry) => entry.literal !== null)
|
|
1809
|
+
.map((entry) => `${entry.cssName}:${escapeHtml(String(entry.literal))}`);
|
|
1810
|
+
|
|
1811
|
+
if (parts.length === 0) {
|
|
1812
|
+
return `""`;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
return stringLiteral(` style="${parts.join(";")}"`);
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
const statements = entries.map((entry) =>
|
|
1819
|
+
`{ const _v = (${entry.valueCode}); if (_v != null && _v !== false) _style += (_style === "" ? "" : ";") + ${stringLiteral(`${entry.cssName}:`)} + ${escapeHelperName}(_v === true ? "" : _v); }`
|
|
1820
|
+
);
|
|
1821
|
+
|
|
1822
|
+
return `(() => { let _style = ""; ${statements.join(" ")} return _style === "" ? "" : ${stringLiteral(" style=\"")} + _style + ${stringLiteral("\"")}; })()`;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
function collectTextareaValueParts(
|
|
1826
|
+
node: Extract<JsxNodeIr, { kind: "element" }>,
|
|
1827
|
+
escapeHelperName: string,
|
|
1828
|
+
asyncBoundaryHelperName: string,
|
|
1829
|
+
outOfOrderBoundaryHelperName: string,
|
|
1830
|
+
reactSuspenseBoundaryHelperName: string,
|
|
1831
|
+
reactSuspenseOutOfOrderBoundaryHelperName: string,
|
|
1832
|
+
state: CollectHtmlState,
|
|
1833
|
+
attributeScan = scanElementAttributes(node.tagName, node.attributes),
|
|
1834
|
+
): HtmlPart[] {
|
|
1835
|
+
const valueCode = attributeScan.formValueAttributeCode;
|
|
1836
|
+
if (valueCode !== undefined) {
|
|
1837
|
+
return [{ kind: "dynamic", code: valueCode, escapeHelperName }];
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
return node.children.flatMap((child) =>
|
|
1841
|
+
collectHtmlParts(
|
|
1842
|
+
child,
|
|
1843
|
+
escapeHelperName,
|
|
1844
|
+
asyncBoundaryHelperName,
|
|
1845
|
+
outOfOrderBoundaryHelperName,
|
|
1846
|
+
reactSuspenseBoundaryHelperName,
|
|
1847
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
1848
|
+
state,
|
|
1849
|
+
)
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function collectOptionSelectedAttributePart(
|
|
1854
|
+
node: Extract<JsxNodeIr, { kind: "element" }>,
|
|
1855
|
+
selectedValueCode: string | undefined,
|
|
1856
|
+
): HtmlSyncPart | undefined {
|
|
1857
|
+
if (selectedValueCode === undefined || node.tagName !== "option") {
|
|
1858
|
+
return undefined;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
const optionValueCode = findOptionValueCode(node);
|
|
1862
|
+
if (optionValueCode === undefined) {
|
|
1863
|
+
return undefined;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
return {
|
|
1867
|
+
kind: "raw-dynamic",
|
|
1868
|
+
code: `(() => { const _selected = (${selectedValueCode}); return _selected == null ? "" : String(_selected) === String(${optionValueCode}) ? ${stringLiteral(' selected=""')} : ""; })()`,
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
function findOptionValueCode(node: Extract<JsxNodeIr, { kind: "element" }>): string | undefined {
|
|
1873
|
+
const valueAttr = node.attributes.find((attr) => attr.kind !== "spread-attr" && attr.name === "value");
|
|
1874
|
+
if (valueAttr !== undefined && valueAttr.kind !== "event" && valueAttr.kind !== "spread-attr") {
|
|
1875
|
+
return valueAttr.kind === "static-attr" ? stringLiteral(valueAttr.value) : `(${valueAttr.code})`;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
return node.children.every((child) => child.kind === "text")
|
|
1879
|
+
? stringLiteral(node.children.map((child) => child.value).join(""))
|
|
1880
|
+
: undefined;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
function htmlAttributeNameForElement(tagName: string, name: string): string {
|
|
1884
|
+
if (tagName === "input") {
|
|
1885
|
+
if (name === "defaultValue") {
|
|
1886
|
+
return "value";
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
if (name === "defaultChecked") {
|
|
1890
|
+
return "checked";
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
return htmlAttributeName(name);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
function findSuspenseAsyncBoundary(children: readonly JsxNodeIr[]): AsyncBoundaryIr | undefined {
|
|
1898
|
+
for (const child of children) {
|
|
1899
|
+
if (child.kind === "async-boundary" && child.placeholderChildren === undefined) {
|
|
1900
|
+
return child;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
const nested =
|
|
1904
|
+
child.kind === "element" || child.kind === "fragment" || child.kind === "component"
|
|
1905
|
+
? findSuspenseAsyncBoundary(child.children)
|
|
1906
|
+
: child.kind === "conditional"
|
|
1907
|
+
? findSuspenseAsyncBoundary([...child.whenTrue, ...child.whenFalse])
|
|
1908
|
+
: child.kind === "list"
|
|
1909
|
+
? findSuspenseAsyncBoundary(child.children)
|
|
1910
|
+
: undefined;
|
|
1911
|
+
|
|
1912
|
+
if (nested !== undefined) {
|
|
1913
|
+
return nested;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
return undefined;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
function replaceSuspenseAsyncBoundary(
|
|
1921
|
+
children: readonly JsxNodeIr[],
|
|
1922
|
+
target: AsyncBoundaryIr,
|
|
1923
|
+
replacement: readonly JsxNodeIr[],
|
|
1924
|
+
): JsxNodeIr[] {
|
|
1925
|
+
return children.flatMap((child): JsxNodeIr[] => {
|
|
1926
|
+
if (child === target) {
|
|
1927
|
+
return [...replacement];
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
if (child.kind === "element") {
|
|
1931
|
+
return [
|
|
1932
|
+
{
|
|
1933
|
+
...child,
|
|
1934
|
+
children: replaceSuspenseAsyncBoundary(child.children, target, replacement),
|
|
1935
|
+
},
|
|
1936
|
+
];
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
if (child.kind === "fragment") {
|
|
1940
|
+
return [
|
|
1941
|
+
{
|
|
1942
|
+
...child,
|
|
1943
|
+
children: replaceSuspenseAsyncBoundary(child.children, target, replacement),
|
|
1944
|
+
},
|
|
1945
|
+
];
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
if (child.kind === "component") {
|
|
1949
|
+
return [
|
|
1950
|
+
{
|
|
1951
|
+
...child,
|
|
1952
|
+
children: replaceSuspenseAsyncBoundary(child.children, target, replacement),
|
|
1953
|
+
},
|
|
1954
|
+
];
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
if (child.kind === "conditional") {
|
|
1958
|
+
return [
|
|
1959
|
+
{
|
|
1960
|
+
...child,
|
|
1961
|
+
whenTrue: replaceSuspenseAsyncBoundary(child.whenTrue, target, replacement),
|
|
1962
|
+
whenFalse: replaceSuspenseAsyncBoundary(child.whenFalse, target, replacement),
|
|
1963
|
+
},
|
|
1964
|
+
];
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
if (child.kind === "list") {
|
|
1968
|
+
return [
|
|
1969
|
+
{
|
|
1970
|
+
...child,
|
|
1971
|
+
children: replaceSuspenseAsyncBoundary(child.children, target, replacement),
|
|
1972
|
+
},
|
|
1973
|
+
];
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
return [child];
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
function collectSuspenseFallbackParts(
|
|
1981
|
+
props: readonly ComponentPropIr[],
|
|
1982
|
+
escapeHelperName: string,
|
|
1983
|
+
asyncBoundaryHelperName: string,
|
|
1984
|
+
outOfOrderBoundaryHelperName: string,
|
|
1985
|
+
reactSuspenseBoundaryHelperName: string,
|
|
1986
|
+
reactSuspenseOutOfOrderBoundaryHelperName: string,
|
|
1987
|
+
state: CollectHtmlState,
|
|
1988
|
+
): HtmlSyncPart[] {
|
|
1989
|
+
for (const prop of props) {
|
|
1990
|
+
if (prop.kind === "render-prop" && prop.name === "fallback") {
|
|
1991
|
+
return prop.children.flatMap((child) =>
|
|
1992
|
+
collectHtmlParts(
|
|
1993
|
+
child,
|
|
1994
|
+
escapeHelperName,
|
|
1995
|
+
asyncBoundaryHelperName,
|
|
1996
|
+
outOfOrderBoundaryHelperName,
|
|
1997
|
+
reactSuspenseBoundaryHelperName,
|
|
1998
|
+
reactSuspenseOutOfOrderBoundaryHelperName,
|
|
1999
|
+
state,
|
|
2000
|
+
),
|
|
2001
|
+
) as HtmlSyncPart[];
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
if (prop.kind === "prop" && prop.name === "fallback") {
|
|
2005
|
+
return [
|
|
2006
|
+
{
|
|
2007
|
+
kind: "dynamic",
|
|
2008
|
+
code: prop.code,
|
|
2009
|
+
escapeHelperName,
|
|
2010
|
+
},
|
|
2011
|
+
];
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
return [];
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
function rawHtmlExpression(code: string): string {
|
|
2019
|
+
return `(() => { const _render = (_value) => { if (_value == null) return ""; if (Array.isArray(_value)) return _value.map(_render).join(""); if (typeof _value === "object" && _value.$$typeof === Symbol.for("modular.react.element")) return ${currentCompatRenderToStringHelperName}(() => _value); return String(_value); }; return _render(${code}); })()`;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
function emitDynamicHtmlAppendStatement(
|
|
2023
|
+
code: string,
|
|
2024
|
+
sinkName: string,
|
|
2025
|
+
escapeHelperName: string,
|
|
2026
|
+
compatRenderToStringHelperName: string,
|
|
2027
|
+
indent: string,
|
|
2028
|
+
): string {
|
|
2029
|
+
if (!looksLikeRawJsxExpression(code)) {
|
|
2030
|
+
return `${indent}${sinkName}.append(${escapeHelperName}(${code}));`;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
return [
|
|
2034
|
+
`${indent}{`,
|
|
2035
|
+
`${indent} const _appendDynamic = async (_value) => {`,
|
|
2036
|
+
`${indent} if (_value == null || _value === false) return;`,
|
|
2037
|
+
`${indent} if (Array.isArray(_value)) { for (const _item of _value) await _appendDynamic(_item); return; }`,
|
|
2038
|
+
`${indent} if (typeof _value === "object" && _value.$$typeof === Symbol.for("modular.react.element")) {`,
|
|
2039
|
+
`${indent} if (typeof _value.type === "function" && _value.type.length >= 2) {`,
|
|
2040
|
+
`${indent} await _value.type(${sinkName}, _value.props ?? {});`,
|
|
2041
|
+
`${indent} } else {`,
|
|
2042
|
+
`${indent} ${sinkName}.append(${compatRenderToStringHelperName}(() => _value));`,
|
|
2043
|
+
`${indent} }`,
|
|
2044
|
+
`${indent} return;`,
|
|
2045
|
+
`${indent} }`,
|
|
2046
|
+
`${indent} ${sinkName}.append(${escapeHelperName}(_value === true ? "" : _value));`,
|
|
2047
|
+
`${indent} };`,
|
|
2048
|
+
`${indent} await _appendDynamic(${code});`,
|
|
2049
|
+
`${indent}}`,
|
|
2050
|
+
].join("\n");
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
function looksLikeRawJsxExpression(code: string): boolean {
|
|
2054
|
+
return /<\s*(?:[A-Za-z]|>)/.test(code);
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
function collectBatchedSimpleChildrenParts(
|
|
2058
|
+
children: readonly JsxNodeIr[],
|
|
2059
|
+
escapeBatchHelperName: string | undefined,
|
|
2060
|
+
): HtmlSyncPart[] | undefined {
|
|
2061
|
+
if (escapeBatchHelperName === undefined) {
|
|
2062
|
+
return undefined;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
const dynamicChildren = children.filter(
|
|
2066
|
+
(child) => child.kind === "expr" && child.renderMode !== "html" && child.renderMode !== "react-node",
|
|
2067
|
+
) as Array<Extract<JsxNodeIr, { kind: "expr" }>>;
|
|
2068
|
+
|
|
2069
|
+
if (dynamicChildren.length < 2) {
|
|
2070
|
+
return undefined;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
if (
|
|
2074
|
+
children.some(
|
|
2075
|
+
(child) =>
|
|
2076
|
+
child.kind !== "text" &&
|
|
2077
|
+
!(child.kind === "expr" && child.renderMode !== "html" && child.renderMode !== "react-node"),
|
|
2078
|
+
)
|
|
2079
|
+
) {
|
|
2080
|
+
return undefined;
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
const values = dynamicChildren.map((child) => child.code);
|
|
2084
|
+
let dynamicIndex = 0;
|
|
2085
|
+
const pieces = children.map((child) => {
|
|
2086
|
+
if (child.kind === "text") {
|
|
2087
|
+
return stringLiteral(escapeHtml(child.value));
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
const index = dynamicIndex;
|
|
2091
|
+
dynamicIndex += 1;
|
|
2092
|
+
|
|
2093
|
+
return `_escaped[${index}]`;
|
|
2094
|
+
});
|
|
2095
|
+
|
|
2096
|
+
return [
|
|
2097
|
+
{
|
|
2098
|
+
kind: "raw-dynamic",
|
|
2099
|
+
code: `(() => { const _escaped = ${escapeBatchHelperName}([${values.join(", ")}]); return ${pieces.join(" + ")}; })()`,
|
|
2100
|
+
},
|
|
2101
|
+
];
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
function emitHtmlExpressionFromChildren(children: JsxNodeIr[], escapeHelperName: string): string {
|
|
2105
|
+
if (children.length === 0) {
|
|
2106
|
+
return '""';
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
const parts = children.flatMap((child) =>
|
|
2110
|
+
collectHtmlParts(
|
|
2111
|
+
child,
|
|
2112
|
+
escapeHelperName,
|
|
2113
|
+
"_renderAsyncBoundary",
|
|
2114
|
+
"_renderOutOfOrderBoundary",
|
|
2115
|
+
"_renderReactSuspenseBoundary",
|
|
2116
|
+
"_renderReactSuspenseOutOfOrderBoundary",
|
|
2117
|
+
{
|
|
2118
|
+
dynamicAttributes: "emit",
|
|
2119
|
+
hydration: false,
|
|
2120
|
+
awaitHydration: false,
|
|
2121
|
+
nextFragmentId: 0,
|
|
2122
|
+
},
|
|
2123
|
+
),
|
|
2124
|
+
);
|
|
2125
|
+
const expressions = parts.map((part) =>
|
|
2126
|
+
isHtmlSyncPart(part)
|
|
2127
|
+
? tryEmitPartAsStringExpression(part, currentCompatRenderToStringHelperName) ?? '""'
|
|
2128
|
+
: '""'
|
|
2129
|
+
);
|
|
2130
|
+
|
|
2131
|
+
return expressions.length === 0 ? '""' : expressions.join(" + ");
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
function emitStreamRendererFromChildren(
|
|
2135
|
+
children: JsxNodeIr[],
|
|
2136
|
+
escapeHelperName: string,
|
|
2137
|
+
): string | undefined {
|
|
2138
|
+
if (children.length === 0) {
|
|
2139
|
+
return undefined;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
const parentState = currentPropChildrenCollectState;
|
|
2143
|
+
const childState: CollectHtmlState = {
|
|
2144
|
+
dynamicAttributes: "emit",
|
|
2145
|
+
hydration: false,
|
|
2146
|
+
awaitHydration: false,
|
|
2147
|
+
nextFragmentId: parentState?.nextFragmentId ?? 0,
|
|
2148
|
+
...(parentState?.reactSuspenseRevealScriptNonce === undefined
|
|
2149
|
+
? {}
|
|
2150
|
+
: { reactSuspenseRevealScriptNonce: parentState.reactSuspenseRevealScriptNonce }),
|
|
2151
|
+
...(parentState?.reactSuspenseRevealScriptSrc === undefined
|
|
2152
|
+
? {}
|
|
2153
|
+
: { reactSuspenseRevealScriptSrc: parentState.reactSuspenseRevealScriptSrc }),
|
|
2154
|
+
};
|
|
2155
|
+
const parts = children.flatMap((child) =>
|
|
2156
|
+
collectHtmlParts(
|
|
2157
|
+
child,
|
|
2158
|
+
escapeHelperName,
|
|
2159
|
+
currentAsyncBoundaryHelperName,
|
|
2160
|
+
currentOutOfOrderBoundaryHelperName,
|
|
2161
|
+
currentReactSuspenseBoundaryHelperName,
|
|
2162
|
+
currentReactSuspenseOutOfOrderBoundaryHelperName,
|
|
2163
|
+
childState,
|
|
2164
|
+
),
|
|
2165
|
+
);
|
|
2166
|
+
if (parentState !== undefined) {
|
|
2167
|
+
parentState.nextFragmentId = childState.nextFragmentId;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
if (
|
|
2171
|
+
parts.every(
|
|
2172
|
+
(part) =>
|
|
2173
|
+
isHtmlSyncPart(part) &&
|
|
2174
|
+
tryEmitPartAsStringExpression(part, currentCompatRenderToStringHelperName) !== undefined,
|
|
2175
|
+
)
|
|
2176
|
+
) {
|
|
2177
|
+
return undefined;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
return `async ($sink) => {\n${emitNestedStreamAppendStatements(parts, "$sink", currentCompatRenderToStringHelperName)}\n}`;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
function containsAsyncBoundary(node: JsxNodeIr, outOfOrder: boolean): boolean {
|
|
2184
|
+
if (node.kind === "async-boundary") {
|
|
2185
|
+
return outOfOrder
|
|
2186
|
+
? node.placeholderChildren !== undefined
|
|
2187
|
+
: node.placeholderChildren === undefined;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
if (node.kind === "conditional") {
|
|
2191
|
+
return [...node.whenTrue, ...node.whenFalse].some((child) =>
|
|
2192
|
+
containsAsyncBoundary(child, outOfOrder),
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
if (node.kind === "list") {
|
|
2197
|
+
return node.children.some((child) => containsAsyncBoundary(child, outOfOrder));
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
if (node.kind === "element" || node.kind === "fragment") {
|
|
2201
|
+
return node.children.some((child) => containsAsyncBoundary(child, outOfOrder));
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
if (node.kind === "component") {
|
|
2205
|
+
if (isClientBoundaryPlaceholder(node)) {
|
|
2206
|
+
return false;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
return node.name === "Suspense" ? false : true;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
return false;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
function containsAnyAsyncBoundary(node: JsxNodeIr): boolean {
|
|
2216
|
+
if (node.kind === "async-boundary") {
|
|
2217
|
+
return true;
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
if (node.kind === "expr") {
|
|
2221
|
+
return (
|
|
2222
|
+
node.renderMode === "stream-node" ||
|
|
2223
|
+
looksLikeRawJsxExpression(node.code) ||
|
|
2224
|
+
(node.renderMode === "html" && isChildrenExpressionCode(node.code))
|
|
2225
|
+
);
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
if (node.kind === "conditional") {
|
|
2229
|
+
return [...node.whenTrue, ...node.whenFalse].some(containsAnyAsyncBoundary);
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
if (node.kind === "list") {
|
|
2233
|
+
return node.children.some(containsAnyAsyncBoundary);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
if (node.kind === "element" || node.kind === "fragment") {
|
|
2237
|
+
return node.children.some(containsAnyAsyncBoundary);
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
if (node.kind === "component") {
|
|
2241
|
+
if (isClientBoundaryPlaceholder(node)) {
|
|
2242
|
+
return false;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
return node.name === "Suspense" ? true : true;
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
return false;
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
function containsReactSuspense(node: JsxNodeIr, outOfOrder: boolean): boolean {
|
|
2252
|
+
if (node.kind === "component" && node.name === "Suspense") {
|
|
2253
|
+
return outOfOrder
|
|
2254
|
+
? findSuspenseAsyncBoundary(node.children) !== undefined ||
|
|
2255
|
+
containsAsyncComponent(node.children)
|
|
2256
|
+
: findSuspenseAsyncBoundary(node.children) === undefined &&
|
|
2257
|
+
!containsAsyncComponent(node.children);
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
if (node.kind === "conditional") {
|
|
2261
|
+
return [...node.whenTrue, ...node.whenFalse].some((child) =>
|
|
2262
|
+
containsReactSuspense(child, outOfOrder),
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
if (node.kind === "list") {
|
|
2267
|
+
return node.children.some((child) => containsReactSuspense(child, outOfOrder));
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
if (node.kind === "element" || node.kind === "fragment") {
|
|
2271
|
+
return node.children.some((child) => containsReactSuspense(child, outOfOrder));
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
if (node.kind === "async-boundary") {
|
|
2275
|
+
return [
|
|
2276
|
+
...node.children,
|
|
2277
|
+
...(node.placeholderChildren ?? []),
|
|
2278
|
+
...(node.catchChildren ?? []),
|
|
2279
|
+
].some((child) => containsReactSuspense(child, outOfOrder));
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
return false;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
function containsAsyncComponent(children: readonly JsxNodeIr[]): boolean {
|
|
2286
|
+
return children.some((child) => {
|
|
2287
|
+
if (child.kind === "component") {
|
|
2288
|
+
if (isClientBoundaryPlaceholder(child)) {
|
|
2289
|
+
return false;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
return (
|
|
2293
|
+
child.async === true ||
|
|
2294
|
+
containsAsyncComponent(child.children) ||
|
|
2295
|
+
child.props.some(
|
|
2296
|
+
(prop) =>
|
|
2297
|
+
prop.kind === "render-prop" && containsAsyncComponent(prop.children),
|
|
2298
|
+
)
|
|
2299
|
+
);
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
if (child.kind === "conditional") {
|
|
2303
|
+
return containsAsyncComponent([...child.whenTrue, ...child.whenFalse]);
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
if (child.kind === "list") {
|
|
2307
|
+
return containsAsyncComponent(child.children);
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
if (child.kind === "element" || child.kind === "fragment") {
|
|
2311
|
+
return containsAsyncComponent(child.children);
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
if (child.kind === "async-boundary") {
|
|
2315
|
+
return containsAsyncComponent([
|
|
2316
|
+
...child.children,
|
|
2317
|
+
...(child.placeholderChildren ?? []),
|
|
2318
|
+
...(child.catchChildren ?? []),
|
|
2319
|
+
]);
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
return false;
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
function containsCompatComponent(node: JsxNodeIr): boolean {
|
|
2327
|
+
if (node.kind === "component") {
|
|
2328
|
+
return (
|
|
2329
|
+
node.runtime === "compat" ||
|
|
2330
|
+
node.children.some(containsCompatComponent) ||
|
|
2331
|
+
node.props.some(
|
|
2332
|
+
(prop) => prop.kind === "render-prop" && prop.children.some(containsCompatComponent),
|
|
2333
|
+
)
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
if (node.kind === "conditional") {
|
|
2338
|
+
return [...node.whenTrue, ...node.whenFalse].some(containsCompatComponent);
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
if (node.kind === "list") {
|
|
2342
|
+
return node.children.some(containsCompatComponent);
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
if (node.kind === "element" || node.kind === "fragment") {
|
|
2346
|
+
return node.children.some(containsCompatComponent);
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
if (node.kind === "async-boundary") {
|
|
2350
|
+
return [
|
|
2351
|
+
...node.children,
|
|
2352
|
+
...(node.placeholderChildren ?? []),
|
|
2353
|
+
...(node.catchChildren ?? []),
|
|
2354
|
+
].some(containsCompatComponent);
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
return false;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
function containsClientBoundary(node: JsxNodeIr): boolean {
|
|
2361
|
+
if (node.kind === "component" && isClientBoundaryPlaceholder(node)) {
|
|
2362
|
+
return true;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
if (node.kind === "component") {
|
|
2366
|
+
return (
|
|
2367
|
+
node.children.some(containsClientBoundary) ||
|
|
2368
|
+
node.props.some(
|
|
2369
|
+
(prop) => prop.kind === "render-prop" && prop.children.some(containsClientBoundary),
|
|
2370
|
+
)
|
|
2371
|
+
);
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
if (node.kind === "conditional") {
|
|
2375
|
+
return [...node.whenTrue, ...node.whenFalse].some(containsClientBoundary);
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
if (node.kind === "list") {
|
|
2379
|
+
return node.children.some(containsClientBoundary);
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
if (node.kind === "element" || node.kind === "fragment") {
|
|
2383
|
+
return node.children.some(containsClientBoundary);
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
if (node.kind === "async-boundary") {
|
|
2387
|
+
return [
|
|
2388
|
+
...node.children,
|
|
2389
|
+
...(node.placeholderChildren ?? []),
|
|
2390
|
+
...(node.catchChildren ?? []),
|
|
2391
|
+
].some(containsClientBoundary);
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
return false;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
function containsReactNodeRender(node: JsxNodeIr): boolean {
|
|
2398
|
+
if (node.kind === "expr") {
|
|
2399
|
+
return node.renderMode === "react-node";
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
if (node.kind === "component") {
|
|
2403
|
+
if (node.runtime === "compat") {
|
|
2404
|
+
return true;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
return (
|
|
2408
|
+
node.children.some(containsReactNodeRender) ||
|
|
2409
|
+
node.props.some(
|
|
2410
|
+
(prop) => prop.kind === "render-prop" && prop.children.some(containsReactNodeRender),
|
|
2411
|
+
)
|
|
2412
|
+
);
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
if (node.kind === "conditional") {
|
|
2416
|
+
return [...node.whenTrue, ...node.whenFalse].some(containsReactNodeRender);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
if (node.kind === "list") {
|
|
2420
|
+
return node.children.some(containsReactNodeRender);
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
if (node.kind === "element" || node.kind === "fragment") {
|
|
2424
|
+
return node.children.some(containsReactNodeRender);
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
if (node.kind === "async-boundary") {
|
|
2428
|
+
return [
|
|
2429
|
+
...node.children,
|
|
2430
|
+
...(node.placeholderChildren ?? []),
|
|
2431
|
+
...(node.catchChildren ?? []),
|
|
2432
|
+
].some(containsReactNodeRender);
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
return false;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
function containsRawJsxDynamicRender(node: JsxNodeIr): boolean {
|
|
2439
|
+
if (node.kind === "expr") {
|
|
2440
|
+
return node.renderMode !== "react-node" && looksLikeRawJsxExpression(node.code);
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
if (node.kind === "conditional") {
|
|
2444
|
+
return [...node.whenTrue, ...node.whenFalse].some(containsRawJsxDynamicRender);
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
if (node.kind === "list") {
|
|
2448
|
+
return node.children.some(containsRawJsxDynamicRender);
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
if (node.kind === "component") {
|
|
2452
|
+
return (
|
|
2453
|
+
node.children.some(containsRawJsxDynamicRender) ||
|
|
2454
|
+
node.props.some(
|
|
2455
|
+
(prop) => prop.kind === "render-prop" && prop.children.some(containsRawJsxDynamicRender),
|
|
2456
|
+
)
|
|
2457
|
+
);
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
if (node.kind === "element" || node.kind === "fragment") {
|
|
2461
|
+
return node.children.some(containsRawJsxDynamicRender);
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
if (node.kind === "async-boundary") {
|
|
2465
|
+
return [
|
|
2466
|
+
...node.children,
|
|
2467
|
+
...(node.placeholderChildren ?? []),
|
|
2468
|
+
...(node.catchChildren ?? []),
|
|
2469
|
+
].some(containsRawJsxDynamicRender);
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
return false;
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
function emitPropsObject(
|
|
2476
|
+
props: ComponentPropIr[],
|
|
2477
|
+
children: JsxNodeIr[] = [],
|
|
2478
|
+
escapeHelperName = "_escapeHtml",
|
|
2479
|
+
): string {
|
|
2480
|
+
const entries = props.map((prop) => {
|
|
2481
|
+
if (prop.kind === "spread-prop") {
|
|
2482
|
+
return `...(${prop.code})`;
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
if (prop.kind === "render-prop") {
|
|
2486
|
+
return `${emitPropName(prop.name)}: ${emitHtmlExpressionFromChildren(prop.children, escapeHelperName)}`;
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
return `${emitPropName(prop.name)}: (${prop.code})`;
|
|
2490
|
+
});
|
|
2491
|
+
|
|
2492
|
+
if (children.length > 0) {
|
|
2493
|
+
entries.push(
|
|
2494
|
+
`children: ${
|
|
2495
|
+
emitStreamRendererFromChildren(children, escapeHelperName) ??
|
|
2496
|
+
emitHtmlExpressionFromChildren(children, escapeHelperName)
|
|
2497
|
+
}`,
|
|
2498
|
+
);
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
return `{ ${entries.join(", ")} }`;
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
function emitPropName(name: string): string {
|
|
2505
|
+
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
function isClientBoundaryPlaceholder(node: Extract<JsxNodeIr, { kind: "component" }>): boolean {
|
|
2509
|
+
return node.clientReference !== undefined && !isCompatClientReference(node);
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
function isCompatClientReference(node: Extract<JsxNodeIr, { kind: "component" }>): boolean {
|
|
2513
|
+
return node.clientReference !== undefined && /\.(?:compat)\.[cm]?[jt]sx?$/.test(node.clientReference.moduleId);
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
function clientBoundaryPlaceholder(node: Extract<JsxNodeIr, { kind: "component" }>): string {
|
|
2517
|
+
return `<!--mreact-client-boundary:${escapeHtml(node.name)}-->`;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
function allocateComponentSinkName(component: ComponentIr): string {
|
|
2521
|
+
const reservedNames = new Set([component.name, component.exportName, ...component.bindingNames]);
|
|
2522
|
+
let name = "$sink";
|
|
2523
|
+
let index = 1;
|
|
2524
|
+
|
|
2525
|
+
while (reservedNames.has(name)) {
|
|
2526
|
+
name = `$sink$${index}`;
|
|
2527
|
+
index += 1;
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
return name;
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
function allocateHelperName(ir: ModuleIr, baseName: string): string {
|
|
2534
|
+
const reservedNames = new Set<string>(ir.moduleBindingNames);
|
|
2535
|
+
|
|
2536
|
+
for (const component of ir.components) {
|
|
2537
|
+
reservedNames.add(component.name);
|
|
2538
|
+
reservedNames.add(component.exportName);
|
|
2539
|
+
|
|
2540
|
+
for (const bindingName of component.bindingNames) {
|
|
2541
|
+
reservedNames.add(bindingName);
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
let name = baseName;
|
|
2546
|
+
let index = 1;
|
|
2547
|
+
|
|
2548
|
+
while (reservedNames.has(name)) {
|
|
2549
|
+
name = `${baseName}$${index}`;
|
|
2550
|
+
index += 1;
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
return name;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
function stringLiteral(value: string): string {
|
|
2557
|
+
return JSON.stringify(value);
|
|
2558
|
+
}
|