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