main
ts 197 lines 5.35 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 import type {ResolvedOptions} from './options';
9 import type {ScopeInfo} from './scope';
10 import type * as t from '@babel/types';
11
12 export interface DebugLogEntry {
13 kind: 'debug';
14 name: string;
15 value: string;
16 }
17
18 export interface BindingRenameInfo {
19 original: string;
20 renamed: string;
21 declarationStart: number;
22 }
23
24 export interface OrderedLogItem {
25 type: 'event' | 'debug';
26 event?: LoggerEvent;
27 entry?: DebugLogEntry;
28 }
29
30 export interface CompileSuccess {
31 kind: 'success';
32 ast: t.File | null;
33 events: Array<LoggerEvent>;
34 orderedLog?: Array<OrderedLogItem>;
35 renames?: Array<BindingRenameInfo>;
36 }
37
38 export interface CompileError {
39 kind: 'error';
40 error: {
41 reason: string;
42 description?: string;
43 details: Array<unknown>;
44 };
45 events: Array<LoggerEvent>;
46 orderedLog?: Array<OrderedLogItem>;
47 }
48
49 export type CompileResult = CompileSuccess | CompileError;
50
51 export type LoggerEvent = {
52 kind: string;
53 [key: string]: unknown;
54 };
55
56 // The napi-rs generated binding.
57 // This will be available once the native module is built.
58 // For now, we use a dynamic require that will be resolved at runtime.
59 let rustCompile:
60 | ((ast: string, scope: string, options: string) => string)
61 | null = null;
62
63 function getRustCompile(): (
64 ast: string,
65 scope: string,
66 options: string,
67 ) => string {
68 if (rustCompile == null) {
69 try {
70 // Try to load the native module
71 const native = require('../native');
72 rustCompile = native.compile;
73 } catch (e) {
74 throw new Error(
75 'babel-plugin-react-compiler-rust: Failed to load native module. ' +
76 'Make sure the native addon is built. Error: ' +
77 (e as Error).message,
78 );
79 }
80 }
81 return rustCompile!;
82 }
83
84 /**
85 * Encode lone surrogate escapes so they survive the Rust serde_json round-trip.
86 * JS JSON.stringify can produce \uD800-\uDFFF lone surrogates which are invalid
87 * in Rust's serde_json (expects valid UTF-8/Unicode). We encode them as recoverable
88 * markers (__SURROGATE_XXXX__) and restore them via restoreJsonSurrogates on output.
89 *
90 * Important: we must NOT replace escaped surrogate sequences like \\uD83D\\uDE80
91 * that appear in extra.raw fields (literal source text). Those have a double
92 * backslash in the JSON (the first \ escapes the second), so we use a negative
93 * lookbehind to skip them.
94 */
95 function sanitizeJsonSurrogates(json: string): string {
96 // Encode lone surrogates as recoverable markers instead of replacing with
97 // \uFFFD. This preserves the original surrogate values through the Rust
98 // round-trip. restoreJsonSurrogates reverses this on the output side.
99 return json
100 .replace(
101 /(?<!\\)\\u([dD][89aAbB][0-9a-fA-F]{2})(?!\\u[dD][c-fC-F][0-9a-fA-F]{2})/g,
102 (_, hex) => `__SURROGATE_${hex.toUpperCase()}__`,
103 )
104 .replace(
105 /(?<!\\u[dD][89aAbB][0-9a-fA-F]{2})(?<!\\)\\u([dD][c-fC-F][0-9a-fA-F]{2})/g,
106 (_, hex) => `__SURROGATE_${hex.toUpperCase()}__`,
107 );
108 }
109
110 function restoreJsonSurrogates(json: string): string {
111 return json.replace(/__SURROGATE_([0-9A-F]{4})__/g, (_, hex) => `\\u${hex}`);
112 }
113
114 export function compileWithRust(
115 ast: t.File,
116 scopeInfo: ScopeInfo,
117 options: ResolvedOptions,
118 code?: string | null,
119 ): CompileResult {
120 const compile = getRustCompile();
121
122 const optionsWithCode =
123 code != null ? {...options, __sourceCode: code} : options;
124 const resultJson = compile(
125 sanitizeJsonSurrogates(JSON.stringify(ast)),
126 JSON.stringify(scopeInfo),
127 JSON.stringify(optionsWithCode),
128 );
129
130 return JSON.parse(restoreJsonSurrogates(resultJson)) as CompileResult;
131 }
132
133 export interface TimingEntry {
134 name: string;
135 duration_us: number;
136 }
137
138 export interface BridgeTiming {
139 jsStringifyAst_us: number;
140 jsStringifyScope_us: number;
141 jsStringifyOptions_us: number;
142 napiCall_us: number;
143 jsParseResult_us: number;
144 }
145
146 export interface ProfiledCompileResult {
147 result: CompileResult;
148 bridgeTiming: BridgeTiming;
149 rustTiming: Array<TimingEntry>;
150 }
151
152 export function compileWithRustProfiled(
153 ast: t.File,
154 scopeInfo: ScopeInfo,
155 options: ResolvedOptions,
156 code?: string | null,
157 ): ProfiledCompileResult {
158 const compile = getRustCompile();
159
160 const optionsWithCode =
161 code != null
162 ? {...options, __sourceCode: code, __profiling: true}
163 : {...options, __profiling: true};
164
165 const t0 = performance.now();
166 const astJson = sanitizeJsonSurrogates(JSON.stringify(ast));
167 const t1 = performance.now();
168 const scopeJson = JSON.stringify(scopeInfo);
169 const t2 = performance.now();
170 const optionsJson = JSON.stringify(optionsWithCode);
171 const t3 = performance.now();
172
173 const resultJson = compile(astJson, scopeJson, optionsJson);
174 const t4 = performance.now();
175
176 const result = JSON.parse(
177 restoreJsonSurrogates(resultJson),
178 ) as CompileResult & {
179 timing?: Array<TimingEntry>;
180 };
181 const t5 = performance.now();
182
183 const rustTiming = result.timing ?? [];
184 delete result.timing;
185
186 return {
187 result,
188 bridgeTiming: {
189 jsStringifyAst_us: Math.round((t1 - t0) * 1000),
190 jsStringifyScope_us: Math.round((t2 - t1) * 1000),
191 jsStringifyOptions_us: Math.round((t3 - t2) * 1000),
192 napiCall_us: Math.round((t4 - t3) * 1000),
193 jsParseResult_us: Math.round((t5 - t4) * 1000),
194 },
195 rustTiming,
196 };
197 }