@reckona/mreact-compiler 0.0.66 → 0.0.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/compiler-module-context.js.map +1 -1
  2. package/dist/diagnostics.js.map +1 -1
  3. package/dist/emit-client.js.map +1 -1
  4. package/dist/emit-compat.js.map +1 -1
  5. package/dist/emit-escape-helper.js.map +1 -1
  6. package/dist/emit-server-shared.js.map +1 -1
  7. package/dist/emit-server-stream.js.map +1 -1
  8. package/dist/emit-server.js.map +1 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/internal.js.map +1 -1
  11. package/dist/ir.js.map +1 -1
  12. package/dist/oxc-analysis-types.js.map +1 -1
  13. package/dist/oxc-await-analysis.js.map +1 -1
  14. package/dist/oxc-await-ids.js.map +1 -1
  15. package/dist/oxc-await-validation.js.map +1 -1
  16. package/dist/oxc-bindings.js.map +1 -1
  17. package/dist/oxc-body-lowering.js.map +1 -1
  18. package/dist/oxc-child-analysis.js.map +1 -1
  19. package/dist/oxc-code-utils.js.map +1 -1
  20. package/dist/oxc-component-detection.js.map +1 -1
  21. package/dist/oxc-component-props.js.map +1 -1
  22. package/dist/oxc-component-references.js.map +1 -1
  23. package/dist/oxc-dom-lowering.js.map +1 -1
  24. package/dist/oxc-expression-utils.js.map +1 -1
  25. package/dist/oxc-jsx-attributes.js.map +1 -1
  26. package/dist/oxc-jsx-text.js.map +1 -1
  27. package/dist/oxc-nested-lowering.js.map +1 -1
  28. package/dist/oxc-node-utils.js.map +1 -1
  29. package/dist/oxc-raw-jsx.js.map +1 -1
  30. package/dist/oxc-render-values.js.map +1 -1
  31. package/dist/oxc-runtime-emit.js.map +1 -1
  32. package/dist/oxc-transform.js.map +1 -1
  33. package/dist/oxc.js.map +1 -1
  34. package/dist/transform.js.map +1 -1
  35. package/dist/types.js.map +1 -1
  36. package/package.json +4 -3
  37. package/src/compiler-module-context.ts +31 -0
  38. package/src/diagnostics.ts +184 -0
  39. package/src/emit-client.ts +837 -0
  40. package/src/emit-compat.ts +567 -0
  41. package/src/emit-escape-helper.ts +45 -0
  42. package/src/emit-server-shared.ts +384 -0
  43. package/src/emit-server-stream.ts +2558 -0
  44. package/src/emit-server.ts +1827 -0
  45. package/src/index.ts +44 -0
  46. package/src/internal.ts +1905 -0
  47. package/src/ir.ts +151 -0
  48. package/src/oxc-analysis-types.ts +5 -0
  49. package/src/oxc-await-analysis.ts +165 -0
  50. package/src/oxc-await-ids.ts +62 -0
  51. package/src/oxc-await-validation.ts +117 -0
  52. package/src/oxc-bindings.ts +70 -0
  53. package/src/oxc-body-lowering.ts +430 -0
  54. package/src/oxc-child-analysis.ts +791 -0
  55. package/src/oxc-code-utils.ts +19 -0
  56. package/src/oxc-component-detection.ts +459 -0
  57. package/src/oxc-component-props.ts +170 -0
  58. package/src/oxc-component-references.ts +613 -0
  59. package/src/oxc-dom-lowering.ts +127 -0
  60. package/src/oxc-expression-utils.ts +42 -0
  61. package/src/oxc-jsx-attributes.ts +110 -0
  62. package/src/oxc-jsx-text.ts +84 -0
  63. package/src/oxc-nested-lowering.ts +319 -0
  64. package/src/oxc-node-utils.ts +65 -0
  65. package/src/oxc-raw-jsx.ts +239 -0
  66. package/src/oxc-render-values.ts +620 -0
  67. package/src/oxc-runtime-emit.ts +212 -0
  68. package/src/oxc-transform.ts +77 -0
  69. package/src/oxc.ts +932 -0
  70. package/src/transform.ts +634 -0
  71. package/src/types.ts +117 -0
