@reckona/mreact-compiler 0.0.1
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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/diagnostics.d.ts +12 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +82 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/emit-client.d.ts +8 -0
- package/dist/emit-client.d.ts.map +1 -0
- package/dist/emit-client.js +401 -0
- package/dist/emit-client.js.map +1 -0
- package/dist/emit-compat.d.ts +11 -0
- package/dist/emit-compat.d.ts.map +1 -0
- package/dist/emit-compat.js +340 -0
- package/dist/emit-compat.js.map +1 -0
- package/dist/emit-escape-helper.d.ts +2 -0
- package/dist/emit-escape-helper.d.ts.map +1 -0
- package/dist/emit-escape-helper.js +45 -0
- package/dist/emit-escape-helper.js.map +1 -0
- package/dist/emit-server-stream.d.ts +18 -0
- package/dist/emit-server-stream.d.ts.map +1 -0
- package/dist/emit-server-stream.js +1415 -0
- package/dist/emit-server-stream.js.map +1 -0
- package/dist/emit-server.d.ts +13 -0
- package/dist/emit-server.d.ts.map +1 -0
- package/dist/emit-server.js +1103 -0
- package/dist/emit-server.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.d.ts +30 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +229 -0
- package/dist/internal.js.map +1 -0
- package/dist/ir.d.ts +115 -0
- package/dist/ir.d.ts.map +1 -0
- package/dist/ir.js +2 -0
- package/dist/ir.js.map +1 -0
- package/dist/oxc-analysis-types.d.ts +2 -0
- package/dist/oxc-analysis-types.d.ts.map +1 -0
- package/dist/oxc-analysis-types.js +2 -0
- package/dist/oxc-analysis-types.js.map +1 -0
- package/dist/oxc-await-analysis.d.ts +5 -0
- package/dist/oxc-await-analysis.d.ts.map +1 -0
- package/dist/oxc-await-analysis.js +109 -0
- package/dist/oxc-await-analysis.js.map +1 -0
- package/dist/oxc-await-ids.d.ts +3 -0
- package/dist/oxc-await-ids.d.ts.map +1 -0
- package/dist/oxc-await-ids.js +50 -0
- package/dist/oxc-await-ids.js.map +1 -0
- package/dist/oxc-await-validation.d.ts +4 -0
- package/dist/oxc-await-validation.d.ts.map +1 -0
- package/dist/oxc-await-validation.js +47 -0
- package/dist/oxc-await-validation.js.map +1 -0
- package/dist/oxc-bindings.d.ts +5 -0
- package/dist/oxc-bindings.d.ts.map +1 -0
- package/dist/oxc-bindings.js +55 -0
- package/dist/oxc-bindings.js.map +1 -0
- package/dist/oxc-body-lowering.d.ts +12 -0
- package/dist/oxc-body-lowering.d.ts.map +1 -0
- package/dist/oxc-body-lowering.js +131 -0
- package/dist/oxc-body-lowering.js.map +1 -0
- package/dist/oxc-child-analysis.d.ts +17 -0
- package/dist/oxc-child-analysis.d.ts.map +1 -0
- package/dist/oxc-child-analysis.js +292 -0
- package/dist/oxc-child-analysis.js.map +1 -0
- package/dist/oxc-code-utils.d.ts +3 -0
- package/dist/oxc-code-utils.d.ts.map +1 -0
- package/dist/oxc-code-utils.js +16 -0
- package/dist/oxc-code-utils.js.map +1 -0
- package/dist/oxc-component-detection.d.ts +22 -0
- package/dist/oxc-component-detection.d.ts.map +1 -0
- package/dist/oxc-component-detection.js +188 -0
- package/dist/oxc-component-detection.js.map +1 -0
- package/dist/oxc-component-props.d.ts +14 -0
- package/dist/oxc-component-props.d.ts.map +1 -0
- package/dist/oxc-component-props.js +92 -0
- package/dist/oxc-component-props.js.map +1 -0
- package/dist/oxc-component-references.d.ts +5 -0
- package/dist/oxc-component-references.d.ts.map +1 -0
- package/dist/oxc-component-references.js +118 -0
- package/dist/oxc-component-references.js.map +1 -0
- package/dist/oxc-dom-lowering.d.ts +2 -0
- package/dist/oxc-dom-lowering.d.ts.map +1 -0
- package/dist/oxc-dom-lowering.js +99 -0
- package/dist/oxc-dom-lowering.js.map +1 -0
- package/dist/oxc-expression-utils.d.ts +5 -0
- package/dist/oxc-expression-utils.d.ts.map +1 -0
- package/dist/oxc-expression-utils.js +31 -0
- package/dist/oxc-expression-utils.js.map +1 -0
- package/dist/oxc-jsx-attributes.d.ts +6 -0
- package/dist/oxc-jsx-attributes.d.ts.map +1 -0
- package/dist/oxc-jsx-attributes.js +88 -0
- package/dist/oxc-jsx-attributes.js.map +1 -0
- package/dist/oxc-jsx-text.d.ts +2 -0
- package/dist/oxc-jsx-text.d.ts.map +1 -0
- package/dist/oxc-jsx-text.js +50 -0
- package/dist/oxc-jsx-text.js.map +1 -0
- package/dist/oxc-nested-lowering.d.ts +8 -0
- package/dist/oxc-nested-lowering.d.ts.map +1 -0
- package/dist/oxc-nested-lowering.js +182 -0
- package/dist/oxc-nested-lowering.js.map +1 -0
- package/dist/oxc-node-utils.d.ts +8 -0
- package/dist/oxc-node-utils.d.ts.map +1 -0
- package/dist/oxc-node-utils.js +41 -0
- package/dist/oxc-node-utils.js.map +1 -0
- package/dist/oxc-raw-jsx.d.ts +3 -0
- package/dist/oxc-raw-jsx.d.ts.map +1 -0
- package/dist/oxc-raw-jsx.js +28 -0
- package/dist/oxc-raw-jsx.js.map +1 -0
- package/dist/oxc-render-values.d.ts +6 -0
- package/dist/oxc-render-values.d.ts.map +1 -0
- package/dist/oxc-render-values.js +149 -0
- package/dist/oxc-render-values.js.map +1 -0
- package/dist/oxc-runtime-emit.d.ts +4 -0
- package/dist/oxc-runtime-emit.d.ts.map +1 -0
- package/dist/oxc-runtime-emit.js +143 -0
- package/dist/oxc-runtime-emit.js.map +1 -0
- package/dist/oxc-transform.d.ts +4 -0
- package/dist/oxc-transform.d.ts.map +1 -0
- package/dist/oxc-transform.js +65 -0
- package/dist/oxc-transform.js.map +1 -0
- package/dist/oxc.d.ts +15 -0
- package/dist/oxc.d.ts.map +1 -0
- package/dist/oxc.js +250 -0
- package/dist/oxc.js.map +1 -0
- package/dist/transform.d.ts +3 -0
- package/dist/transform.d.ts.map +1 -0
- package/dist/transform.js +429 -0
- package/dist/transform.js.map +1 -0
- package/dist/types.d.ts +96 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,1103 @@
|
|
|
1
|
+
import { emitEscapeHtmlHelper } from "./emit-escape-helper.js";
|
|
2
|
+
import { escapeHtmlAttribute as escapeHtml } from "@reckona/mreact-shared/html-escape";
|
|
3
|
+
// Module-local handle to the URL-safety helper name for the current emit
|
|
4
|
+
// call. Used by deeply-nested attribute emitters to avoid threading the
|
|
5
|
+
// name through every signature. Reset at the top of `emitServer`.
|
|
6
|
+
let currentUrlSafeHelperName = "_urlAttrSafe";
|
|
7
|
+
let currentClientBoundaryHelperName;
|
|
8
|
+
// Attribute names whose value enters a navigation / script-execution
|
|
9
|
+
// context when the browser dereferences it. Kept in sync with
|
|
10
|
+
// packages/server/src/url-safety.ts.
|
|
11
|
+
const URL_ATTRIBUTE_NAMES = new Set([
|
|
12
|
+
"href",
|
|
13
|
+
"src",
|
|
14
|
+
"action",
|
|
15
|
+
"formaction",
|
|
16
|
+
"xlink:href",
|
|
17
|
+
"ping",
|
|
18
|
+
"poster",
|
|
19
|
+
"background",
|
|
20
|
+
"manifest",
|
|
21
|
+
]);
|
|
22
|
+
function isUrlAttribute(name) {
|
|
23
|
+
return URL_ATTRIBUTE_NAMES.has(name);
|
|
24
|
+
}
|
|
25
|
+
// HTML-bearing attributes (Issue 077). A static string value is always
|
|
26
|
+
// dropped; dynamic values require the `{ __html: "..." }` opt-in.
|
|
27
|
+
const DANGEROUS_HTML_ATTRIBUTE_NAMES = new Set(["srcdoc"]);
|
|
28
|
+
function isDangerousHtmlAttribute(name) {
|
|
29
|
+
return DANGEROUS_HTML_ATTRIBUTE_NAMES.has(name);
|
|
30
|
+
}
|
|
31
|
+
export function emitServer(ir, options = {}) {
|
|
32
|
+
const escapeHelperName = allocateEscapeHelperName(ir);
|
|
33
|
+
const escapeBatchHelperName = options.escape === undefined
|
|
34
|
+
? undefined
|
|
35
|
+
: allocateHelperName(ir, "_escapeHtmlBatch");
|
|
36
|
+
const contextProviderHelperName = usesContextProvider(ir)
|
|
37
|
+
? allocateHelperName(ir, "_renderContextProviderToString")
|
|
38
|
+
: undefined;
|
|
39
|
+
const contextConsumerHelperName = usesContextConsumer(ir)
|
|
40
|
+
? allocateHelperName(ir, "_renderContextConsumerToString")
|
|
41
|
+
: undefined;
|
|
42
|
+
const reactNodeRenderHelperName = usesReactNodeRender(ir)
|
|
43
|
+
? allocateHelperName(ir, "_renderReactNodeToString")
|
|
44
|
+
: undefined;
|
|
45
|
+
const clientBoundaryHelperName = usesClientBoundary(ir)
|
|
46
|
+
? allocateHelperName(ir, "_renderClientBoundary")
|
|
47
|
+
: undefined;
|
|
48
|
+
const outAccumulatorName = allocateHelperName(ir, "_out");
|
|
49
|
+
const urlSafeHelperName = allocateHelperName(ir, "_urlAttrSafe");
|
|
50
|
+
currentUrlSafeHelperName = urlSafeHelperName;
|
|
51
|
+
currentClientBoundaryHelperName = clientBoundaryHelperName;
|
|
52
|
+
const helper = emitEscapeHtmlHelper(escapeHelperName);
|
|
53
|
+
// Inline URL-scheme guard mirroring packages/server/src/url-safety.ts.
|
|
54
|
+
// Returns the original value when safe to emit and undefined when the
|
|
55
|
+
// attribute should be dropped. Inlined so compiler output stays free
|
|
56
|
+
// of cross-package runtime imports.
|
|
57
|
+
// Mirrors packages/server/src/url-safety.ts. Issue 078: in-scheme
|
|
58
|
+
// tab/CR/LF must be stripped anywhere in the value, not just at the
|
|
59
|
+
// start, to match the browser's URL parser.
|
|
60
|
+
const urlSafeHelper = [
|
|
61
|
+
`function ${urlSafeHelperName}(name, value) {`,
|
|
62
|
+
` if (typeof value !== "string") return value;`,
|
|
63
|
+
` const _canonical = value`,
|
|
64
|
+
` .replace(/^[\\x00-\\x20]+/u, "")`,
|
|
65
|
+
` .replace(/[\\t\\r\\n]/g, "");`,
|
|
66
|
+
` const _match = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(_canonical);`,
|
|
67
|
+
` if (_match === null) return value;`,
|
|
68
|
+
` const _scheme = _match[1].toLowerCase();`,
|
|
69
|
+
` if (_scheme !== "javascript" && _scheme !== "vbscript" && _scheme !== "livescript" && _scheme !== "mhtml" && _scheme !== "file" && _scheme !== "data") return value;`,
|
|
70
|
+
` if (_scheme === "data" && (name === "src" || name === "poster") && /^data:image\\//i.test(_canonical)) return value;`,
|
|
71
|
+
` return undefined;`,
|
|
72
|
+
`}`,
|
|
73
|
+
].join("\n");
|
|
74
|
+
const asyncComponentNames = collectAsyncServerComponentNames(ir.components);
|
|
75
|
+
const components = ir.components
|
|
76
|
+
.map((component) => emitComponent(component, escapeHelperName, escapeBatchHelperName, outAccumulatorName, options, asyncComponentNames, options.dynamicAttributes ?? "emit", contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName))
|
|
77
|
+
.join("\n\n");
|
|
78
|
+
// Tree-shake the URL-safety helper when it is not referenced by any
|
|
79
|
+
// component output. Same shape as the existing escapeImport check.
|
|
80
|
+
const urlSafeBlock = components.includes(urlSafeHelperName) ? urlSafeHelper : "";
|
|
81
|
+
const clientBoundaryBlock = clientBoundaryHelperName === undefined || !components.includes(clientBoundaryHelperName)
|
|
82
|
+
? ""
|
|
83
|
+
: emitClientBoundaryHelper(clientBoundaryHelperName);
|
|
84
|
+
// Emit batch escape import only when the helper is actually referenced
|
|
85
|
+
// by the generated component code (issue 048: dead-import elimination).
|
|
86
|
+
// Helper names are uniquely allocated, so a literal substring check is
|
|
87
|
+
// both correct and inexpensive.
|
|
88
|
+
const escapeImport = options.escape === undefined ||
|
|
89
|
+
escapeBatchHelperName === undefined ||
|
|
90
|
+
!components.includes(escapeBatchHelperName)
|
|
91
|
+
? ""
|
|
92
|
+
: `import { ${options.escape.batchImportName} as ${escapeBatchHelperName} } from ${stringLiteral(options.escape.batchImportSource)};`;
|
|
93
|
+
const userImports = emitUserImports(ir);
|
|
94
|
+
const contextImport = emitContextImport(contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName);
|
|
95
|
+
const moduleStatements = emitModuleStatements(ir);
|
|
96
|
+
return {
|
|
97
|
+
code: `${[userImports, escapeImport, contextImport, moduleStatements, helper, urlSafeBlock, clientBoundaryBlock].filter(Boolean).join("\n\n")}\n\n${components}\n`,
|
|
98
|
+
imports: collectContextImports(contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function emitContextImport(contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName) {
|
|
102
|
+
const specifiers = [
|
|
103
|
+
reactNodeRenderHelperName === undefined
|
|
104
|
+
? undefined
|
|
105
|
+
: `renderToString as ${reactNodeRenderHelperName}`,
|
|
106
|
+
contextProviderHelperName === undefined
|
|
107
|
+
? undefined
|
|
108
|
+
: `renderContextProviderToString as ${contextProviderHelperName}`,
|
|
109
|
+
contextConsumerHelperName === undefined
|
|
110
|
+
? undefined
|
|
111
|
+
: `renderContextConsumerToString as ${contextConsumerHelperName}`,
|
|
112
|
+
].filter((specifier) => specifier !== undefined);
|
|
113
|
+
return specifiers.length === 0
|
|
114
|
+
? ""
|
|
115
|
+
: `import { ${specifiers.join(", ")} } from "@reckona/mreact-compat";`;
|
|
116
|
+
}
|
|
117
|
+
function collectContextImports(contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName) {
|
|
118
|
+
const specifiers = [
|
|
119
|
+
reactNodeRenderHelperName === undefined ? undefined : "renderToString",
|
|
120
|
+
contextProviderHelperName === undefined
|
|
121
|
+
? undefined
|
|
122
|
+
: "renderContextProviderToString",
|
|
123
|
+
contextConsumerHelperName === undefined
|
|
124
|
+
? undefined
|
|
125
|
+
: "renderContextConsumerToString",
|
|
126
|
+
].filter((specifier) => specifier !== undefined);
|
|
127
|
+
return specifiers.length === 0
|
|
128
|
+
? []
|
|
129
|
+
: [{ source: "@reckona/mreact-compat", specifiers }];
|
|
130
|
+
}
|
|
131
|
+
function emitUserImports(ir) {
|
|
132
|
+
return ir.userImports.join("\n");
|
|
133
|
+
}
|
|
134
|
+
function emitModuleStatements(ir) {
|
|
135
|
+
return ir.moduleStatements.join("\n");
|
|
136
|
+
}
|
|
137
|
+
function emitComponent(component, escapeHelperName, escapeBatchHelperName, outAccumulatorName, options, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName) {
|
|
138
|
+
const body = component.bodyStatements.map((statement) => ` ${statement}`);
|
|
139
|
+
const parameters = component.parameters.join(", ");
|
|
140
|
+
const htmlStatements = collectHtmlStatements(component.root, outAccumulatorName, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName);
|
|
141
|
+
const markerStart = stringLiteral(`<!--mreact-h:start:${encodeURIComponent(component.name)}-->`);
|
|
142
|
+
const markerEnd = stringLiteral(`<!--mreact-h:end:${encodeURIComponent(component.name)}-->`);
|
|
143
|
+
const hydrationOpenStatements = options.serverHydration === true ? [` ${outAccumulatorName} += ${markerStart};`] : [];
|
|
144
|
+
const hydrationCloseStatements = options.serverHydration === true ? [` ${outAccumulatorName} += ${markerEnd};`] : [];
|
|
145
|
+
const functionKeyword = `${component.exportDefault === true ? "export default " : component.exported === false ? "" : "export "}${asyncComponentNames.has(component.name) ? "async " : ""}function`;
|
|
146
|
+
return [
|
|
147
|
+
`${functionKeyword} ${component.name}(${parameters}) {`,
|
|
148
|
+
...body,
|
|
149
|
+
` let ${outAccumulatorName} = "";`,
|
|
150
|
+
...hydrationOpenStatements,
|
|
151
|
+
...htmlStatements.map((statement) => ` ${statement}`),
|
|
152
|
+
...hydrationCloseStatements,
|
|
153
|
+
` return ${outAccumulatorName};`,
|
|
154
|
+
`}`,
|
|
155
|
+
].join("\n");
|
|
156
|
+
}
|
|
157
|
+
function emitHtmlExpression(node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName) {
|
|
158
|
+
const parts = collectHtmlParts(node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName);
|
|
159
|
+
if (parts.length === 0) {
|
|
160
|
+
return "\"\"";
|
|
161
|
+
}
|
|
162
|
+
return parts.join(" + ");
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Statement-list IR walker (issue 046 followup). Produces a sequence of
|
|
166
|
+
* statements that each append to a shared accumulator variable instead of
|
|
167
|
+
* a single concat expression. Used at the top of component bodies; sub
|
|
168
|
+
* callbacks (`renderContextProviderToString`, async list renderers, etc.)
|
|
169
|
+
* still use the expression form via `emitHtmlExpression`.
|
|
170
|
+
*
|
|
171
|
+
* The benefits over expression mode:
|
|
172
|
+
* - intermediate string allocations from `+ +` chains disappear
|
|
173
|
+
* - conditional branches lower to `if/else` (no ternary expression spaghetti)
|
|
174
|
+
* - sync list rendering inlines the for-loop append without an IIFE wrapper
|
|
175
|
+
* - debugger / source maps step naturally over the generated statements
|
|
176
|
+
*/
|
|
177
|
+
function collectHtmlStatements(node, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, selectedValueCode) {
|
|
178
|
+
if (node.kind === "text") {
|
|
179
|
+
const literal = escapeHtml(node.value);
|
|
180
|
+
if (literal === "") {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
return [`${outVar} += ${stringLiteral(literal)};`];
|
|
184
|
+
}
|
|
185
|
+
if (node.kind === "expr") {
|
|
186
|
+
if (node.renderMode === "html") {
|
|
187
|
+
return [`${outVar} += ${rawHtmlExpression(node.code)};`];
|
|
188
|
+
}
|
|
189
|
+
if (node.renderMode === "react-node" && reactNodeRenderHelperName !== undefined) {
|
|
190
|
+
return [`${outVar} += ${reactNodeRenderHelperName}(() => (${node.code}));`];
|
|
191
|
+
}
|
|
192
|
+
return [`${outVar} += ${escapeHelperName}(${node.code});`];
|
|
193
|
+
}
|
|
194
|
+
if (node.kind === "conditional") {
|
|
195
|
+
const whenTrueStatements = node.whenTrue.flatMap((child) => collectHtmlStatements(child, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName));
|
|
196
|
+
const whenFalseStatements = node.whenFalse.flatMap((child) => collectHtmlStatements(child, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName));
|
|
197
|
+
if (whenTrueStatements.length === 0 && whenFalseStatements.length === 0) {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
if (whenFalseStatements.length === 0) {
|
|
201
|
+
return [
|
|
202
|
+
`if (${node.conditionCode}) {`,
|
|
203
|
+
...whenTrueStatements.map((statement) => ` ${statement}`),
|
|
204
|
+
`}`,
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
if (whenTrueStatements.length === 0) {
|
|
208
|
+
return [
|
|
209
|
+
`if (!(${node.conditionCode})) {`,
|
|
210
|
+
...whenFalseStatements.map((statement) => ` ${statement}`),
|
|
211
|
+
`}`,
|
|
212
|
+
];
|
|
213
|
+
}
|
|
214
|
+
return [
|
|
215
|
+
`if (${node.conditionCode}) {`,
|
|
216
|
+
...whenTrueStatements.map((statement) => ` ${statement}`),
|
|
217
|
+
`} else {`,
|
|
218
|
+
...whenFalseStatements.map((statement) => ` ${statement}`),
|
|
219
|
+
`}`,
|
|
220
|
+
];
|
|
221
|
+
}
|
|
222
|
+
if (node.kind === "list") {
|
|
223
|
+
const isAsync = containsAsyncServerOperationInChildren(node.children, asyncComponentNames);
|
|
224
|
+
if (isAsync) {
|
|
225
|
+
// Parallel async path keeps the existing renderer + Promise.all + join
|
|
226
|
+
// form to preserve concurrent resolution semantics.
|
|
227
|
+
const parameters = node.indexName === undefined
|
|
228
|
+
? node.itemName
|
|
229
|
+
: `${node.itemName}, ${node.indexName}`;
|
|
230
|
+
const renderer = emitListRenderer(node, parameters, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName);
|
|
231
|
+
const mapped = `(${node.itemsCode}).map(${renderer})`;
|
|
232
|
+
return [`${outVar} += (await Promise.all(${mapped})).join("");`];
|
|
233
|
+
}
|
|
234
|
+
// Sync list — inline for-loop appending to the caller's accumulator.
|
|
235
|
+
// No inner IIFE wrapper and no intermediate string concat per iteration.
|
|
236
|
+
const itemBinding = `const ${node.itemName} = _arr[_i];`;
|
|
237
|
+
const indexBinding = node.indexName === undefined ? undefined : `const ${node.indexName} = _i;`;
|
|
238
|
+
const bodyStatements = node.bodyStatements ?? [];
|
|
239
|
+
const childStatements = node.children.flatMap((child) => collectHtmlStatements(child, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName));
|
|
240
|
+
return [
|
|
241
|
+
`{`,
|
|
242
|
+
` const _arr = (${node.itemsCode});`,
|
|
243
|
+
` for (let _i = 0, _len = _arr.length; _i < _len; _i++) {`,
|
|
244
|
+
` ${itemBinding}`,
|
|
245
|
+
...(indexBinding === undefined ? [] : [` ${indexBinding}`]),
|
|
246
|
+
...bodyStatements.map((statement) => ` ${statement}`),
|
|
247
|
+
...childStatements.map((statement) => ` ${statement}`),
|
|
248
|
+
` }`,
|
|
249
|
+
`}`,
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
if (node.kind === "fragment") {
|
|
253
|
+
return node.children.flatMap((child) => collectHtmlStatements(child, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName));
|
|
254
|
+
}
|
|
255
|
+
if (node.kind === "component") {
|
|
256
|
+
if (node.name === "Suspense") {
|
|
257
|
+
return [
|
|
258
|
+
`${outVar} += "<!--$-->";`,
|
|
259
|
+
...node.children.flatMap((child) => collectHtmlStatements(child, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)),
|
|
260
|
+
`${outVar} += "<!--/$-->";`,
|
|
261
|
+
];
|
|
262
|
+
}
|
|
263
|
+
if (contextProviderHelperName !== undefined && node.name.endsWith(".Provider")) {
|
|
264
|
+
// Provider helper takes a string-returning callback. Use the
|
|
265
|
+
// expression form inside the callback to preserve the existing
|
|
266
|
+
// helper contract.
|
|
267
|
+
const valueCode = findComponentPropCode(node.props, "value") ?? "undefined";
|
|
268
|
+
return [
|
|
269
|
+
`${outVar} += ${contextProviderHelperName}(${node.name}, ${valueCode}, () => ${emitHtmlExpressionFromChildren(node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)});`,
|
|
270
|
+
];
|
|
271
|
+
}
|
|
272
|
+
if (contextConsumerHelperName !== undefined && node.name.endsWith(".Consumer")) {
|
|
273
|
+
const renderProp = findComponentRenderProp(node.props, "children");
|
|
274
|
+
if (renderProp !== undefined) {
|
|
275
|
+
const valueName = renderProp.valueName ?? "_value";
|
|
276
|
+
return [
|
|
277
|
+
`${outVar} += ${contextConsumerHelperName}(${node.name}, (${valueName}) => ${emitHtmlExpressionFromChildren(renderProp.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)});`,
|
|
278
|
+
];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (isClientBoundaryPlaceholder(node)) {
|
|
282
|
+
const helperName = currentClientBoundaryHelperName;
|
|
283
|
+
if (helperName !== undefined) {
|
|
284
|
+
return [
|
|
285
|
+
`${outVar} += ${helperName}(${stringLiteral(node.name)}, ${emitPropsObject(node.props, node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)});`,
|
|
286
|
+
];
|
|
287
|
+
}
|
|
288
|
+
return [`${outVar} += ${stringLiteral(clientBoundaryPlaceholder(node))};`];
|
|
289
|
+
}
|
|
290
|
+
if (node.runtime === "compat" && reactNodeRenderHelperName !== undefined) {
|
|
291
|
+
return [
|
|
292
|
+
`${outVar} += ${reactNodeRenderHelperName}(${node.name}, ${emitPropsObject(node.props, node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)});`,
|
|
293
|
+
];
|
|
294
|
+
}
|
|
295
|
+
return [
|
|
296
|
+
`${outVar} += ${emitComponentCallExpression(node.name, emitPropsObject(node.props, node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName), asyncComponentNames)};`,
|
|
297
|
+
];
|
|
298
|
+
}
|
|
299
|
+
if (node.kind === "async-boundary") {
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
// element
|
|
303
|
+
const statements = [];
|
|
304
|
+
if (node.tagName === "textarea") {
|
|
305
|
+
statements.push(`${outVar} += ${stringLiteral("<textarea")};`);
|
|
306
|
+
for (const attributePart of collectElementAttributeParts(node.tagName, node.attributes, escapeHelperName, escapeBatchHelperName, dynamicAttributes)) {
|
|
307
|
+
statements.push(`${outVar} += ${attributePart};`);
|
|
308
|
+
}
|
|
309
|
+
statements.push(`${outVar} += ">";`);
|
|
310
|
+
for (const valuePart of collectTextareaValueParts(node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)) {
|
|
311
|
+
statements.push(`${outVar} += ${valuePart};`);
|
|
312
|
+
}
|
|
313
|
+
statements.push(`${outVar} += "</textarea>";`);
|
|
314
|
+
return statements;
|
|
315
|
+
}
|
|
316
|
+
statements.push(`${outVar} += ${stringLiteral(`<${node.tagName}`)};`);
|
|
317
|
+
for (const attributePart of collectElementAttributeParts(node.tagName, node.attributes, escapeHelperName, escapeBatchHelperName, dynamicAttributes)) {
|
|
318
|
+
statements.push(`${outVar} += ${attributePart};`);
|
|
319
|
+
}
|
|
320
|
+
const selectedAttributePart = collectOptionSelectedAttributePart(node, selectedValueCode);
|
|
321
|
+
if (selectedAttributePart !== undefined) {
|
|
322
|
+
statements.push(`${outVar} += ${selectedAttributePart};`);
|
|
323
|
+
}
|
|
324
|
+
statements.push(`${outVar} += ">";`);
|
|
325
|
+
const childrenExpression = emitBatchedSimpleChildrenExpression(node.children, escapeBatchHelperName);
|
|
326
|
+
const childSelectedValueCode = node.tagName === "select"
|
|
327
|
+
? findFormValueAttributeCode(node.attributes)
|
|
328
|
+
: undefined;
|
|
329
|
+
if (childrenExpression !== undefined && childSelectedValueCode === undefined) {
|
|
330
|
+
statements.push(`${outVar} += ${childrenExpression};`);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
for (const child of node.children) {
|
|
334
|
+
statements.push(...collectHtmlStatements(child, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, childSelectedValueCode));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
statements.push(`${outVar} += ${stringLiteral(`</${node.tagName}>`)};`);
|
|
338
|
+
return statements;
|
|
339
|
+
}
|
|
340
|
+
function collectHtmlParts(node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, selectedValueCode) {
|
|
341
|
+
if (node.kind === "text") {
|
|
342
|
+
return [stringLiteral(escapeHtml(node.value))];
|
|
343
|
+
}
|
|
344
|
+
if (node.kind === "expr") {
|
|
345
|
+
if (node.renderMode === "html") {
|
|
346
|
+
return [rawHtmlExpression(node.code)];
|
|
347
|
+
}
|
|
348
|
+
if (node.renderMode === "react-node" && reactNodeRenderHelperName !== undefined) {
|
|
349
|
+
return [`${reactNodeRenderHelperName}(() => (${node.code}))`];
|
|
350
|
+
}
|
|
351
|
+
return [`${escapeHelperName}(${node.code})`];
|
|
352
|
+
}
|
|
353
|
+
if (node.kind === "conditional") {
|
|
354
|
+
return [
|
|
355
|
+
`((${node.conditionCode}) ? ${emitHtmlExpressionFromChildren(node.whenTrue, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)} : ${emitHtmlExpressionFromChildren(node.whenFalse, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)})`,
|
|
356
|
+
];
|
|
357
|
+
}
|
|
358
|
+
if (node.kind === "list") {
|
|
359
|
+
const isAsync = containsAsyncServerOperationInChildren(node.children, asyncComponentNames);
|
|
360
|
+
if (isAsync) {
|
|
361
|
+
// Async lists rely on Promise.all() for parallel resolution; the
|
|
362
|
+
// callback allocation is amortized across `await` latency, so we keep
|
|
363
|
+
// the `.map().then(...).join("")` form.
|
|
364
|
+
const parameters = node.indexName === undefined
|
|
365
|
+
? node.itemName
|
|
366
|
+
: `${node.itemName}, ${node.indexName}`;
|
|
367
|
+
const renderer = emitListRenderer(node, parameters, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName);
|
|
368
|
+
const mapped = `(${node.itemsCode}).map(${renderer})`;
|
|
369
|
+
return [`(await Promise.all(${mapped})).join("")`];
|
|
370
|
+
}
|
|
371
|
+
// Synchronous list — imperative accumulator avoids the per-render
|
|
372
|
+
// callback allocation, the intermediate `.map()` result array, and the
|
|
373
|
+
// trailing `.join("")` call.
|
|
374
|
+
return [emitSyncListIife(node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)];
|
|
375
|
+
}
|
|
376
|
+
if (node.kind === "fragment") {
|
|
377
|
+
return node.children.flatMap((child) => collectHtmlParts(child, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName));
|
|
378
|
+
}
|
|
379
|
+
if (node.kind === "component") {
|
|
380
|
+
if (node.name === "Suspense") {
|
|
381
|
+
return [
|
|
382
|
+
stringLiteral("<!--$-->"),
|
|
383
|
+
...node.children.flatMap((child) => collectHtmlParts(child, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)),
|
|
384
|
+
stringLiteral("<!--/$-->"),
|
|
385
|
+
];
|
|
386
|
+
}
|
|
387
|
+
if (contextProviderHelperName !== undefined && node.name.endsWith(".Provider")) {
|
|
388
|
+
const valueCode = findComponentPropCode(node.props, "value") ?? "undefined";
|
|
389
|
+
return [
|
|
390
|
+
`${contextProviderHelperName}(${node.name}, ${valueCode}, () => ${emitHtmlExpressionFromChildren(node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)})`,
|
|
391
|
+
];
|
|
392
|
+
}
|
|
393
|
+
if (contextConsumerHelperName !== undefined && node.name.endsWith(".Consumer")) {
|
|
394
|
+
const renderProp = findComponentRenderProp(node.props, "children");
|
|
395
|
+
if (renderProp !== undefined) {
|
|
396
|
+
const valueName = renderProp.valueName ?? "_value";
|
|
397
|
+
return [
|
|
398
|
+
`${contextConsumerHelperName}(${node.name}, (${valueName}) => ${emitHtmlExpressionFromChildren(renderProp.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)})`,
|
|
399
|
+
];
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (isClientBoundaryPlaceholder(node)) {
|
|
403
|
+
const helperName = currentClientBoundaryHelperName;
|
|
404
|
+
if (helperName !== undefined) {
|
|
405
|
+
return [
|
|
406
|
+
`${helperName}(${stringLiteral(node.name)}, ${emitPropsObject(node.props, node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)})`,
|
|
407
|
+
];
|
|
408
|
+
}
|
|
409
|
+
return [stringLiteral(clientBoundaryPlaceholder(node))];
|
|
410
|
+
}
|
|
411
|
+
if (node.runtime === "compat" && reactNodeRenderHelperName !== undefined) {
|
|
412
|
+
return [
|
|
413
|
+
`${reactNodeRenderHelperName}(${node.name}, ${emitPropsObject(node.props, node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)})`,
|
|
414
|
+
];
|
|
415
|
+
}
|
|
416
|
+
return [
|
|
417
|
+
emitComponentCallExpression(node.name, emitPropsObject(node.props, node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName), asyncComponentNames),
|
|
418
|
+
];
|
|
419
|
+
}
|
|
420
|
+
if (node.kind === "async-boundary") {
|
|
421
|
+
return [];
|
|
422
|
+
}
|
|
423
|
+
const closeTag = `</${node.tagName}>`;
|
|
424
|
+
if (node.tagName === "textarea") {
|
|
425
|
+
return [
|
|
426
|
+
stringLiteral("<textarea"),
|
|
427
|
+
...collectElementAttributeParts(node.tagName, node.attributes, escapeHelperName, escapeBatchHelperName, dynamicAttributes),
|
|
428
|
+
stringLiteral(">"),
|
|
429
|
+
...collectTextareaValueParts(node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName),
|
|
430
|
+
stringLiteral(closeTag),
|
|
431
|
+
];
|
|
432
|
+
}
|
|
433
|
+
const childrenExpression = emitBatchedSimpleChildrenExpression(node.children, escapeBatchHelperName);
|
|
434
|
+
const childSelectedValueCode = node.tagName === "select"
|
|
435
|
+
? findFormValueAttributeCode(node.attributes)
|
|
436
|
+
: undefined;
|
|
437
|
+
const selectedAttributePart = collectOptionSelectedAttributePart(node, selectedValueCode);
|
|
438
|
+
return [
|
|
439
|
+
stringLiteral(`<${node.tagName}`),
|
|
440
|
+
...collectElementAttributeParts(node.tagName, node.attributes, escapeHelperName, escapeBatchHelperName, dynamicAttributes),
|
|
441
|
+
...(selectedAttributePart === undefined ? [] : [selectedAttributePart]),
|
|
442
|
+
stringLiteral(">"),
|
|
443
|
+
...(childrenExpression === undefined || childSelectedValueCode !== undefined
|
|
444
|
+
? node.children.flatMap((child) => collectHtmlParts(child, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, childSelectedValueCode))
|
|
445
|
+
: [childrenExpression]),
|
|
446
|
+
stringLiteral(closeTag),
|
|
447
|
+
];
|
|
448
|
+
}
|
|
449
|
+
function collectHtmlAttributeParts(tagName, attr, escapeHelperName, escapeBatchHelperName, dynamicAttributes) {
|
|
450
|
+
if (attr.kind === "event" || attr.kind === "spread-attr" || attr.name === "key") {
|
|
451
|
+
return [];
|
|
452
|
+
}
|
|
453
|
+
const htmlName = htmlAttributeNameForElement(tagName, attr.name);
|
|
454
|
+
if (attr.kind === "static-attr") {
|
|
455
|
+
// Reject literal `javascript:` / `data:` / etc. in JSX source. This
|
|
456
|
+
// never produces a runtime branch because the value is known at
|
|
457
|
+
// compile time -- we just drop the attribute (matching the dynamic
|
|
458
|
+
// path) so a developer cannot statically introduce the same XSS.
|
|
459
|
+
if (isUrlAttribute(htmlName) && isStaticUrlValueUnsafe(htmlName, attr.value)) {
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
// Issue 077: a literal string value for `srcdoc` etc. can never be
|
|
463
|
+
// the `{ __html: ... }` opt-in shape, so it is dropped at compile
|
|
464
|
+
// time.
|
|
465
|
+
if (isDangerousHtmlAttribute(htmlName)) {
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
468
|
+
return [`${stringLiteral(` ${htmlName}="${escapeHtml(attr.value)}"`)}`];
|
|
469
|
+
}
|
|
470
|
+
if (dynamicAttributes === "drop") {
|
|
471
|
+
return [];
|
|
472
|
+
}
|
|
473
|
+
if (attr.name === "style") {
|
|
474
|
+
return [emitDynamicStyleAttributeExpression(attr.code, escapeHelperName, escapeBatchHelperName)];
|
|
475
|
+
}
|
|
476
|
+
if (isDangerousHtmlAttribute(htmlName)) {
|
|
477
|
+
// Dynamic srcdoc must arrive as `{ __html: "..." }`. Drop anything
|
|
478
|
+
// else at runtime so a value computed from a loader cannot inject
|
|
479
|
+
// executable HTML into the iframe document.
|
|
480
|
+
return [
|
|
481
|
+
`(() => { const _value = (${attr.code}); if (_value == null || _value === false) return ""; if (typeof _value === "object" && _value !== null && typeof _value.__html === "string") return ${stringLiteral(` ${htmlName}="`)} + ${escapeHelperName}(_value.__html) + ${stringLiteral("\"")}; return ""; })()`,
|
|
482
|
+
];
|
|
483
|
+
}
|
|
484
|
+
return [emitDynamicAttributeExpression(htmlName, attr.code, escapeHelperName)];
|
|
485
|
+
}
|
|
486
|
+
function isStaticUrlValueUnsafe(name, value) {
|
|
487
|
+
// Same canonicalization as the runtime helper (Issue 078).
|
|
488
|
+
let start = 0;
|
|
489
|
+
while (start < value.length && value.charCodeAt(start) <= 0x20) {
|
|
490
|
+
start += 1;
|
|
491
|
+
}
|
|
492
|
+
const canonical = value.slice(start).replace(/[\t\r\n]/g, "");
|
|
493
|
+
const match = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(canonical);
|
|
494
|
+
if (match === null || match[1] === undefined)
|
|
495
|
+
return false;
|
|
496
|
+
const scheme = match[1].toLowerCase();
|
|
497
|
+
if (scheme === "javascript" || scheme === "vbscript" || scheme === "livescript" || scheme === "mhtml" || scheme === "file")
|
|
498
|
+
return true;
|
|
499
|
+
if (scheme === "data") {
|
|
500
|
+
if ((name === "src" || name === "poster") && /^data:image\//i.test(canonical))
|
|
501
|
+
return false;
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
function collectElementAttributeParts(tagName, attrs, escapeHelperName, escapeBatchHelperName, dynamicAttributes) {
|
|
507
|
+
const hasExplicitInputValue = tagName === "input" &&
|
|
508
|
+
attrs.some((attr) => attr.kind !== "spread-attr" && attr.name === "value");
|
|
509
|
+
const hasExplicitInputChecked = tagName === "input" &&
|
|
510
|
+
attrs.some((attr) => attr.kind !== "spread-attr" && attr.name === "checked");
|
|
511
|
+
return attrs.flatMap((attr) => attr.kind !== "spread-attr" &&
|
|
512
|
+
((tagName === "input" &&
|
|
513
|
+
((attr.name === "defaultValue" && hasExplicitInputValue) ||
|
|
514
|
+
(attr.name === "defaultChecked" && hasExplicitInputChecked))) ||
|
|
515
|
+
((tagName === "textarea" || tagName === "select") &&
|
|
516
|
+
(attr.name === "value" || attr.name === "defaultValue")))
|
|
517
|
+
? []
|
|
518
|
+
: collectHtmlAttributeParts(tagName, attr, escapeHelperName, escapeBatchHelperName, dynamicAttributes));
|
|
519
|
+
}
|
|
520
|
+
function emitDynamicAttributeExpression(name, code, escapeHelperName) {
|
|
521
|
+
if (isUrlAttribute(name)) {
|
|
522
|
+
// Run the value through the inline URL safety helper. The helper
|
|
523
|
+
// returns the value when safe and `undefined` when the attribute
|
|
524
|
+
// should be dropped. Using an IIFE here is necessary because we
|
|
525
|
+
// need to capture the value once and branch on the helper output.
|
|
526
|
+
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("\"")}; })()`;
|
|
527
|
+
}
|
|
528
|
+
const inlineExpr = simpleSideEffectFreeExpression(code);
|
|
529
|
+
if (inlineExpr !== undefined) {
|
|
530
|
+
// Inline 3 evaluations to avoid per-attribute IIFE closure allocation.
|
|
531
|
+
// Safe because `simpleSideEffectFreeExpression` only matches expressions
|
|
532
|
+
// whose evaluation has no observable side effects (identifier read,
|
|
533
|
+
// member chain, literal, this).
|
|
534
|
+
return `(${inlineExpr} == null || ${inlineExpr} === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(${inlineExpr} === true ? "" : ${inlineExpr}) + ${stringLiteral("\"")})`;
|
|
535
|
+
}
|
|
536
|
+
return `(() => { const _value = (${code}); return _value == null || _value === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_value === true ? "" : _value) + ${stringLiteral("\"")}; })()`;
|
|
537
|
+
}
|
|
538
|
+
function emitDynamicStyleAttributeExpression(code, escapeHelperName, escapeBatchHelperName) {
|
|
539
|
+
const staticStyleExpression = emitStaticStyleObjectAttributeExpression(code, escapeHelperName);
|
|
540
|
+
if (staticStyleExpression !== undefined) {
|
|
541
|
+
return staticStyleExpression;
|
|
542
|
+
}
|
|
543
|
+
const escapedPair = escapeBatchHelperName === undefined
|
|
544
|
+
? `${escapeHelperName}(_cssName) + ":" + ${escapeHelperName}(_styleValue === true ? "" : _styleValue)`
|
|
545
|
+
: `(() => { const _escaped = ${escapeBatchHelperName}([_cssName, _styleValue === true ? "" : _styleValue]); return _escaped[0] + ":" + _escaped[1]; })()`;
|
|
546
|
+
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("\"")}; })()`;
|
|
547
|
+
}
|
|
548
|
+
function emitStaticStyleObjectAttributeExpression(code, escapeHelperName) {
|
|
549
|
+
const entries = parseStaticStyleObjectLiteral(code);
|
|
550
|
+
if (entries === undefined) {
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
if (entries.length === 0) {
|
|
554
|
+
return `""`;
|
|
555
|
+
}
|
|
556
|
+
// Stage B — all values are compile-time literals: collapse to a single
|
|
557
|
+
// constant string. null/false entries are dropped at build time.
|
|
558
|
+
const literalEntries = entries.map((entry) => ({
|
|
559
|
+
cssName: entry.cssName,
|
|
560
|
+
literal: parseStyleLiteralValue(entry.valueCode),
|
|
561
|
+
}));
|
|
562
|
+
if (literalEntries.every((entry) => entry.literal !== undefined)) {
|
|
563
|
+
const parts = literalEntries
|
|
564
|
+
.filter((entry) => entry.literal !== null)
|
|
565
|
+
.map((entry) => `${entry.cssName}:${escapeHtml(String(entry.literal))}`);
|
|
566
|
+
if (parts.length === 0) {
|
|
567
|
+
return `""`;
|
|
568
|
+
}
|
|
569
|
+
return stringLiteral(` style="${parts.join(";")}"`);
|
|
570
|
+
}
|
|
571
|
+
// Stage A — needSep tracking with inline string accumulator, no intermediate
|
|
572
|
+
// array allocation and no `.join(";")` per render.
|
|
573
|
+
const statements = entries.map((entry) => `{ const _v = (${entry.valueCode}); if (_v != null && _v !== false) _style += (_style === "" ? "" : ";") + ${stringLiteral(`${entry.cssName}:`)} + ${escapeHelperName}(_v === true ? "" : _v); }`);
|
|
574
|
+
return `(() => { let _style = ""; ${statements.join(" ")} return _style === "" ? "" : ${stringLiteral(" style=\"")} + _style + ${stringLiteral("\"")}; })()`;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Returns the value (as JS value) if `code` is a build-time literal whose
|
|
578
|
+
* stringification is deterministic and safe to embed in style serialization.
|
|
579
|
+
* Returns `null` for compile-time `null`/`false`/`undefined` (i.e., entries
|
|
580
|
+
* that should be dropped). Returns `undefined` if the value isn't a literal.
|
|
581
|
+
*/
|
|
582
|
+
function parseStyleLiteralValue(code) {
|
|
583
|
+
const trimmed = unwrapParenthesized(code.trim());
|
|
584
|
+
if (trimmed === "null" || trimmed === "false" || trimmed === "undefined") {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
if (trimmed === "true") {
|
|
588
|
+
return "";
|
|
589
|
+
}
|
|
590
|
+
if (NUMERIC_LITERAL_RE.test(trimmed)) {
|
|
591
|
+
return Number(trimmed);
|
|
592
|
+
}
|
|
593
|
+
if (SIMPLE_STRING_LITERAL_RE.test(trimmed)) {
|
|
594
|
+
return JSON.parse(trimmed);
|
|
595
|
+
}
|
|
596
|
+
if (SIMPLE_SINGLE_QUOTE_RE.test(trimmed)) {
|
|
597
|
+
return JSON.parse(`"${trimmed.slice(1, -1).replaceAll('"', '\\"')}"`);
|
|
598
|
+
}
|
|
599
|
+
return undefined;
|
|
600
|
+
}
|
|
601
|
+
function parseStaticStyleObjectLiteral(code) {
|
|
602
|
+
const objectCode = unwrapParenthesized(code.trim());
|
|
603
|
+
if (!objectCode.startsWith("{") || !objectCode.endsWith("}")) {
|
|
604
|
+
return undefined;
|
|
605
|
+
}
|
|
606
|
+
const body = objectCode.slice(1, -1).trim();
|
|
607
|
+
if (body === "") {
|
|
608
|
+
return [];
|
|
609
|
+
}
|
|
610
|
+
const entries = [];
|
|
611
|
+
for (const property of splitTopLevel(body, ",")) {
|
|
612
|
+
const trimmed = property.trim();
|
|
613
|
+
if (trimmed === "" || trimmed.startsWith("...") || trimmed.startsWith("[")) {
|
|
614
|
+
return undefined;
|
|
615
|
+
}
|
|
616
|
+
const colonIndex = findTopLevelColon(trimmed);
|
|
617
|
+
if (colonIndex < 0) {
|
|
618
|
+
return undefined;
|
|
619
|
+
}
|
|
620
|
+
const rawKey = trimmed.slice(0, colonIndex).trim();
|
|
621
|
+
const valueCode = trimmed.slice(colonIndex + 1).trim();
|
|
622
|
+
const key = parseStaticObjectKey(rawKey);
|
|
623
|
+
if (key === undefined || valueCode === "") {
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
626
|
+
entries.push({ cssName: cssPropertyName(key), valueCode });
|
|
627
|
+
}
|
|
628
|
+
return entries;
|
|
629
|
+
}
|
|
630
|
+
// Matches an identifier or member-access chain such as `foo`, `foo.bar`, or
|
|
631
|
+
// `this.cell.row`. Computed access (`foo[i]`) is excluded because the index
|
|
632
|
+
// can itself have side effects.
|
|
633
|
+
const SIMPLE_IDENT_CHAIN_RE = /^(this|[A-Za-z_$][\w$]*)(\.[A-Za-z_$][\w$]*)*$/;
|
|
634
|
+
const NUMERIC_LITERAL_RE = /^-?(?:\d+(?:\.\d+)?|\.\d+)$/;
|
|
635
|
+
const SIMPLE_STRING_LITERAL_RE = /^"(?:[^"\\]|\\.)*"$/;
|
|
636
|
+
const SIMPLE_SINGLE_QUOTE_RE = /^'(?:[^'\\]|\\.)*'$/;
|
|
637
|
+
/**
|
|
638
|
+
* Returns the normalized source if `code` is a side-effect-free expression
|
|
639
|
+
* safe to evaluate multiple times inline, otherwise undefined.
|
|
640
|
+
*
|
|
641
|
+
* Used by attribute emit to skip the per-attribute IIFE closure allocation
|
|
642
|
+
* when the value can be re-evaluated cheaply (Identifier / MemberExpression
|
|
643
|
+
* chain / literal / `this`).
|
|
644
|
+
*/
|
|
645
|
+
function simpleSideEffectFreeExpression(code) {
|
|
646
|
+
const trimmed = unwrapParenthesized(code.trim());
|
|
647
|
+
if (trimmed === "") {
|
|
648
|
+
return undefined;
|
|
649
|
+
}
|
|
650
|
+
if (trimmed === "true" ||
|
|
651
|
+
trimmed === "false" ||
|
|
652
|
+
trimmed === "null" ||
|
|
653
|
+
trimmed === "undefined") {
|
|
654
|
+
return trimmed;
|
|
655
|
+
}
|
|
656
|
+
if (NUMERIC_LITERAL_RE.test(trimmed) ||
|
|
657
|
+
SIMPLE_STRING_LITERAL_RE.test(trimmed) ||
|
|
658
|
+
SIMPLE_SINGLE_QUOTE_RE.test(trimmed) ||
|
|
659
|
+
SIMPLE_IDENT_CHAIN_RE.test(trimmed)) {
|
|
660
|
+
return trimmed;
|
|
661
|
+
}
|
|
662
|
+
return undefined;
|
|
663
|
+
}
|
|
664
|
+
function unwrapParenthesized(code) {
|
|
665
|
+
let current = code;
|
|
666
|
+
while (current.startsWith("(") && current.endsWith(")") && findMatchingClose(current, 0) === current.length - 1) {
|
|
667
|
+
current = current.slice(1, -1).trim();
|
|
668
|
+
}
|
|
669
|
+
return current;
|
|
670
|
+
}
|
|
671
|
+
function splitTopLevel(code, separator) {
|
|
672
|
+
const parts = [];
|
|
673
|
+
let start = 0;
|
|
674
|
+
let depth = 0;
|
|
675
|
+
let quote;
|
|
676
|
+
for (let index = 0; index < code.length; index += 1) {
|
|
677
|
+
const char = code[index];
|
|
678
|
+
if (quote !== undefined) {
|
|
679
|
+
if (char === "\\") {
|
|
680
|
+
index += 1;
|
|
681
|
+
}
|
|
682
|
+
else if (char === quote) {
|
|
683
|
+
quote = undefined;
|
|
684
|
+
}
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
if (char === "\"" || char === "'" || char === "`") {
|
|
688
|
+
quote = char;
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (char === "{" || char === "[" || char === "(") {
|
|
692
|
+
depth += 1;
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
if (char === "}" || char === "]" || char === ")") {
|
|
696
|
+
depth -= 1;
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
if (depth === 0 && char === separator) {
|
|
700
|
+
parts.push(code.slice(start, index));
|
|
701
|
+
start = index + 1;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
parts.push(code.slice(start));
|
|
705
|
+
return parts;
|
|
706
|
+
}
|
|
707
|
+
function findTopLevelColon(code) {
|
|
708
|
+
return splitTopLevel(code, ":")[0]?.length ?? -1;
|
|
709
|
+
}
|
|
710
|
+
function findMatchingClose(code, openIndex) {
|
|
711
|
+
let depth = 0;
|
|
712
|
+
let quote;
|
|
713
|
+
for (let index = openIndex; index < code.length; index += 1) {
|
|
714
|
+
const char = code[index];
|
|
715
|
+
if (quote !== undefined) {
|
|
716
|
+
if (char === "\\") {
|
|
717
|
+
index += 1;
|
|
718
|
+
}
|
|
719
|
+
else if (char === quote) {
|
|
720
|
+
quote = undefined;
|
|
721
|
+
}
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
if (char === "\"" || char === "'" || char === "`") {
|
|
725
|
+
quote = char;
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
if (char === "(") {
|
|
729
|
+
depth += 1;
|
|
730
|
+
}
|
|
731
|
+
else if (char === ")") {
|
|
732
|
+
depth -= 1;
|
|
733
|
+
if (depth === 0) {
|
|
734
|
+
return index;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return -1;
|
|
739
|
+
}
|
|
740
|
+
function parseStaticObjectKey(rawKey) {
|
|
741
|
+
if (/^[A-Za-z_$][\w$-]*$/.test(rawKey)) {
|
|
742
|
+
return rawKey;
|
|
743
|
+
}
|
|
744
|
+
if ((rawKey.startsWith("\"") && rawKey.endsWith("\"")) ||
|
|
745
|
+
(rawKey.startsWith("'") && rawKey.endsWith("'"))) {
|
|
746
|
+
return rawKey.slice(1, -1);
|
|
747
|
+
}
|
|
748
|
+
return undefined;
|
|
749
|
+
}
|
|
750
|
+
function cssPropertyName(name) {
|
|
751
|
+
return name.startsWith("--")
|
|
752
|
+
? name
|
|
753
|
+
: name.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
|
|
754
|
+
}
|
|
755
|
+
function htmlAttributeName(name) {
|
|
756
|
+
return HTML_ATTRIBUTE_ALIASES[name] ?? name;
|
|
757
|
+
}
|
|
758
|
+
const HTML_ATTRIBUTE_ALIASES = {
|
|
759
|
+
acceptCharset: "accept-charset",
|
|
760
|
+
autoFocus: "autofocus",
|
|
761
|
+
autoPlay: "autoplay",
|
|
762
|
+
charSet: "charset",
|
|
763
|
+
className: "class",
|
|
764
|
+
colSpan: "colspan",
|
|
765
|
+
contentEditable: "contenteditable",
|
|
766
|
+
crossOrigin: "crossorigin",
|
|
767
|
+
encType: "enctype",
|
|
768
|
+
formAction: "formaction",
|
|
769
|
+
frameBorder: "frameborder",
|
|
770
|
+
htmlFor: "for",
|
|
771
|
+
httpEquiv: "http-equiv",
|
|
772
|
+
maxLength: "maxlength",
|
|
773
|
+
minLength: "minlength",
|
|
774
|
+
noValidate: "novalidate",
|
|
775
|
+
playsInline: "playsinline",
|
|
776
|
+
readOnly: "readonly",
|
|
777
|
+
rowSpan: "rowspan",
|
|
778
|
+
spellCheck: "spellcheck",
|
|
779
|
+
srcDoc: "srcdoc",
|
|
780
|
+
srcSet: "srcset",
|
|
781
|
+
tabIndex: "tabindex",
|
|
782
|
+
useMap: "usemap",
|
|
783
|
+
};
|
|
784
|
+
function findFormValueAttributeCode(attrs) {
|
|
785
|
+
const valueAttr = attrs.find((attr) => attr.kind !== "spread-attr" && attr.name === "value");
|
|
786
|
+
const defaultValueAttr = attrs.find((attr) => attr.kind !== "spread-attr" && attr.name === "defaultValue");
|
|
787
|
+
const attr = valueAttr ?? defaultValueAttr;
|
|
788
|
+
if (attr === undefined || attr.kind === "event" || attr.kind === "spread-attr") {
|
|
789
|
+
return undefined;
|
|
790
|
+
}
|
|
791
|
+
return attr.kind === "static-attr" ? stringLiteral(attr.value) : `(${attr.code})`;
|
|
792
|
+
}
|
|
793
|
+
function collectTextareaValueParts(node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName) {
|
|
794
|
+
const valueCode = findFormValueAttributeCode(node.attributes);
|
|
795
|
+
if (valueCode !== undefined) {
|
|
796
|
+
return [`${escapeHelperName}(${valueCode})`];
|
|
797
|
+
}
|
|
798
|
+
return node.children.flatMap((child) => collectHtmlParts(child, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName));
|
|
799
|
+
}
|
|
800
|
+
function collectOptionSelectedAttributePart(node, selectedValueCode) {
|
|
801
|
+
if (selectedValueCode === undefined || node.tagName !== "option") {
|
|
802
|
+
return undefined;
|
|
803
|
+
}
|
|
804
|
+
const optionValueCode = findOptionValueCode(node);
|
|
805
|
+
if (optionValueCode === undefined) {
|
|
806
|
+
return undefined;
|
|
807
|
+
}
|
|
808
|
+
return `(() => { const _selected = (${selectedValueCode}); return _selected == null ? "" : String(_selected) === String(${optionValueCode}) ? ${stringLiteral(' selected=""')} : ""; })()`;
|
|
809
|
+
}
|
|
810
|
+
function findOptionValueCode(node) {
|
|
811
|
+
const valueAttr = node.attributes.find((attr) => attr.kind !== "spread-attr" && attr.name === "value");
|
|
812
|
+
if (valueAttr !== undefined && valueAttr.kind !== "event" && valueAttr.kind !== "spread-attr") {
|
|
813
|
+
return valueAttr.kind === "static-attr" ? stringLiteral(valueAttr.value) : `(${valueAttr.code})`;
|
|
814
|
+
}
|
|
815
|
+
return node.children.every((child) => child.kind === "text")
|
|
816
|
+
? stringLiteral(node.children.map((child) => child.value).join(""))
|
|
817
|
+
: undefined;
|
|
818
|
+
}
|
|
819
|
+
function htmlAttributeNameForElement(tagName, name) {
|
|
820
|
+
if (tagName === "input") {
|
|
821
|
+
if (name === "defaultValue") {
|
|
822
|
+
return "value";
|
|
823
|
+
}
|
|
824
|
+
if (name === "defaultChecked") {
|
|
825
|
+
return "checked";
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return htmlAttributeName(name);
|
|
829
|
+
}
|
|
830
|
+
function rawHtmlExpression(code) {
|
|
831
|
+
return `(() => { const _value = (${code}); return Array.isArray(_value) ? _value.join("") : String(_value ?? ""); })()`;
|
|
832
|
+
}
|
|
833
|
+
function emitBatchedSimpleChildrenExpression(children, escapeBatchHelperName) {
|
|
834
|
+
if (escapeBatchHelperName === undefined) {
|
|
835
|
+
return undefined;
|
|
836
|
+
}
|
|
837
|
+
const dynamicChildren = children.filter((child) => child.kind === "expr" && child.renderMode !== "html" && child.renderMode !== "react-node");
|
|
838
|
+
if (dynamicChildren.length < 2) {
|
|
839
|
+
return undefined;
|
|
840
|
+
}
|
|
841
|
+
if (children.some((child) => child.kind !== "text" &&
|
|
842
|
+
!(child.kind === "expr" && child.renderMode !== "html" && child.renderMode !== "react-node"))) {
|
|
843
|
+
return undefined;
|
|
844
|
+
}
|
|
845
|
+
const values = dynamicChildren.map((child) => child.code);
|
|
846
|
+
let dynamicIndex = 0;
|
|
847
|
+
const pieces = children.map((child) => {
|
|
848
|
+
if (child.kind === "text") {
|
|
849
|
+
return stringLiteral(escapeHtml(child.value));
|
|
850
|
+
}
|
|
851
|
+
const index = dynamicIndex;
|
|
852
|
+
dynamicIndex += 1;
|
|
853
|
+
return `_escaped[${index}]`;
|
|
854
|
+
});
|
|
855
|
+
return `(() => { const _escaped = ${escapeBatchHelperName}([${values.join(", ")}]); return ${pieces.join(" + ")}; })()`;
|
|
856
|
+
}
|
|
857
|
+
function emitHtmlExpressionFromChildren(children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName) {
|
|
858
|
+
if (children.length === 0) {
|
|
859
|
+
return "\"\"";
|
|
860
|
+
}
|
|
861
|
+
return emitHtmlExpression({ kind: "fragment", children }, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName);
|
|
862
|
+
}
|
|
863
|
+
function emitSyncListIife(node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName) {
|
|
864
|
+
const valueExpression = emitHtmlExpressionFromChildren(node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName);
|
|
865
|
+
const itemBinding = `const ${node.itemName} = _arr[_i];`;
|
|
866
|
+
const indexBinding = node.indexName === undefined ? "" : ` const ${node.indexName} = _i;`;
|
|
867
|
+
const bodyStatements = node.bodyStatements === undefined || node.bodyStatements.length === 0
|
|
868
|
+
? ""
|
|
869
|
+
: ` ${node.bodyStatements.join(" ")}`;
|
|
870
|
+
return `(() => { let _o = ""; const _arr = (${node.itemsCode}); for (let _i = 0, _len = _arr.length; _i < _len; _i++) { ${itemBinding}${indexBinding}${bodyStatements} _o += ${valueExpression}; } return _o; })()`;
|
|
871
|
+
}
|
|
872
|
+
function emitListRenderer(node, parameters, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName) {
|
|
873
|
+
const valueExpression = emitHtmlExpressionFromChildren(node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName);
|
|
874
|
+
const asyncKeyword = containsAsyncServerOperationInChildren(node.children, asyncComponentNames)
|
|
875
|
+
? "async "
|
|
876
|
+
: "";
|
|
877
|
+
if (node.bodyStatements === undefined || node.bodyStatements.length === 0) {
|
|
878
|
+
return `${asyncKeyword}(${parameters}) => ${valueExpression}`;
|
|
879
|
+
}
|
|
880
|
+
return `${asyncKeyword}(${parameters}) => {\n${node.bodyStatements.map((statement) => ` ${statement}`).join("\n")}\n return ${valueExpression};\n }`;
|
|
881
|
+
}
|
|
882
|
+
function emitPropsObject(props, children = [], escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName) {
|
|
883
|
+
const entries = props.map((prop) => {
|
|
884
|
+
if (prop.kind === "spread-prop") {
|
|
885
|
+
return `...(${prop.code})`;
|
|
886
|
+
}
|
|
887
|
+
if (prop.kind === "render-prop") {
|
|
888
|
+
return `${emitPropName(prop.name)}: ${emitHtmlExpressionFromChildren(prop.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)}`;
|
|
889
|
+
}
|
|
890
|
+
return `${emitPropName(prop.name)}: (${prop.code})`;
|
|
891
|
+
});
|
|
892
|
+
if (children.length > 0) {
|
|
893
|
+
entries.push(`children: ${emitHtmlExpressionFromChildren(children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)}`);
|
|
894
|
+
}
|
|
895
|
+
return `{ ${entries.join(", ")} }`;
|
|
896
|
+
}
|
|
897
|
+
function emitComponentCallExpression(name, propsCode, asyncComponentNames) {
|
|
898
|
+
const call = `${name}(${propsCode})`;
|
|
899
|
+
return asyncComponentNames.has(name) ? `(await ${call})` : call;
|
|
900
|
+
}
|
|
901
|
+
function isClientBoundaryPlaceholder(node) {
|
|
902
|
+
return node.clientReference !== undefined && !isCompatClientReference(node);
|
|
903
|
+
}
|
|
904
|
+
function isCompatClientReference(node) {
|
|
905
|
+
return node.clientReference !== undefined && /\.(?:compat)\.[cm]?[jt]sx?$/.test(node.clientReference.moduleId);
|
|
906
|
+
}
|
|
907
|
+
function clientBoundaryPlaceholder(node) {
|
|
908
|
+
return `<!--mreact-client-boundary:${escapeHtml(node.name)}-->`;
|
|
909
|
+
}
|
|
910
|
+
function collectAsyncServerComponentNames(components) {
|
|
911
|
+
const names = new Set(components
|
|
912
|
+
.filter((component) => component.async === true)
|
|
913
|
+
.map((component) => component.name));
|
|
914
|
+
let changed = true;
|
|
915
|
+
while (changed) {
|
|
916
|
+
changed = false;
|
|
917
|
+
for (const component of components) {
|
|
918
|
+
if (!names.has(component.name) &&
|
|
919
|
+
containsAsyncServerOperation(component.root, names)) {
|
|
920
|
+
names.add(component.name);
|
|
921
|
+
changed = true;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return names;
|
|
926
|
+
}
|
|
927
|
+
function containsAsyncServerOperationInChildren(children, asyncComponentNames) {
|
|
928
|
+
return children.some((child) => containsAsyncServerOperation(child, asyncComponentNames));
|
|
929
|
+
}
|
|
930
|
+
function containsAsyncServerOperation(node, asyncComponentNames) {
|
|
931
|
+
if (node.kind === "async-boundary") {
|
|
932
|
+
return true;
|
|
933
|
+
}
|
|
934
|
+
if (node.kind === "component") {
|
|
935
|
+
if (node.runtime === "compat" && !isClientBoundaryPlaceholder(node)) {
|
|
936
|
+
return true;
|
|
937
|
+
}
|
|
938
|
+
return (asyncComponentNames.has(node.name) ||
|
|
939
|
+
containsAsyncServerOperationInChildren(node.children, asyncComponentNames) ||
|
|
940
|
+
node.props.some((prop) => prop.kind === "render-prop" &&
|
|
941
|
+
containsAsyncServerOperationInChildren(prop.children, asyncComponentNames)));
|
|
942
|
+
}
|
|
943
|
+
if (node.kind === "conditional") {
|
|
944
|
+
return containsAsyncServerOperationInChildren([...node.whenTrue, ...node.whenFalse], asyncComponentNames);
|
|
945
|
+
}
|
|
946
|
+
if (node.kind === "list") {
|
|
947
|
+
return containsAsyncServerOperationInChildren(node.children, asyncComponentNames);
|
|
948
|
+
}
|
|
949
|
+
if (node.kind === "element" || node.kind === "fragment") {
|
|
950
|
+
return containsAsyncServerOperationInChildren(node.children, asyncComponentNames);
|
|
951
|
+
}
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
function emitPropName(name) {
|
|
955
|
+
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
|
|
956
|
+
}
|
|
957
|
+
function allocateEscapeHelperName(ir) {
|
|
958
|
+
return allocateHelperName(ir, "_escapeHtml");
|
|
959
|
+
}
|
|
960
|
+
function allocateHelperName(ir, baseName) {
|
|
961
|
+
const reservedNames = new Set(ir.moduleBindingNames);
|
|
962
|
+
for (const component of ir.components) {
|
|
963
|
+
reservedNames.add(component.name);
|
|
964
|
+
for (const bindingName of component.bindingNames) {
|
|
965
|
+
reservedNames.add(bindingName);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
let name = baseName;
|
|
969
|
+
let index = 1;
|
|
970
|
+
while (reservedNames.has(name)) {
|
|
971
|
+
name = `${baseName}$${index}`;
|
|
972
|
+
index += 1;
|
|
973
|
+
}
|
|
974
|
+
return name;
|
|
975
|
+
}
|
|
976
|
+
function usesContextProvider(ir) {
|
|
977
|
+
return ir.components.some((component) => containsContextProvider(component.root));
|
|
978
|
+
}
|
|
979
|
+
function usesContextConsumer(ir) {
|
|
980
|
+
return ir.components.some((component) => containsContextConsumer(component.root));
|
|
981
|
+
}
|
|
982
|
+
function usesReactNodeRender(ir) {
|
|
983
|
+
return ir.components.some((component) => containsReactNodeRender(component.root));
|
|
984
|
+
}
|
|
985
|
+
function usesClientBoundary(ir) {
|
|
986
|
+
return ir.components.some((component) => containsClientBoundary(component.root));
|
|
987
|
+
}
|
|
988
|
+
function emitClientBoundaryHelper(name) {
|
|
989
|
+
return [
|
|
990
|
+
`function ${name}(name, props) {`,
|
|
991
|
+
` const _name = String(name);`,
|
|
992
|
+
` const _escapedName = _name.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");`,
|
|
993
|
+
` const _json = (JSON.stringify(props ?? {}) ?? "{}").replaceAll("<", "\\\\u003c");`,
|
|
994
|
+
` return \`<template data-mreact-client-boundary="\${_escapedName}"></template><script type="application/json" data-mreact-client-boundary-props="\${_escapedName}">\${_json}</script>\`;`,
|
|
995
|
+
`}`,
|
|
996
|
+
].join("\n");
|
|
997
|
+
}
|
|
998
|
+
function containsClientBoundary(node) {
|
|
999
|
+
if (node.kind === "component" && isClientBoundaryPlaceholder(node)) {
|
|
1000
|
+
return true;
|
|
1001
|
+
}
|
|
1002
|
+
if (node.kind === "conditional") {
|
|
1003
|
+
return [...node.whenTrue, ...node.whenFalse].some(containsClientBoundary);
|
|
1004
|
+
}
|
|
1005
|
+
if (node.kind === "list") {
|
|
1006
|
+
return node.children.some(containsClientBoundary);
|
|
1007
|
+
}
|
|
1008
|
+
if (node.kind === "fragment" || node.kind === "element") {
|
|
1009
|
+
return node.children.some(containsClientBoundary);
|
|
1010
|
+
}
|
|
1011
|
+
if (node.kind === "component") {
|
|
1012
|
+
return (node.children.some(containsClientBoundary) ||
|
|
1013
|
+
node.props.some((prop) => prop.kind === "render-prop" && prop.children.some(containsClientBoundary)));
|
|
1014
|
+
}
|
|
1015
|
+
if (node.kind === "async-boundary") {
|
|
1016
|
+
return (node.children.some(containsClientBoundary) ||
|
|
1017
|
+
node.placeholderChildren?.some(containsClientBoundary) === true ||
|
|
1018
|
+
node.catchChildren?.some(containsClientBoundary) === true);
|
|
1019
|
+
}
|
|
1020
|
+
return false;
|
|
1021
|
+
}
|
|
1022
|
+
function containsReactNodeRender(node) {
|
|
1023
|
+
if (node.kind === "expr") {
|
|
1024
|
+
return node.renderMode === "react-node";
|
|
1025
|
+
}
|
|
1026
|
+
if (node.kind === "conditional") {
|
|
1027
|
+
return [...node.whenTrue, ...node.whenFalse].some(containsReactNodeRender);
|
|
1028
|
+
}
|
|
1029
|
+
if (node.kind === "list") {
|
|
1030
|
+
return node.children.some(containsReactNodeRender);
|
|
1031
|
+
}
|
|
1032
|
+
if (node.kind === "fragment" || node.kind === "element") {
|
|
1033
|
+
return node.children.some(containsReactNodeRender);
|
|
1034
|
+
}
|
|
1035
|
+
if (node.kind === "component") {
|
|
1036
|
+
return (node.children.some(containsReactNodeRender) ||
|
|
1037
|
+
node.props.some((prop) => prop.kind === "render-prop" && prop.children.some(containsReactNodeRender)));
|
|
1038
|
+
}
|
|
1039
|
+
if (node.kind === "async-boundary") {
|
|
1040
|
+
return (node.children.some(containsReactNodeRender) ||
|
|
1041
|
+
node.placeholderChildren?.some(containsReactNodeRender) === true ||
|
|
1042
|
+
node.catchChildren?.some(containsReactNodeRender) === true);
|
|
1043
|
+
}
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
function containsContextProvider(node) {
|
|
1047
|
+
if (node.kind === "component" && node.name.endsWith(".Provider")) {
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
1050
|
+
if (node.kind === "conditional") {
|
|
1051
|
+
return [...node.whenTrue, ...node.whenFalse].some(containsContextProvider);
|
|
1052
|
+
}
|
|
1053
|
+
if (node.kind === "list") {
|
|
1054
|
+
return node.children.some(containsContextProvider);
|
|
1055
|
+
}
|
|
1056
|
+
if (node.kind === "fragment" || node.kind === "element") {
|
|
1057
|
+
return node.children.some(containsContextProvider);
|
|
1058
|
+
}
|
|
1059
|
+
if (node.kind === "component") {
|
|
1060
|
+
return (node.children.some(containsContextProvider) ||
|
|
1061
|
+
node.props.some((prop) => prop.kind === "render-prop" && prop.children.some(containsContextProvider)));
|
|
1062
|
+
}
|
|
1063
|
+
return false;
|
|
1064
|
+
}
|
|
1065
|
+
function containsContextConsumer(node) {
|
|
1066
|
+
if (node.kind === "component" && node.name.endsWith(".Consumer")) {
|
|
1067
|
+
return true;
|
|
1068
|
+
}
|
|
1069
|
+
if (node.kind === "conditional") {
|
|
1070
|
+
return [...node.whenTrue, ...node.whenFalse].some(containsContextConsumer);
|
|
1071
|
+
}
|
|
1072
|
+
if (node.kind === "list") {
|
|
1073
|
+
return node.children.some(containsContextConsumer);
|
|
1074
|
+
}
|
|
1075
|
+
if (node.kind === "fragment" || node.kind === "element") {
|
|
1076
|
+
return node.children.some(containsContextConsumer);
|
|
1077
|
+
}
|
|
1078
|
+
if (node.kind === "component") {
|
|
1079
|
+
return (node.children.some(containsContextConsumer) ||
|
|
1080
|
+
node.props.some((prop) => prop.kind === "render-prop" && prop.children.some(containsContextConsumer)));
|
|
1081
|
+
}
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
function findComponentPropCode(props, name) {
|
|
1085
|
+
for (const prop of props) {
|
|
1086
|
+
if (prop.kind === "prop" && prop.name === name) {
|
|
1087
|
+
return prop.code;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return undefined;
|
|
1091
|
+
}
|
|
1092
|
+
function findComponentRenderProp(props, name) {
|
|
1093
|
+
for (const prop of props) {
|
|
1094
|
+
if (prop.kind === "render-prop" && prop.name === name) {
|
|
1095
|
+
return prop;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return undefined;
|
|
1099
|
+
}
|
|
1100
|
+
function stringLiteral(value) {
|
|
1101
|
+
return JSON.stringify(value);
|
|
1102
|
+
}
|
|
1103
|
+
//# sourceMappingURL=emit-server.js.map
|