main
js 250 lines 5.81 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 'use strict';
9
10 const fs = require('fs');
11 const HermesParser = require('hermes-parser');
12 const BabelParser = require('@babel/parser');
13 const BabelCore = require('@babel/core');
14 const invariant = require('invariant');
15 const {argv, stdin} = require('process');
16 const prettier = require('prettier');
17 const {JSXText} = require('hermes-parser/dist/generated/ESTreeVisitorKeys');
18
19 function runPlugin(text, file, language) {
20 let ast;
21 if (language === 'flow') {
22 ast = HermesParser.parse(text, {
23 babel: true,
24 flow: 'all',
25 sourceFilename: file,
26 sourceType: 'module',
27 enableExperimentalComponentSyntax: true,
28 });
29 } else {
30 ast = BabelParser.parse(text, {
31 sourceFilename: file,
32 plugins: ['typescript', 'jsx'],
33 sourceType: 'module',
34 });
35 }
36 const result = BabelCore.transformFromAstSync(ast, text, {
37 ast: false,
38 filename: file,
39 highlightCode: false,
40 retainLines: true,
41 plugins: [[AnonymizePlugin]],
42 sourceType: 'module',
43 configFile: false,
44 babelrc: false,
45 });
46 invariant(
47 result?.code != null,
48 `Expected BabelPluginReactForget to codegen successfully, got: ${result}`
49 );
50 return result.code;
51 }
52
53 async function format(code, language) {
54 return await prettier.format(code, {
55 semi: true,
56 parser: language === 'typescript' ? 'babel-ts' : 'flow',
57 });
58 }
59
60 const TAG_NAMES = new Set([
61 'a',
62 'body',
63 'button',
64 'div',
65 'form',
66 'head',
67 'html',
68 'input',
69 'label',
70 'select',
71 'span',
72 'textarea',
73
74 // property/attribute names
75 'value',
76 'checked',
77 'onClick',
78 'onSubmit',
79 'name',
80 ]);
81
82 const BUILTIN_HOOKS = new Set([
83 'useContext',
84 'useEffect',
85 'useInsertionEffect',
86 'useLayoutEffect',
87 'useReducer',
88 'useState',
89 ]);
90
91 const GLOBALS = new Set([
92 'String',
93 'Object',
94 'Function',
95 'Number',
96 'RegExp',
97 'Date',
98 'Error',
99 'Function',
100 'TypeError',
101 'RangeError',
102 'ReferenceError',
103 'SyntaxError',
104 'URIError',
105 'EvalError',
106 'Boolean',
107 'DataView',
108 'Float32Array',
109 'Float64Array',
110 'Int8Array',
111 'Int16Array',
112 'Int32Array',
113 'Map',
114 'Set',
115 'WeakMap',
116 'Uint8Array',
117 'Uint8ClampedArray',
118 'Uint16Array',
119 'Uint32Array',
120 'ArrayBuffer',
121 'JSON',
122 'parseFloat',
123 'parseInt',
124 'console',
125 'isNaN',
126 'eval',
127 'isFinite',
128 'encodeURI',
129 'decodeURI',
130 'encodeURIComponent',
131 'decodeURIComponent',
132
133 // common method/property names of globals
134 'map',
135 'push',
136 'at',
137 'filter',
138 'slice',
139 'splice',
140 'add',
141 'get',
142 'set',
143 'has',
144 'size',
145 'length',
146 'toString',
147 ]);
148
149 function AnonymizePlugin(_babel) {
150 let index = 0;
151 const identifiers = new Map();
152 const literals = new Map();
153 return {
154 name: 'anonymize',
155 visitor: {
156 JSXNamespacedName(path) {
157 throw error('TODO: handle JSXNamedspacedName');
158 },
159 JSXIdentifier(path) {
160 const name = path.node.name;
161 if (TAG_NAMES.has(name)) {
162 return;
163 }
164 let nextName = identifiers.get(name);
165 if (nextName == null) {
166 const isCapitalized =
167 name.slice(0, 1).toUpperCase() === name.slice(0, 1);
168 nextName = isCapitalized
169 ? `Component${(index++).toString(16).toUpperCase()}`
170 : `c${(index++).toString(16)}`;
171 identifiers.set(name, nextName);
172 }
173 path.node.name = nextName;
174 },
175 Identifier(path) {
176 const name = path.node.name;
177 if (BUILTIN_HOOKS.has(name) || GLOBALS.has(name)) {
178 return;
179 }
180 let nextName = identifiers.get(name);
181 if (nextName == null) {
182 const isCapitalized =
183 name.slice(0, 1).toUpperCase() === name.slice(0, 1);
184 const prefix = isCapitalized ? 'V' : 'v';
185 nextName = `${prefix}${(index++).toString(16)}`;
186 if (name.startsWith('use')) {
187 nextName =
188 'use' + nextName.slice(0, 1).toUpperCase() + nextName.slice(1);
189 }
190 identifiers.set(name, nextName);
191 }
192 path.node.name = nextName;
193 },
194 JSXText(path) {
195 const value = path.node.value;
196 let nextValue = literals.get(value);
197 if (nextValue == null) {
198 let string = '';
199 while (string.length < value.length) {
200 string += String.fromCharCode(Math.round(Math.random() * 25) + 97);
201 }
202 nextValue = string;
203 literals.set(value, nextValue);
204 }
205 path.node.value = nextValue;
206 },
207 StringLiteral(path) {
208 const value = path.node.value;
209 let nextValue = literals.get(value);
210 if (nextValue == null) {
211 let string = '';
212 while (string.length < value.length) {
213 string += String.fromCharCode(Math.round(Math.random() * 58) + 65);
214 }
215 nextValue = string;
216 literals.set(value, nextValue);
217 }
218 path.node.value = nextValue;
219 },
220 NumericLiteral(path) {
221 const value = path.node.value;
222 let nextValue = literals.get(value);
223 if (nextValue == null) {
224 nextValue = Number.isInteger(value)
225 ? Math.round(Math.random() * Number.MAX_SAFE_INTEGER)
226 : Math.random() * Number.MAX_VALUE;
227 literals.set(value, nextValue);
228 }
229 path.node.value = nextValue;
230 },
231 },
232 };
233 }
234
235 let file;
236 let text;
237 if (argv.length >= 3) {
238 file = argv[2];
239 text = fs.readFileSync(file, 'utf8');
240 } else {
241 // read from stdin
242 file = 'stdin.js';
243 text = fs.readFileSync(stdin.fd, 'utf8');
244 }
245 const language =
246 file.endsWith('.ts') || file.endsWith('.tsx') ? 'typescript' : 'flow';
247 const result = runPlugin(text, file, language);
248 format(result, language).then(formatted => {
249 console.log(formatted);
250 });