@@ -0,0 +1,567 @@
1
+ import type {
2
+ AttributeIr,
3
+ ComponentPropIr,
4
+ ComponentIr,
5
+ JsxElementIr,
6
+ JsxFragmentIr,
7
+ JsxNodeIr,
8
+ ModuleIr,
9
+ } from "./ir.js";
10
+ import type { RuntimeImport } from "./types.js";
11
+
12
+ export interface EmitCompatResult {
13
+ code: string;
14
+ imports: RuntimeImport[];
15
+ }
16
+
17
+ export interface EmitCompatOptions {
18
+ dev?: boolean;
19
+ }
20
+
21
+ const JSX_RUNTIME_SOURCE = "@reckona/mreact-compat/jsx-runtime";
22
+ const JSX_DEV_RUNTIME_SOURCE = "@reckona/mreact-compat/jsx-dev-runtime";
23
+
24
+ export function emitCompat(
25
+ ir: ModuleIr,
26
+ options: EmitCompatOptions = {},
27
+ ): EmitCompatResult {
28
+ if (ir.components.length === 0 && ir.moduleStatements.length === 0) {
29
+ return {
30
+ code: "",
31
+ imports: [],
32
+ };
33
+ }
34
+
35
+ const normalizedModuleStatements = normalizeCompatModuleStatements(ir.moduleStatements);
36
+ const dev = options.dev === true;
37
+ const componentImportSource = dev ? JSX_DEV_RUNTIME_SOURCE : JSX_RUNTIME_SOURCE;
38
+ const componentSpecifiers = collectComponentImportSpecifiers(ir, dev);
39
+ const helperNames = allocateHelperNames(ir, componentSpecifiers);
40
+ const importGroups = createImportGroups(
41
+ componentSpecifiers,
42
+ helperNames,
43
+ normalizedModuleStatements.importSpecifiers,
44
+ componentImportSource,
45
+ );
46
+ const imports = collectImports(importGroups);
47
+ const importLine = emitImportLines(importGroups);
48
+ const userImports = emitUserImports(ir);
49
+ const moduleStatements = emitModuleStatements(normalizedModuleStatements.statements);
50
+ const components = ir.components
51
+ .map((component) => emitComponent(component, helperNames, dev))
52
+ .join("\n\n");
53
+
54
+ return {
55
+ code: `${[importLine, userImports, moduleStatements].filter(Boolean).join("\n")}\n\n${components}\n`,
56
+ imports,
57
+ };
58
+ }
59
+
60
+ function emitUserImports(ir: ModuleIr): string {
61
+ return ir.userImports.join("\n");
62
+ }
63
+
64
+ function emitModuleStatements(statements: readonly string[]): string {
65
+ return statements.join("\n");
66
+ }
67
+
68
+ function collectComponentImportSpecifiers(ir: ModuleIr, dev: boolean): string[] {
69
+ const specifiers = new Set<string>();
70
+
71
+ for (const component of ir.components) {
72
+ visit(component.root, (node) => {
73
+ if (node.kind === "fragment") {
74
+ specifiers.add("Fragment");
75
+ }
76
+
77
+ if (node.kind === "component") {
78
+ specifiers.add(dev ? "jsxDEV" : "jsx");
79
+ }
80
+
81
+ if (node.kind === "element" || node.kind === "fragment") {
82
+ specifiers.add(dev ? "jsxDEV" : node.children.length > 1 ? "jsxs" : "jsx");
83
+ }
84
+ });
85
+ }
86
+
87
+ return Array.from(specifiers).sort();
88
+ }
89
+
90
+ interface CompatHelperNames {
91
+ Fragment?: string;
92
+ jsx?: string;
93
+ jsxDEV?: string;
94
+ jsxs?: string;
95
+ }
96
+
97
+ function allocateHelperNames(
98
+ ir: ModuleIr,
99
+ specifiers: readonly string[],
100
+ ): CompatHelperNames {
101
+ const allocator = createNameAllocator(collectReservedHelperNames(ir));
102
+ const helperNames: CompatHelperNames = {};
103
+
104
+ for (const specifier of specifiers) {
105
+ if (specifier === "Fragment") {
106
+ helperNames.Fragment = allocator("_Fragment");
107
+ continue;
108
+ }
109
+
110
+ if (specifier === "jsx") {
111
+ helperNames.jsx = allocator("_jsx");
112
+ continue;
113
+ }
114
+
115
+ if (specifier === "jsxDEV") {
116
+ helperNames.jsxDEV = allocator("_jsxDEV");
117
+ continue;
118
+ }
119
+
120
+ if (specifier === "jsxs") {
121
+ helperNames.jsxs = allocator("_jsxs");
122
+ }
123
+ }
124
+
125
+ return helperNames;
126
+ }
127
+
128
+ function collectReservedHelperNames(ir: ModuleIr): string[] {
129
+ return [
130
+ ...ir.moduleBindingNames,
131
+ ...ir.components.flatMap((component) => [
132
+ component.name,
133
+ component.exportName,
134
+ ...component.bindingNames,
135
+ ]),
136
+ ];
137
+ }
138
+
139
+ interface CompatRuntimeImportSpecifier {
140
+ importedName: string;
141
+ localName: string;
142
+ source: string;
143
+ }
144
+
145
+ interface CompatImportGroup {
146
+ source: string;
147
+ specifiers: Map<string, string>;
148
+ }
149
+
150
+ interface NormalizedModuleStatements {
151
+ statements: string[];
152
+ importSpecifiers: CompatRuntimeImportSpecifier[];
153
+ }
154
+
155
+ function normalizeCompatModuleStatements(statements: readonly string[]): NormalizedModuleStatements {
156
+ const importSpecifiers = new Map<string, CompatRuntimeImportSpecifier>();
157
+ const normalizedStatements = statements.map((statement) =>
158
+ stripCompatRuntimeImports(statement, importSpecifiers)
159
+ );
160
+
161
+ return {
162
+ statements: normalizedStatements,
163
+ importSpecifiers: Array.from(importSpecifiers.values()),
164
+ };
165
+ }
166
+
167
+ function stripCompatRuntimeImports(
168
+ statement: string,
169
+ importSpecifiers: Map<string, CompatRuntimeImportSpecifier>,
170
+ ): string {
171
+ return statement
172
+ .split("\n")
173
+ .filter((line) => {
174
+ const parsed = parseCompatRuntimeImportLine(line);
175
+
176
+ if (parsed === undefined) {
177
+ return true;
178
+ }
179
+
180
+ for (const specifier of parsed) {
181
+ importSpecifiers.set(`${specifier.importedName}:${specifier.localName}`, specifier);
182
+ }
183
+
184
+ return false;
185
+ })
186
+ .join("\n");
187
+ }
188
+
189
+ function parseCompatRuntimeImportLine(
190
+ line: string,
191
+ ): CompatRuntimeImportSpecifier[] | undefined {
192
+ const match = line.match(
193
+ /^\s*import\s+\{\s*(?<specifiers>[^}]*)\s*\}\s+from\s+["@'](?<source>@reckona\/mreact-compat\/jsx(?:-dev)?-runtime)(?:\.js)?["@'];?\s*$/,
194
+ );
195
+ const specifierText = match?.groups?.specifiers;
196
+ const source = match?.groups?.source;
197
+
198
+ if (specifierText === undefined || source === undefined) {
199
+ return undefined;
200
+ }
201
+
202
+ if (specifierText.trim() === "") {
203
+ return [];
204
+ }
205
+
206
+ return specifierText.split(",").flatMap((rawSpecifier): CompatRuntimeImportSpecifier[] => {
207
+ const specifier = rawSpecifier.trim();
208
+ const aliasMatch = specifier.match(
209
+ /^(?<importedName>Fragment|jsx|jsxDEV|jsxs)\s+as\s+(?<localName>[A-Za-z_$][\w$]*)$/,
210
+ );
211
+
212
+ if (aliasMatch?.groups !== undefined) {
213
+ const { importedName, localName } = aliasMatch.groups;
214
+
215
+ if (importedName === undefined || localName === undefined) {
216
+ return [];
217
+ }
218
+
219
+ return [{
220
+ importedName,
221
+ localName,
222
+ source,
223
+ }];
224
+ }
225
+
226
+ return /^(Fragment|jsx|jsxDEV|jsxs)$/.test(specifier)
227
+ ? [{ importedName: specifier, localName: specifier, source }]
228
+ : [];
229
+ });
230
+ }
231
+
232
+ function createImportGroups(
233
+ componentSpecifiers: readonly string[],
234
+ helperNames: CompatHelperNames,
235
+ moduleImportSpecifiers: readonly CompatRuntimeImportSpecifier[],
236
+ componentImportSource: string,
237
+ ): CompatImportGroup[] {
238
+ const groups = new Map<string, CompatImportGroup>();
239
+
240
+ for (const moduleSpecifier of moduleImportSpecifiers) {
241
+ addImportSpecifier(
242
+ groups,
243
+ moduleSpecifier.source,
244
+ moduleSpecifier.importedName,
245
+ moduleSpecifier.localName,
246
+ );
247
+ }
248
+
249
+ for (const specifier of componentSpecifiers) {
250
+ if (specifier === "Fragment") {
251
+ const localName = helperNames.Fragment ?? "_Fragment";
252
+ addImportSpecifier(groups, componentImportSource, "Fragment", localName);
253
+ continue;
254
+ }
255
+
256
+ const localName = helperNames[specifier as "jsx" | "jsxDEV" | "jsxs"] ?? `_${specifier}`;
257
+ addImportSpecifier(groups, componentImportSource, specifier, localName);
258
+ }
259
+
260
+ return Array.from(groups.values());
261
+ }
262
+
263
+ function addImportSpecifier(
264
+ groups: Map<string, CompatImportGroup>,
265
+ source: string,
266
+ importedName: string,
267
+ localName: string,
268
+ ): void {
269
+ const group = groups.get(source) ?? {
270
+ source,
271
+ specifiers: new Map<string, string>(),
272
+ };
273
+
274
+ group.specifiers.set(
275
+ `${importedName}:${localName}`,
276
+ importedName === localName ? importedName : `${importedName} as ${localName}`,
277
+ );
278
+ groups.set(source, group);
279
+ }
280
+
281
+ function collectImports(groups: readonly CompatImportGroup[]): RuntimeImport[] {
282
+ return groups.map((group) => ({
283
+ source: group.source,
284
+ specifiers: Array.from(
285
+ new Set(Array.from(group.specifiers.keys(), (key) => key.split(":")[0] as string)),
286
+ ).sort(),
287
+ }));
288
+ }
289
+
290
+ function emitImportLines(groups: readonly CompatImportGroup[]): string {
291
+ return groups
292
+ .map((group) =>
293
+ `import { ${Array.from(group.specifiers.values()).join(", ")} } from "${group.source}";`
294
+ )
295
+ .join("\n");
296
+ }
297
+
298
+ function emitComponent(
299
+ component: ComponentIr,
300
+ helperNames: CompatHelperNames,
301
+ dev: boolean,
302
+ ): string {
303
+ const body = component.bodyStatements.map((statement) => ` ${statement}`);
304
+ const parameters = component.parameters.join(", ");
305
+
306
+ return [
307
+ `${component.exportDefault === true ? "export default " : component.exported === false ? "" : "export "}function ${component.name}(${parameters}) {`,
308
+ ...body,
309
+ ` return ${emitJsxNode(component.root, helperNames, dev)};`,
310
+ `}`,
311
+ ].join("\n");
312
+ }
313
+
314
+ function emitJsxNode(
315
+ node: JsxNodeIr,
316
+ helperNames: CompatHelperNames,
317
+ dev: boolean,
318
+ ): string {
319
+ if (node.kind === "text") {
320
+ return JSON.stringify(node.value);
321
+ }
322
+
323
+ if (node.kind === "expr") {
324
+ return `(${node.code})`;
325
+ }
326
+
327
+ if (node.kind === "conditional") {
328
+ return `(${node.conditionCode}) ? ${emitCompatChildren(node.whenTrue, helperNames, dev)} : ${emitCompatChildren(node.whenFalse, helperNames, dev)}`;
329
+ }
330
+
331
+ if (node.kind === "list") {
332
+ const parameters =
333
+ node.indexName === undefined
334
+ ? node.itemName
335
+ : `${node.itemName}, ${node.indexName}`;
336
+ return `(${node.itemsCode}).map(${emitListRenderer(node, parameters, helperNames, dev)})`;
337
+ }
338
+
339
+ if (node.kind === "fragment") {
340
+ return emitJsxCall(helperNames.Fragment ?? "_Fragment", node, helperNames, dev);
341
+ }
342
+
343
+ if (node.kind === "component") {
344
+ const keyArgument =
345
+ node.keyCode === undefined ? undefined : `(${node.keyCode})`;
346
+ const props = emitComponentProps(node.props, node.children, helperNames, dev);
347
+ return dev
348
+ ? emitJsxDevCall(helperNames.jsxDEV ?? "_jsxDEV", node.name, props, keyArgument, node.children.length > 1)
349
+ : `${helperNames.jsx ?? "_jsx"}(${node.name}, ${props}${keyArgument === undefined ? "" : `, ${keyArgument}`})`;
350
+ }
351
+
352
+ if (node.kind === "async-boundary") {
353
+ return "null";
354
+ }
355
+
356
+ return emitJsxCall(JSON.stringify(node.tagName), node, helperNames, dev);
357
+ }
358
+
359
+ function emitCompatChildren(
360
+ children: JsxNodeIr[],
361
+ helperNames: CompatHelperNames,
362
+ dev: boolean,
363
+ ): string {
364
+ if (children.length === 0) {
365
+ return "null";
366
+ }
367
+
368
+ if (children.length === 1) {
369
+ return emitJsxNode(children[0] as JsxNodeIr, helperNames, dev);
370
+ }
371
+
372
+ return `[${children.map((child) => emitJsxNode(child, helperNames, dev)).join(", ")}]`;
373
+ }
374
+
375
+ function emitListRenderer(
376
+ node: Extract<JsxNodeIr, { kind: "list" }>,
377
+ parameters: string,
378
+ helperNames: CompatHelperNames,
379
+ dev: boolean,
380
+ ): string {
381
+ const valueExpression = emitCompatChildren(node.children, helperNames, dev);
382
+
383
+ if (node.bodyStatements === undefined || node.bodyStatements.length === 0) {
384
+ return `(${parameters}) => ${valueExpression}`;
385
+ }
386
+
387
+ return `(${parameters}) => {\n${node.bodyStatements.map((statement) => ` ${statement}`).join("\n")}\n return ${valueExpression};\n }`;
388
+ }
389
+
390
+ function emitJsxCall(
391
+ typeExpression: string,
392
+ node: JsxElementIr | JsxFragmentIr,
393
+ helperNames: CompatHelperNames,
394
+ dev: boolean,
395
+ ): string {
396
+ if (dev) {
397
+ const keyArgument =
398
+ node.kind === "element" && node.keyCode !== undefined
399
+ ? `(${node.keyCode})`
400
+ : undefined;
401
+ return emitJsxDevCall(
402
+ helperNames.jsxDEV ?? "_jsxDEV",
403
+ typeExpression,
404
+ emitProps(node, helperNames, dev),
405
+ keyArgument,
406
+ node.children.length > 1,
407
+ );
408
+ }
409
+
410
+ const callee =
411
+ node.children.length > 1
412
+ ? (helperNames.jsxs ?? "_jsxs")
413
+ : (helperNames.jsx ?? "_jsx");
414
+ const keyArgument =
415
+ node.kind === "element" && node.keyCode !== undefined
416
+ ? `, (${node.keyCode})`
417
+ : "";
418
+
419
+ return `${callee}(${typeExpression}, ${emitProps(node, helperNames, dev)}${keyArgument})`;
420
+ }
421
+
422
+ function emitJsxDevCall(
423
+ callee: string,
424
+ typeExpression: string,
425
+ props: string,
426
+ keyArgument: string | undefined,
427
+ isStaticChildren: boolean,
428
+ ): string {
429
+ return `${callee}(${typeExpression}, ${props}, ${keyArgument ?? "undefined"}, ${isStaticChildren}, undefined, undefined)`;
430
+ }
431
+
432
+ function emitProps(
433
+ node: JsxElementIr | JsxFragmentIr,
434
+ helperNames: CompatHelperNames,
435
+ dev: boolean,
436
+ ): string {
437
+ const entries =
438
+ node.kind === "element" ? node.attributes.map(emitAttribute) : [];
439
+ const children = emitChildren(node.children, helperNames, dev);
440
+
441
+ if (children !== undefined) {
442
+ entries.push(`children: ${children}`);
443
+ }
444
+
445
+ return `{ ${entries.join(", ")} }`;
446
+ }
447
+
448
+ function emitChildren(
449
+ children: JsxNodeIr[],
450
+ helperNames: CompatHelperNames,
451
+ dev: boolean,
452
+ ): string | undefined {
453
+ if (children.length === 0) {
454
+ return undefined;
455
+ }
456
+
457
+ if (children.length === 1) {
458
+ return emitJsxNode(children[0] as JsxNodeIr, helperNames, dev);
459
+ }
460
+
461
+ return `[${children.map((child) => emitJsxNode(child, helperNames, dev)).join(", ")}]`;
462
+ }
463
+
464
+ function emitAttribute(attr: AttributeIr): string {
465
+ if (attr.kind === "spread-attr") {
466
+ return `...(${attr.code})`;
467
+ }
468
+
469
+ if (attr.kind === "static-attr") {
470
+ return `${emitPropName(attr.name)}: ${JSON.stringify(attr.value)}`;
471
+ }
472
+
473
+ if (attr.kind === "dynamic-attr") {
474
+ return `${emitPropName(attr.name)}: (${attr.code})`;
475
+ }
476
+
477
+ return `${emitPropName(attr.name)}: ${attr.code}`;
478
+ }
479
+
480
+ function emitComponentProps(
481
+ props: ComponentPropIr[],
482
+ children: JsxNodeIr[],
483
+ helperNames: CompatHelperNames,
484
+ dev: boolean,
485
+ ): string {
486
+ const entries = props
487
+ .map((prop) => {
488
+ if (prop.kind === "spread-prop") {
489
+ return `...(${prop.code})`;
490
+ }
491
+
492
+ if (prop.kind === "render-prop") {
493
+ const renderedChildren = emitChildren(prop.children, helperNames, dev) ?? "null";
494
+ return prop.valueName === undefined
495
+ ? `${emitPropName(prop.name)}: ${renderedChildren}`
496
+ : `${emitPropName(prop.name)}: (${prop.valueName}) => ${renderedChildren}`;
497
+ }
498
+
499
+ return `${emitPropName(prop.name)}: (${prop.code})`;
500
+ })
501
+ .filter(Boolean);
502
+
503
+ if (children.length > 0) {
504
+ entries.push(`children: ${emitChildren(children, helperNames, dev) ?? "null"}`);
505
+ }
506
+
507
+ return `{ ${entries.join(", ")} }`;
508
+ }
509
+
510
+ function emitPropName(name: string): string {
511
+ return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
512
+ }
513
+
514
+ function createNameAllocator(
515
+ reservedNames: readonly string[],
516
+ ): (baseName: string) => string {
517
+ const usedNames = new Set(reservedNames);
518
+
519
+ return (baseName: string): string => {
520
+ let name = baseName;
521
+ let index = 1;
522
+
523
+ while (usedNames.has(name)) {
524
+ name = `${baseName}$${index}`;
525
+ index += 1;
526
+ }
527
+
528
+ usedNames.add(name);
529
+ return name;
530
+ };
531
+ }
532
+
533
+ function visit(node: JsxNodeIr, fn: (node: JsxNodeIr) => void): void {
534
+ fn(node);
535
+
536
+ if (node.kind === "conditional") {
537
+ for (const child of [...node.whenTrue, ...node.whenFalse]) {
538
+ visit(child, fn);
539
+ }
540
+ }
541
+
542
+ if (node.kind === "list") {
543
+ for (const child of node.children) {
544
+ visit(child, fn);
545
+ }
546
+ }
547
+
548
+ if (node.kind === "component") {
549
+ for (const prop of node.props) {
550
+ if (prop.kind === "render-prop") {
551
+ for (const child of prop.children) {
552
+ visit(child, fn);
553
+ }
554
+ }
555
+ }
556
+
557
+ for (const child of node.children) {
558
+ visit(child, fn);
559
+ }
560
+ }
561
+
562
+ if (node.kind === "element" || node.kind === "fragment") {
563
+ for (const child of node.children) {
564
+ visit(child, fn);
565
+ }
566
+ }
567
+ }
@@ -0,0 +1,45 @@
1
+ // Shared text for the `_escapeHtml` helper emitted into compiled
2
+ // server-mode JS (both `emit-server.ts` and `emit-server-stream.ts`).
3
+ //
4
+ // Issue 088: the previous chain of four `String.prototype.replaceAll`
5
+ // calls scanned the entire input four times even when no escape
6
+ // character was present. On the hot path (numeric / short strings
7
+ // passed through `_escapeHtml`) it cost ~115 ns per call and
8
+ // dominated the per-request budget at N=5000 (≈58 % of total).
9
+ //
10
+ // The replacement is the well-known `escape-html` library shape that
11
+ // Marko's compiled output uses: one regex test, return the input
12
+ // unchanged when nothing needs escaping, otherwise a forward char-scan
13
+ // from the first match. On the same inputs it costs 4-11 ns per call.
14
+ //
15
+ // We only escape `& < > "` -- matching the previous implementation
16
+ // exactly. `'` is intentionally not escaped because every attribute
17
+ // the compiler emits uses double quotes, so a single quote inside an
18
+ // attribute value is safe.
19
+
20
+ export function emitEscapeHtmlHelper(name: string): string {
21
+ return [
22
+ `function ${name}(value) {`,
23
+ ` const _str = "" + (value ?? "");`,
24
+ ` const _match = /["&<>]/.exec(_str);`,
25
+ ` if (_match === null) return _str;`,
26
+ ` let _html = "";`,
27
+ ` let _last = 0;`,
28
+ ` let _i = _match.index;`,
29
+ ` for (; _i < _str.length; _i++) {`,
30
+ ` let _esc;`,
31
+ ` switch (_str.charCodeAt(_i)) {`,
32
+ ` case 34: _esc = "&quot;"; break;`,
33
+ ` case 38: _esc = "&amp;"; break;`,
34
+ ` case 60: _esc = "&lt;"; break;`,
35
+ ` case 62: _esc = "&gt;"; break;`,
36
+ ` default: continue;`,
37
+ ` }`,
38
+ ` if (_last !== _i) _html += _str.substring(_last, _i);`,
39
+ ` _last = _i + 1;`,
40
+ ` _html += _esc;`,
41
+ ` }`,
42
+ ` return _last !== _i ? _html + _str.substring(_last, _i) : _html;`,
43
+ `}`,
44
+ ].join("\n");
45
+ }