Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/lib/mangle/index.ts
3520 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import v8 from 'node:v8';
7
import fs from 'fs';
8
import path from 'path';
9
import { argv } from 'process';
10
import { Mapping, SourceMapGenerator } from 'source-map';
11
import ts from 'typescript';
12
import { pathToFileURL } from 'url';
13
import workerpool from 'workerpool';
14
import { StaticLanguageServiceHost } from './staticLanguageServiceHost';
15
const buildfile = require('../../buildfile');
16
17
class ShortIdent {
18
19
private static _keywords = new Set(['await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
20
'default', 'delete', 'do', 'else', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if',
21
'import', 'in', 'instanceof', 'let', 'new', 'null', 'return', 'static', 'super', 'switch', 'this', 'throw',
22
'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield']);
23
24
private static _alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890$_'.split('');
25
26
private _value = 0;
27
28
constructor(
29
private readonly prefix: string
30
) { }
31
32
next(isNameTaken?: (name: string) => boolean): string {
33
const candidate = this.prefix + ShortIdent.convert(this._value);
34
this._value++;
35
if (ShortIdent._keywords.has(candidate) || /^[_0-9]/.test(candidate) || isNameTaken?.(candidate)) {
36
// try again
37
return this.next(isNameTaken);
38
}
39
return candidate;
40
}
41
42
private static convert(n: number): string {
43
const base = this._alphabet.length;
44
let result = '';
45
do {
46
const rest = n % base;
47
result += this._alphabet[rest];
48
n = (n / base) | 0;
49
} while (n > 0);
50
return result;
51
}
52
}
53
54
const enum FieldType {
55
Public,
56
Protected,
57
Private
58
}
59
60
class ClassData {
61
62
fields = new Map<string, { type: FieldType; pos: number }>();
63
64
private replacements: Map<string, string> | undefined;
65
66
parent: ClassData | undefined;
67
children: ClassData[] | undefined;
68
69
constructor(
70
readonly fileName: string,
71
readonly node: ts.ClassDeclaration | ts.ClassExpression,
72
) {
73
// analyse all fields (properties and methods). Find usages of all protected and
74
// private ones and keep track of all public ones (to prevent naming collisions)
75
76
const candidates: (ts.NamedDeclaration)[] = [];
77
for (const member of node.members) {
78
if (ts.isMethodDeclaration(member)) {
79
// method `foo() {}`
80
candidates.push(member);
81
82
} else if (ts.isPropertyDeclaration(member)) {
83
// property `foo = 234`
84
candidates.push(member);
85
86
} else if (ts.isGetAccessor(member)) {
87
// getter: `get foo() { ... }`
88
candidates.push(member);
89
90
} else if (ts.isSetAccessor(member)) {
91
// setter: `set foo() { ... }`
92
candidates.push(member);
93
94
} else if (ts.isConstructorDeclaration(member)) {
95
// constructor-prop:`constructor(private foo) {}`
96
for (const param of member.parameters) {
97
if (hasModifier(param, ts.SyntaxKind.PrivateKeyword)
98
|| hasModifier(param, ts.SyntaxKind.ProtectedKeyword)
99
|| hasModifier(param, ts.SyntaxKind.PublicKeyword)
100
|| hasModifier(param, ts.SyntaxKind.ReadonlyKeyword)
101
) {
102
candidates.push(param);
103
}
104
}
105
}
106
}
107
for (const member of candidates) {
108
const ident = ClassData._getMemberName(member);
109
if (!ident) {
110
continue;
111
}
112
const type = ClassData._getFieldType(member);
113
this.fields.set(ident, { type, pos: member.name!.getStart() });
114
}
115
}
116
117
private static _getMemberName(node: ts.NamedDeclaration): string | undefined {
118
if (!node.name) {
119
return undefined;
120
}
121
const { name } = node;
122
let ident = name.getText();
123
if (name.kind === ts.SyntaxKind.ComputedPropertyName) {
124
if (name.expression.kind !== ts.SyntaxKind.StringLiteral) {
125
// unsupported: [Symbol.foo] or [abc + 'field']
126
return;
127
}
128
// ['foo']
129
ident = name.expression.getText().slice(1, -1);
130
}
131
132
return ident;
133
}
134
135
private static _getFieldType(node: ts.Node): FieldType {
136
if (hasModifier(node, ts.SyntaxKind.PrivateKeyword)) {
137
return FieldType.Private;
138
} else if (hasModifier(node, ts.SyntaxKind.ProtectedKeyword)) {
139
return FieldType.Protected;
140
} else {
141
return FieldType.Public;
142
}
143
}
144
145
static _shouldMangle(type: FieldType): boolean {
146
return type === FieldType.Private
147
|| type === FieldType.Protected
148
;
149
}
150
151
static makeImplicitPublicActuallyPublic(data: ClassData, reportViolation: (name: string, what: string, why: string) => void): void {
152
// TS-HACK
153
// A subtype can make an inherited protected field public. To prevent accidential
154
// mangling of public fields we mark the original (protected) fields as public...
155
for (const [name, info] of data.fields) {
156
if (info.type !== FieldType.Public) {
157
continue;
158
}
159
let parent: ClassData | undefined = data.parent;
160
while (parent) {
161
if (parent.fields.get(name)?.type === FieldType.Protected) {
162
const parentPos = parent.node.getSourceFile().getLineAndCharacterOfPosition(parent.fields.get(name)!.pos);
163
const infoPos = data.node.getSourceFile().getLineAndCharacterOfPosition(info.pos);
164
reportViolation(name, `'${name}' from ${parent.fileName}:${parentPos.line + 1}`, `${data.fileName}:${infoPos.line + 1}`);
165
166
parent.fields.get(name)!.type = FieldType.Public;
167
}
168
parent = parent.parent;
169
}
170
}
171
}
172
173
static fillInReplacement(data: ClassData) {
174
175
if (data.replacements) {
176
// already done
177
return;
178
}
179
180
// fill in parents first
181
if (data.parent) {
182
ClassData.fillInReplacement(data.parent);
183
}
184
185
data.replacements = new Map();
186
187
const isNameTaken = (name: string) => {
188
// locally taken
189
if (data._isNameTaken(name)) {
190
return true;
191
}
192
193
// parents
194
let parent: ClassData | undefined = data.parent;
195
while (parent) {
196
if (parent._isNameTaken(name)) {
197
return true;
198
}
199
parent = parent.parent;
200
}
201
202
// children
203
if (data.children) {
204
const stack = [...data.children];
205
while (stack.length) {
206
const node = stack.pop()!;
207
if (node._isNameTaken(name)) {
208
return true;
209
}
210
if (node.children) {
211
stack.push(...node.children);
212
}
213
}
214
}
215
216
return false;
217
};
218
const identPool = new ShortIdent('');
219
220
for (const [name, info] of data.fields) {
221
if (ClassData._shouldMangle(info.type)) {
222
const shortName = identPool.next(isNameTaken);
223
data.replacements.set(name, shortName);
224
}
225
}
226
}
227
228
// a name is taken when a field that doesn't get mangled exists or
229
// when the name is already in use for replacement
230
private _isNameTaken(name: string) {
231
if (this.fields.has(name) && !ClassData._shouldMangle(this.fields.get(name)!.type)) {
232
// public field
233
return true;
234
}
235
if (this.replacements) {
236
for (const shortName of this.replacements.values()) {
237
if (shortName === name) {
238
// replaced already (happens wih super types)
239
return true;
240
}
241
}
242
}
243
244
if (isNameTakenInFile(this.node, name)) {
245
return true;
246
}
247
248
return false;
249
}
250
251
lookupShortName(name: string): string {
252
let value = this.replacements!.get(name)!;
253
let parent = this.parent;
254
while (parent) {
255
if (parent.replacements!.has(name) && parent.fields.get(name)?.type === FieldType.Protected) {
256
value = parent.replacements!.get(name)! ?? value;
257
}
258
parent = parent.parent;
259
}
260
return value;
261
}
262
263
// --- parent chaining
264
265
addChild(child: ClassData) {
266
this.children ??= [];
267
this.children.push(child);
268
child.parent = this;
269
}
270
}
271
272
function isNameTakenInFile(node: ts.Node, name: string): boolean {
273
const identifiers = (<any>node.getSourceFile()).identifiers;
274
if (identifiers instanceof Map) {
275
if (identifiers.has(name)) {
276
return true;
277
}
278
}
279
return false;
280
}
281
282
const skippedExportMangledFiles = [
283
284
// Monaco
285
'editorCommon',
286
'editorOptions',
287
'editorZoom',
288
'standaloneEditor',
289
'standaloneEnums',
290
'standaloneLanguages',
291
292
// Generated
293
'extensionsApiProposals',
294
295
// Module passed around as type
296
'pfs',
297
298
// entry points
299
...[
300
buildfile.workerEditor,
301
buildfile.workerExtensionHost,
302
buildfile.workerNotebook,
303
buildfile.workerLanguageDetection,
304
buildfile.workerLocalFileSearch,
305
buildfile.workerProfileAnalysis,
306
buildfile.workerOutputLinks,
307
buildfile.workerBackgroundTokenization,
308
buildfile.workbenchDesktop,
309
buildfile.workbenchWeb,
310
buildfile.code,
311
buildfile.codeWeb
312
].flat().map(x => x.name),
313
];
314
315
const skippedExportMangledProjects = [
316
// Test projects
317
'vscode-api-tests',
318
319
// These projects use webpack to dynamically rewrite imports, which messes up our mangling
320
'configuration-editing',
321
'microsoft-authentication',
322
'github-authentication',
323
'html-language-features/server',
324
];
325
326
const skippedExportMangledSymbols = [
327
// Don't mangle extension entry points
328
'activate',
329
'deactivate',
330
];
331
332
class DeclarationData {
333
334
readonly replacementName: string;
335
336
constructor(
337
readonly fileName: string,
338
readonly node: ts.FunctionDeclaration | ts.ClassDeclaration | ts.EnumDeclaration | ts.VariableDeclaration,
339
fileIdents: ShortIdent,
340
) {
341
// Todo: generate replacement names based on usage count, with more used names getting shorter identifiers
342
this.replacementName = fileIdents.next();
343
}
344
345
getLocations(service: ts.LanguageService): Iterable<{ fileName: string; offset: number }> {
346
if (ts.isVariableDeclaration(this.node)) {
347
// If the const aliases any types, we need to rename those too
348
const definitionResult = service.getDefinitionAndBoundSpan(this.fileName, this.node.name.getStart());
349
if (definitionResult?.definitions && definitionResult.definitions.length > 1) {
350
return definitionResult.definitions.map(x => ({ fileName: x.fileName, offset: x.textSpan.start }));
351
}
352
}
353
354
return [{
355
fileName: this.fileName,
356
offset: this.node.name!.getStart()
357
}];
358
}
359
360
shouldMangle(newName: string): boolean {
361
const currentName = this.node.name!.getText();
362
if (currentName.startsWith('$') || skippedExportMangledSymbols.includes(currentName)) {
363
return false;
364
}
365
366
// New name is longer the existing one :'(
367
if (newName.length >= currentName.length) {
368
return false;
369
}
370
371
// Don't mangle functions we've explicitly opted out
372
if (this.node.getFullText().includes('@skipMangle')) {
373
return false;
374
}
375
376
return true;
377
}
378
}
379
380
export interface MangleOutput {
381
out: string;
382
sourceMap?: string;
383
}
384
385
/**
386
* TypeScript2TypeScript transformer that mangles all private and protected fields
387
*
388
* 1. Collect all class fields (properties, methods)
389
* 2. Collect all sub and super-type relations between classes
390
* 3. Compute replacement names for each field
391
* 4. Lookup rename locations for these fields
392
* 5. Prepare and apply edits
393
*/
394
export class Mangler {
395
396
private readonly allClassDataByKey = new Map<string, ClassData>();
397
private readonly allExportedSymbols = new Set<DeclarationData>();
398
399
private readonly renameWorkerPool: workerpool.WorkerPool;
400
401
constructor(
402
private readonly projectPath: string,
403
private readonly log: typeof console.log = () => { },
404
private readonly config: { readonly manglePrivateFields: boolean; readonly mangleExports: boolean },
405
) {
406
407
this.renameWorkerPool = workerpool.pool(path.join(__dirname, 'renameWorker.js'), {
408
maxWorkers: 4,
409
minWorkers: 'max'
410
});
411
}
412
413
async computeNewFileContents(strictImplicitPublicHandling?: Set<string>): Promise<Map<string, MangleOutput>> {
414
415
const service = ts.createLanguageService(new StaticLanguageServiceHost(this.projectPath));
416
417
// STEP:
418
// - Find all classes and their field info.
419
// - Find exported symbols.
420
421
const fileIdents = new ShortIdent('$');
422
423
const visit = (node: ts.Node): void => {
424
if (this.config.manglePrivateFields) {
425
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
426
const anchor = node.name ?? node;
427
const key = `${node.getSourceFile().fileName}|${anchor.getStart()}`;
428
if (this.allClassDataByKey.has(key)) {
429
throw new Error('DUPE?');
430
}
431
this.allClassDataByKey.set(key, new ClassData(node.getSourceFile().fileName, node));
432
}
433
}
434
435
if (this.config.mangleExports) {
436
// Find exported classes, functions, and vars
437
if (
438
(
439
// Exported class
440
ts.isClassDeclaration(node)
441
&& hasModifier(node, ts.SyntaxKind.ExportKeyword)
442
&& node.name
443
) || (
444
// Exported function
445
ts.isFunctionDeclaration(node)
446
&& ts.isSourceFile(node.parent)
447
&& hasModifier(node, ts.SyntaxKind.ExportKeyword)
448
&& node.name && node.body // On named function and not on the overload
449
) || (
450
// Exported variable
451
ts.isVariableDeclaration(node)
452
&& hasModifier(node.parent.parent, ts.SyntaxKind.ExportKeyword) // Variable statement is exported
453
&& ts.isSourceFile(node.parent.parent.parent)
454
)
455
456
// Disabled for now because we need to figure out how to handle
457
// enums that are used in monaco or extHost interfaces.
458
/* || (
459
// Exported enum
460
ts.isEnumDeclaration(node)
461
&& ts.isSourceFile(node.parent)
462
&& hasModifier(node, ts.SyntaxKind.ExportKeyword)
463
&& !hasModifier(node, ts.SyntaxKind.ConstKeyword) // Don't bother mangling const enums because these are inlined
464
&& node.name
465
*/
466
) {
467
if (isInAmbientContext(node)) {
468
return;
469
}
470
471
this.allExportedSymbols.add(new DeclarationData(node.getSourceFile().fileName, node, fileIdents));
472
}
473
}
474
475
ts.forEachChild(node, visit);
476
};
477
478
for (const file of service.getProgram()!.getSourceFiles()) {
479
if (!file.isDeclarationFile) {
480
ts.forEachChild(file, visit);
481
}
482
}
483
this.log(`Done collecting. Classes: ${this.allClassDataByKey.size}. Exported symbols: ${this.allExportedSymbols.size}`);
484
485
486
// STEP: connect sub and super-types
487
488
const setupParents = (data: ClassData) => {
489
const extendsClause = data.node.heritageClauses?.find(h => h.token === ts.SyntaxKind.ExtendsKeyword);
490
if (!extendsClause) {
491
// no EXTENDS-clause
492
return;
493
}
494
495
const info = service.getDefinitionAtPosition(data.fileName, extendsClause.types[0].expression.getEnd());
496
if (!info || info.length === 0) {
497
// throw new Error('SUPER type not found');
498
return;
499
}
500
501
if (info.length !== 1) {
502
// inherits from declared/library type
503
return;
504
}
505
506
const [definition] = info;
507
const key = `${definition.fileName}|${definition.textSpan.start}`;
508
const parent = this.allClassDataByKey.get(key);
509
if (!parent) {
510
// throw new Error(`SUPER type not found: ${key}`);
511
return;
512
}
513
parent.addChild(data);
514
};
515
for (const data of this.allClassDataByKey.values()) {
516
setupParents(data);
517
}
518
519
// STEP: make implicit public (actually protected) field really public
520
const violations = new Map<string, string[]>();
521
let violationsCauseFailure = false;
522
for (const data of this.allClassDataByKey.values()) {
523
ClassData.makeImplicitPublicActuallyPublic(data, (name: string, what, why) => {
524
const arr = violations.get(what);
525
if (arr) {
526
arr.push(why);
527
} else {
528
violations.set(what, [why]);
529
}
530
531
if (strictImplicitPublicHandling && !strictImplicitPublicHandling.has(name)) {
532
violationsCauseFailure = true;
533
}
534
});
535
}
536
for (const [why, whys] of violations) {
537
this.log(`WARN: ${why} became PUBLIC because of: ${whys.join(' , ')}`);
538
}
539
if (violationsCauseFailure) {
540
const message = 'Protected fields have been made PUBLIC. This hurts minification and is therefore not allowed. Review the WARN messages further above';
541
this.log(`ERROR: ${message}`);
542
throw new Error(message);
543
}
544
545
// STEP: compute replacement names for each class
546
for (const data of this.allClassDataByKey.values()) {
547
ClassData.fillInReplacement(data);
548
}
549
this.log(`Done creating class replacements`);
550
551
// STEP: prepare rename edits
552
this.log(`Starting prepare rename edits`);
553
554
type Edit = { newText: string; offset: number; length: number };
555
const editsByFile = new Map<string, Edit[]>();
556
557
const appendEdit = (fileName: string, edit: Edit) => {
558
const edits = editsByFile.get(fileName);
559
if (!edits) {
560
editsByFile.set(fileName, [edit]);
561
} else {
562
edits.push(edit);
563
}
564
};
565
const appendRename = (newText: string, loc: ts.RenameLocation) => {
566
appendEdit(loc.fileName, {
567
newText: (loc.prefixText || '') + newText + (loc.suffixText || ''),
568
offset: loc.textSpan.start,
569
length: loc.textSpan.length
570
});
571
};
572
573
type RenameFn = (projectName: string, fileName: string, pos: number) => ts.RenameLocation[];
574
575
const renameResults: Array<Promise<{ readonly newName: string; readonly locations: readonly ts.RenameLocation[] }>> = [];
576
577
const queueRename = (fileName: string, pos: number, newName: string) => {
578
renameResults.push(Promise.resolve(this.renameWorkerPool.exec<RenameFn>('findRenameLocations', [this.projectPath, fileName, pos]))
579
.then((locations) => ({ newName, locations })));
580
};
581
582
for (const data of this.allClassDataByKey.values()) {
583
if (hasModifier(data.node, ts.SyntaxKind.DeclareKeyword)) {
584
continue;
585
}
586
587
fields: for (const [name, info] of data.fields) {
588
if (!ClassData._shouldMangle(info.type)) {
589
continue fields;
590
}
591
592
// TS-HACK: protected became public via 'some' child
593
// and because of that we might need to ignore this now
594
let parent = data.parent;
595
while (parent) {
596
if (parent.fields.get(name)?.type === FieldType.Public) {
597
continue fields;
598
}
599
parent = parent.parent;
600
}
601
602
const newName = data.lookupShortName(name);
603
queueRename(data.fileName, info.pos, newName);
604
}
605
}
606
607
for (const data of this.allExportedSymbols.values()) {
608
if (data.fileName.endsWith('.d.ts')
609
|| skippedExportMangledProjects.some(proj => data.fileName.includes(proj))
610
|| skippedExportMangledFiles.some(file => data.fileName.endsWith(file + '.ts'))
611
) {
612
continue;
613
}
614
615
if (!data.shouldMangle(data.replacementName)) {
616
continue;
617
}
618
619
const newText = data.replacementName;
620
for (const { fileName, offset } of data.getLocations(service)) {
621
queueRename(fileName, offset, newText);
622
}
623
}
624
625
await Promise.all(renameResults).then((result) => {
626
for (const { newName, locations } of result) {
627
for (const loc of locations) {
628
appendRename(newName, loc);
629
}
630
}
631
});
632
633
await this.renameWorkerPool.terminate();
634
635
this.log(`Done preparing edits: ${editsByFile.size} files`);
636
637
// STEP: apply all rename edits (per file)
638
const result = new Map<string, MangleOutput>();
639
let savedBytes = 0;
640
641
for (const item of service.getProgram()!.getSourceFiles()) {
642
643
const { mapRoot, sourceRoot } = service.getProgram()!.getCompilerOptions();
644
const projectDir = path.dirname(this.projectPath);
645
const sourceMapRoot = mapRoot ?? pathToFileURL(sourceRoot ?? projectDir).toString();
646
647
// source maps
648
let generator: SourceMapGenerator | undefined;
649
650
let newFullText: string;
651
const edits = editsByFile.get(item.fileName);
652
if (!edits) {
653
// just copy
654
newFullText = item.getFullText();
655
656
} else {
657
// source map generator
658
const relativeFileName = normalize(path.relative(projectDir, item.fileName));
659
const mappingsByLine = new Map<number, Mapping[]>();
660
661
// apply renames
662
edits.sort((a, b) => b.offset - a.offset);
663
const characters = item.getFullText().split('');
664
665
let lastEdit: Edit | undefined;
666
667
for (const edit of edits) {
668
if (lastEdit && lastEdit.offset === edit.offset) {
669
//
670
if (lastEdit.length !== edit.length || lastEdit.newText !== edit.newText) {
671
this.log('ERROR: Overlapping edit', item.fileName, edit.offset, edits);
672
throw new Error('OVERLAPPING edit');
673
} else {
674
continue;
675
}
676
}
677
lastEdit = edit;
678
const mangledName = characters.splice(edit.offset, edit.length, edit.newText).join('');
679
savedBytes += mangledName.length - edit.newText.length;
680
681
// source maps
682
const pos = item.getLineAndCharacterOfPosition(edit.offset);
683
684
685
let mappings = mappingsByLine.get(pos.line);
686
if (!mappings) {
687
mappings = [];
688
mappingsByLine.set(pos.line, mappings);
689
}
690
mappings.unshift({
691
source: relativeFileName,
692
original: { line: pos.line + 1, column: pos.character },
693
generated: { line: pos.line + 1, column: pos.character },
694
name: mangledName
695
}, {
696
source: relativeFileName,
697
original: { line: pos.line + 1, column: pos.character + edit.length },
698
generated: { line: pos.line + 1, column: pos.character + edit.newText.length },
699
});
700
}
701
702
// source map generation, make sure to get mappings per line correct
703
generator = new SourceMapGenerator({ file: path.basename(item.fileName), sourceRoot: sourceMapRoot });
704
generator.setSourceContent(relativeFileName, item.getFullText());
705
for (const [, mappings] of mappingsByLine) {
706
let lineDelta = 0;
707
for (const mapping of mappings) {
708
generator.addMapping({
709
...mapping,
710
generated: { line: mapping.generated.line, column: mapping.generated.column - lineDelta }
711
});
712
lineDelta += mapping.original.column - mapping.generated.column;
713
}
714
}
715
716
newFullText = characters.join('');
717
}
718
result.set(item.fileName, { out: newFullText, sourceMap: generator?.toString() });
719
}
720
721
service.dispose();
722
this.renameWorkerPool.terminate();
723
724
this.log(`Done: ${savedBytes / 1000}kb saved, memory-usage: ${JSON.stringify(v8.getHeapStatistics())}`);
725
return result;
726
}
727
}
728
729
// --- ast utils
730
731
function hasModifier(node: ts.Node, kind: ts.SyntaxKind) {
732
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
733
return Boolean(modifiers?.find(mode => mode.kind === kind));
734
}
735
736
function isInAmbientContext(node: ts.Node): boolean {
737
for (let p = node.parent; p; p = p.parent) {
738
if (ts.isModuleDeclaration(p)) {
739
return true;
740
}
741
}
742
return false;
743
}
744
745
function normalize(path: string): string {
746
return path.replace(/\\/g, '/');
747
}
748
749
async function _run() {
750
const root = path.join(__dirname, '..', '..', '..');
751
const projectBase = path.join(root, 'src');
752
const projectPath = path.join(projectBase, 'tsconfig.json');
753
const newProjectBase = path.join(path.dirname(projectBase), path.basename(projectBase) + '2');
754
755
fs.cpSync(projectBase, newProjectBase, { recursive: true });
756
757
const mangler = new Mangler(projectPath, console.log, {
758
mangleExports: true,
759
manglePrivateFields: true,
760
});
761
for (const [fileName, contents] of await mangler.computeNewFileContents(new Set(['saveState']))) {
762
const newFilePath = path.join(newProjectBase, path.relative(projectBase, fileName));
763
await fs.promises.mkdir(path.dirname(newFilePath), { recursive: true });
764
await fs.promises.writeFile(newFilePath, contents.out);
765
if (contents.sourceMap) {
766
await fs.promises.writeFile(newFilePath + '.map', contents.sourceMap);
767
}
768
}
769
}
770
771
if (__filename === argv[1]) {
772
_run();
773
}
774
775