@esportsplus/typescript 0.9.2 → 0.11.0
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.
- package/.github/workflows/bump.yml +20 -2
- package/.github/workflows/dependabot.yml +50 -4
- package/.github/workflows/publish.yml +32 -5
- package/.github/workflows/templates/bump.yml +9 -0
- package/.github/workflows/templates/dependabot.yml +12 -0
- package/.github/workflows/templates/publish.yml +16 -0
- package/build/transformer/constants.d.ts +4 -0
- package/build/transformer/constants.js +4 -0
- package/build/transformer/index.d.ts +13 -0
- package/build/transformer/index.js +157 -0
- package/build/transformer/types.d.ts +23 -0
- package/build/transformer/types.js +1 -0
- package/package.json +12 -3
- package/src/transformer/constants.ts +8 -0
- package/src/transformer/index.ts +244 -0
- package/src/transformer/types.ts +38 -0
|
@@ -1,9 +1,27 @@
|
|
|
1
|
-
name: bump
|
|
1
|
+
name: bump version
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
5
|
branches: '**' # only trigger on branches, not on tags
|
|
6
|
+
workflow_call:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: write
|
|
6
14
|
|
|
7
15
|
jobs:
|
|
8
16
|
bump:
|
|
9
|
-
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- name: checkout repo
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
with:
|
|
22
|
+
fetch-depth: 0
|
|
23
|
+
- name: bump version
|
|
24
|
+
uses: jpb06/bump-package@latest
|
|
25
|
+
with:
|
|
26
|
+
minor-keywords: feat,minor,refactor
|
|
27
|
+
patch-keywords: fix,chore
|
|
@@ -3,10 +3,56 @@ name: dependabot automerge
|
|
|
3
3
|
on:
|
|
4
4
|
pull_request:
|
|
5
5
|
types: [opened, synchronize, labeled]
|
|
6
|
-
|
|
6
|
+
workflow_call:
|
|
7
|
+
secrets:
|
|
8
|
+
NPM_TOKEN:
|
|
9
|
+
required: true
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: write
|
|
17
|
+
pull-requests: write
|
|
7
18
|
|
|
8
19
|
jobs:
|
|
20
|
+
build:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
- uses: pnpm/action-setup@v4
|
|
25
|
+
name: Install pnpm
|
|
26
|
+
with:
|
|
27
|
+
run_install: false
|
|
28
|
+
version: latest
|
|
29
|
+
- uses: actions/setup-node@v4
|
|
30
|
+
with:
|
|
31
|
+
cache: 'pnpm'
|
|
32
|
+
node-version: 'latest'
|
|
33
|
+
registry-url: 'https://registry.npmjs.org'
|
|
34
|
+
- run: pnpm i && pnpm build
|
|
35
|
+
env:
|
|
36
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
37
|
+
|
|
9
38
|
automerge:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
39
|
+
needs: build
|
|
40
|
+
permissions:
|
|
41
|
+
pull-requests: write
|
|
42
|
+
contents: write
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request' }}
|
|
45
|
+
steps:
|
|
46
|
+
- uses: dependabot/fetch-metadata@v2
|
|
47
|
+
id: metadata
|
|
48
|
+
with:
|
|
49
|
+
alert-lookup: true
|
|
50
|
+
compat-lookup: true
|
|
51
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
52
|
+
- name: Approve and merge Dependabot PR
|
|
53
|
+
run: |
|
|
54
|
+
gh pr review --approve "$PR_URL"
|
|
55
|
+
gh pr merge --squash --auto "$PR_URL"
|
|
56
|
+
env:
|
|
57
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
58
|
+
PR_URL: ${{ github.event.pull_request.html_url }}
|
|
@@ -3,14 +3,41 @@ name: publish to npm
|
|
|
3
3
|
on:
|
|
4
4
|
release:
|
|
5
5
|
types: [published]
|
|
6
|
+
workflow_call:
|
|
7
|
+
secrets:
|
|
8
|
+
NPM_TOKEN:
|
|
9
|
+
required: true
|
|
6
10
|
workflow_dispatch:
|
|
7
11
|
workflow_run:
|
|
8
|
-
workflows: [bump]
|
|
12
|
+
workflows: [bump version]
|
|
9
13
|
types:
|
|
10
14
|
- completed
|
|
11
15
|
|
|
16
|
+
concurrency:
|
|
17
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
18
|
+
cancel-in-progress: true
|
|
19
|
+
|
|
12
20
|
jobs:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
build:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
permissions:
|
|
24
|
+
contents: read
|
|
25
|
+
id-token: write
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
- uses: pnpm/action-setup@v4
|
|
29
|
+
name: Install pnpm
|
|
30
|
+
with:
|
|
31
|
+
run_install: false
|
|
32
|
+
version: latest
|
|
33
|
+
- uses: actions/setup-node@v4
|
|
34
|
+
with:
|
|
35
|
+
cache: 'pnpm'
|
|
36
|
+
node-version: 'latest'
|
|
37
|
+
registry-url: 'https://registry.npmjs.org'
|
|
38
|
+
- run: pnpm config delete always-auth
|
|
39
|
+
- run: pnpm i
|
|
40
|
+
env:
|
|
41
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
42
|
+
- run: pnpm build
|
|
43
|
+
- run: pnpm publish --provenance --no-git-checks
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
workflow_run:
|
|
8
|
+
workflows: [bump version]
|
|
9
|
+
types:
|
|
10
|
+
- completed
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
secrets:
|
|
15
|
+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
16
|
+
uses: esportsplus/typescript/.github/workflows/publish.yml@main
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import type { ImportModification, NodeMatch, QuickCheckPattern, Replacement, VisitorCallback, VisitorPredicate } from './types.js';
|
|
3
|
+
declare const addImport: (code: string, module: string, specifiers: string[]) => string;
|
|
4
|
+
declare const applyReplacements: (code: string, replacements: Replacement[]) => string;
|
|
5
|
+
declare const applyReplacementsReverse: (code: string, replacements: Replacement[]) => string;
|
|
6
|
+
declare const collectNodes: <T>(sourceFile: ts.SourceFile, predicate: (node: ts.Node) => T | null) => NodeMatch<T>[];
|
|
7
|
+
declare const mightNeedTransform: (code: string, check: QuickCheckPattern) => boolean;
|
|
8
|
+
declare const uid: (prefix?: string) => string;
|
|
9
|
+
declare const updateImports: (code: string, modification: ImportModification) => string;
|
|
10
|
+
declare const visitAst: <T>(sourceFile: ts.SourceFile, callback: VisitorCallback<T>, state: T, predicate?: VisitorPredicate) => T;
|
|
11
|
+
declare const visitAstWithDepth: <T>(sourceFile: ts.SourceFile, callback: (node: ts.Node, depth: number, state: T) => void, state: T, depthTrigger: (node: ts.Node) => boolean) => T;
|
|
12
|
+
export { addImport, applyReplacements, applyReplacementsReverse, collectNodes, mightNeedTransform, uid, updateImports, visitAst, visitAstWithDepth };
|
|
13
|
+
export type { ImportModification, NodeMatch, QuickCheckPattern, Replacement, VisitorCallback, VisitorPredicate };
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { uuid } from '@esportsplus/utilities';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { BRACES_CONTENT_REGEX, REGEX_ESCAPE_PATTERN, UUID_DASH_REGEX } from './constants.js';
|
|
4
|
+
function buildImportRegex(escapedModule) {
|
|
5
|
+
return new RegExp(`(import\\s*\\{[^}]*\\}\\s*from\\s*['"]${escapedModule}['"])`);
|
|
6
|
+
}
|
|
7
|
+
function mergeAndSort(a, b) {
|
|
8
|
+
let combined = new Array(a.length + b.size), idx = 0, n = a.length;
|
|
9
|
+
for (let i = 0; i < n; i++) {
|
|
10
|
+
if (a[i]) {
|
|
11
|
+
combined[idx++] = a[i];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
for (let item of b) {
|
|
15
|
+
if (item) {
|
|
16
|
+
combined[idx++] = item;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
combined.length = idx;
|
|
20
|
+
combined.sort();
|
|
21
|
+
return combined.join(', ');
|
|
22
|
+
}
|
|
23
|
+
function parseSpecifiers(str) {
|
|
24
|
+
let parts = str.split(','), result = new Set();
|
|
25
|
+
for (let i = 0, n = parts.length; i < n; i++) {
|
|
26
|
+
let trimmed = parts[i].trim();
|
|
27
|
+
if (trimmed) {
|
|
28
|
+
result.add(trimmed);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
function updateImportsWithRegex(code, specifiers, importRegex) {
|
|
34
|
+
let match = code.match(importRegex);
|
|
35
|
+
if (!match) {
|
|
36
|
+
return code;
|
|
37
|
+
}
|
|
38
|
+
let bracesMatch = match[1].match(BRACES_CONTENT_REGEX), existing = bracesMatch?.[1] ? parseSpecifiers(bracesMatch[1]) : new Set(), toAdd = [];
|
|
39
|
+
for (let spec of specifiers) {
|
|
40
|
+
if (!existing.has(spec)) {
|
|
41
|
+
toAdd.push(spec);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (toAdd.length === 0) {
|
|
45
|
+
return code;
|
|
46
|
+
}
|
|
47
|
+
return code.replace(match[1], match[1].replace(BRACES_CONTENT_REGEX, `{ ${mergeAndSort(toAdd, existing)} }`));
|
|
48
|
+
}
|
|
49
|
+
const addImport = (code, module, specifiers) => {
|
|
50
|
+
if (specifiers.length === 0) {
|
|
51
|
+
return code;
|
|
52
|
+
}
|
|
53
|
+
let regex = buildImportRegex(module.replace(REGEX_ESCAPE_PATTERN, '\\$&'));
|
|
54
|
+
if (regex.test(code)) {
|
|
55
|
+
return updateImportsWithRegex(code, new Set(specifiers), regex);
|
|
56
|
+
}
|
|
57
|
+
let adding = `import { ${specifiers.sort().join(', ')} } from '${module}';\n`, first = code.indexOf('import ');
|
|
58
|
+
if (first === -1) {
|
|
59
|
+
return adding + code;
|
|
60
|
+
}
|
|
61
|
+
return code.substring(0, first) + adding + code.substring(first);
|
|
62
|
+
};
|
|
63
|
+
const applyReplacements = (code, replacements) => {
|
|
64
|
+
if (replacements.length === 0) {
|
|
65
|
+
return code;
|
|
66
|
+
}
|
|
67
|
+
replacements.sort((a, b) => a.start - b.start);
|
|
68
|
+
let parts = [], pos = 0;
|
|
69
|
+
for (let i = 0, n = replacements.length; i < n; i++) {
|
|
70
|
+
let r = replacements[i];
|
|
71
|
+
if (r.start > pos) {
|
|
72
|
+
parts.push(code.substring(pos, r.start));
|
|
73
|
+
}
|
|
74
|
+
parts.push(r.newText);
|
|
75
|
+
pos = r.end;
|
|
76
|
+
}
|
|
77
|
+
if (pos < code.length) {
|
|
78
|
+
parts.push(code.substring(pos));
|
|
79
|
+
}
|
|
80
|
+
return parts.join('');
|
|
81
|
+
};
|
|
82
|
+
const applyReplacementsReverse = (code, replacements) => {
|
|
83
|
+
if (replacements.length === 0) {
|
|
84
|
+
return code;
|
|
85
|
+
}
|
|
86
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
87
|
+
let result = code;
|
|
88
|
+
for (let i = 0, n = replacements.length; i < n; i++) {
|
|
89
|
+
let r = replacements[i];
|
|
90
|
+
result = result.substring(0, r.start) + r.newText + result.substring(r.end);
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
};
|
|
94
|
+
const collectNodes = (sourceFile, predicate) => {
|
|
95
|
+
let matches = [];
|
|
96
|
+
function visit(node) {
|
|
97
|
+
let data = predicate(node);
|
|
98
|
+
if (data !== null) {
|
|
99
|
+
matches.push({
|
|
100
|
+
data,
|
|
101
|
+
end: node.end,
|
|
102
|
+
node,
|
|
103
|
+
start: node.getStart(sourceFile)
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
ts.forEachChild(node, visit);
|
|
107
|
+
}
|
|
108
|
+
visit(sourceFile);
|
|
109
|
+
return matches;
|
|
110
|
+
};
|
|
111
|
+
const mightNeedTransform = (code, check) => {
|
|
112
|
+
if (check.regex) {
|
|
113
|
+
return check.regex.test(code);
|
|
114
|
+
}
|
|
115
|
+
if (check.patterns) {
|
|
116
|
+
for (let i = 0, n = check.patterns.length; i < n; i++) {
|
|
117
|
+
if (code.indexOf(check.patterns[i]) !== -1) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
};
|
|
124
|
+
const uid = (prefix) => {
|
|
125
|
+
return (prefix ? prefix + '_' : '_') + uuid().replace(UUID_DASH_REGEX, '_');
|
|
126
|
+
};
|
|
127
|
+
const updateImports = (code, modification) => {
|
|
128
|
+
let { module, specifiers } = modification;
|
|
129
|
+
if (specifiers.size === 0) {
|
|
130
|
+
return code;
|
|
131
|
+
}
|
|
132
|
+
let escapedModule = module.replace(REGEX_ESCAPE_PATTERN, '\\$&'), importRegex = buildImportRegex(escapedModule);
|
|
133
|
+
return updateImportsWithRegex(code, specifiers, importRegex);
|
|
134
|
+
};
|
|
135
|
+
const visitAst = (sourceFile, callback, state, predicate) => {
|
|
136
|
+
function visit(node) {
|
|
137
|
+
if (!predicate || predicate(node)) {
|
|
138
|
+
callback(node, state);
|
|
139
|
+
}
|
|
140
|
+
ts.forEachChild(node, visit);
|
|
141
|
+
}
|
|
142
|
+
visit(sourceFile);
|
|
143
|
+
return state;
|
|
144
|
+
};
|
|
145
|
+
const visitAstWithDepth = (sourceFile, callback, state, depthTrigger) => {
|
|
146
|
+
let depthStack = [0];
|
|
147
|
+
function visit(node) {
|
|
148
|
+
let depth = depthStack[depthStack.length - 1], nextDepth = depthTrigger(node) ? depth + 1 : depth;
|
|
149
|
+
callback(node, depth, state);
|
|
150
|
+
depthStack.push(nextDepth);
|
|
151
|
+
ts.forEachChild(node, visit);
|
|
152
|
+
depthStack.pop();
|
|
153
|
+
}
|
|
154
|
+
visit(sourceFile);
|
|
155
|
+
return state;
|
|
156
|
+
};
|
|
157
|
+
export { addImport, applyReplacements, applyReplacementsReverse, collectNodes, mightNeedTransform, uid, updateImports, visitAst, visitAstWithDepth };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
type ImportModification = {
|
|
3
|
+
module: string;
|
|
4
|
+
specifiers: Set<string>;
|
|
5
|
+
};
|
|
6
|
+
type NodeMatch<T> = {
|
|
7
|
+
data: T;
|
|
8
|
+
end: number;
|
|
9
|
+
node: ts.Node;
|
|
10
|
+
start: number;
|
|
11
|
+
};
|
|
12
|
+
type QuickCheckPattern = {
|
|
13
|
+
patterns?: string[];
|
|
14
|
+
regex?: RegExp;
|
|
15
|
+
};
|
|
16
|
+
type Replacement = {
|
|
17
|
+
end: number;
|
|
18
|
+
newText: string;
|
|
19
|
+
start: number;
|
|
20
|
+
};
|
|
21
|
+
type VisitorCallback<T> = (node: ts.Node, state: T) => void;
|
|
22
|
+
type VisitorPredicate = (node: ts.Node) => boolean;
|
|
23
|
+
export type { ImportModification, NodeMatch, QuickCheckPattern, Replacement, VisitorCallback, VisitorPredicate };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -5,15 +5,20 @@
|
|
|
5
5
|
"tsc-alias": "./bin/tsc-alias"
|
|
6
6
|
},
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@esportsplus/cli-passthrough": "^0.0.
|
|
8
|
+
"@esportsplus/cli-passthrough": "^0.0.12",
|
|
9
|
+
"@esportsplus/utilities": "^0.27.2",
|
|
9
10
|
"tsc-alias": "^1.8.16",
|
|
10
|
-
"typescript": "^5.
|
|
11
|
+
"typescript": "^5.9.3"
|
|
11
12
|
},
|
|
12
13
|
"exports": {
|
|
13
14
|
"./package.json": "./package.json",
|
|
14
15
|
"./tsconfig.browser.json": "./tsconfig.browser.json",
|
|
15
16
|
"./tsconfig.node.json": "./tsconfig.node.json",
|
|
16
17
|
"./tsconfig.package.json": "./tsconfig.package.json",
|
|
18
|
+
"./transformer": {
|
|
19
|
+
"types": "./build/transformer/index.d.ts",
|
|
20
|
+
"default": "./build/transformer/index.js"
|
|
21
|
+
},
|
|
17
22
|
".": {
|
|
18
23
|
"types": "./build/index.d.ts",
|
|
19
24
|
"default": "./build/index.js"
|
|
@@ -22,9 +27,13 @@
|
|
|
22
27
|
"main": "build/index.js",
|
|
23
28
|
"name": "@esportsplus/typescript",
|
|
24
29
|
"private": false,
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/esportsplus/typescript"
|
|
33
|
+
},
|
|
25
34
|
"type": "module",
|
|
26
35
|
"types": "build/index.d.ts",
|
|
27
|
-
"version": "0.
|
|
36
|
+
"version": "0.11.0",
|
|
28
37
|
"scripts": {
|
|
29
38
|
"build": "tsc && tsc-alias",
|
|
30
39
|
"-": "-"
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { uuid } from '@esportsplus/utilities';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { BRACES_CONTENT_REGEX, REGEX_ESCAPE_PATTERN, UUID_DASH_REGEX } from './constants.js';
|
|
4
|
+
import type { ImportModification, NodeMatch, QuickCheckPattern, Replacement, VisitorCallback, VisitorPredicate } from './types.js';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
function buildImportRegex(escapedModule: string): RegExp {
|
|
8
|
+
return new RegExp(`(import\\s*\\{[^}]*\\}\\s*from\\s*['"]${escapedModule}['"])`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function mergeAndSort(a: string[], b: Set<string>): string {
|
|
12
|
+
let combined = new Array<string>(a.length + b.size),
|
|
13
|
+
idx = 0,
|
|
14
|
+
n = a.length;
|
|
15
|
+
|
|
16
|
+
for (let i = 0; i < n; i++) {
|
|
17
|
+
if (a[i]) {
|
|
18
|
+
combined[idx++] = a[i];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (let item of b) {
|
|
23
|
+
if (item) {
|
|
24
|
+
combined[idx++] = item;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
combined.length = idx;
|
|
29
|
+
combined.sort();
|
|
30
|
+
|
|
31
|
+
return combined.join(', ');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseSpecifiers(str: string): Set<string> {
|
|
35
|
+
let parts = str.split(','),
|
|
36
|
+
result = new Set<string>();
|
|
37
|
+
|
|
38
|
+
for (let i = 0, n = parts.length; i < n; i++) {
|
|
39
|
+
let trimmed = parts[i].trim();
|
|
40
|
+
|
|
41
|
+
if (trimmed) {
|
|
42
|
+
result.add(trimmed);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function updateImportsWithRegex(code: string, specifiers: Set<string>, importRegex: RegExp): string {
|
|
50
|
+
let match = code.match(importRegex);
|
|
51
|
+
|
|
52
|
+
if (!match) {
|
|
53
|
+
return code;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let bracesMatch = match[1].match(BRACES_CONTENT_REGEX),
|
|
57
|
+
existing = bracesMatch?.[1] ? parseSpecifiers(bracesMatch[1]) : new Set<string>(),
|
|
58
|
+
toAdd: string[] = [];
|
|
59
|
+
|
|
60
|
+
for (let spec of specifiers) {
|
|
61
|
+
if (!existing.has(spec)) {
|
|
62
|
+
toAdd.push(spec);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (toAdd.length === 0) {
|
|
67
|
+
return code;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return code.replace(
|
|
71
|
+
match[1],
|
|
72
|
+
match[1].replace(BRACES_CONTENT_REGEX, `{ ${mergeAndSort(toAdd, existing)} }`)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
const addImport = (code: string, module: string, specifiers: string[]): string => {
|
|
78
|
+
if (specifiers.length === 0) {
|
|
79
|
+
return code;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let regex = buildImportRegex( module.replace(REGEX_ESCAPE_PATTERN, '\\$&') );
|
|
83
|
+
|
|
84
|
+
if (regex.test(code)) {
|
|
85
|
+
return updateImportsWithRegex(code, new Set(specifiers), regex);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let adding = `import { ${specifiers.sort().join(', ')} } from '${module}';\n`,
|
|
89
|
+
first = code.indexOf('import ');
|
|
90
|
+
|
|
91
|
+
if (first === -1) {
|
|
92
|
+
return adding + code;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return code.substring(0, first) + adding + code.substring(first);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const applyReplacements = (code: string, replacements: Replacement[]): string => {
|
|
99
|
+
if (replacements.length === 0) {
|
|
100
|
+
return code;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
replacements.sort((a, b) => a.start - b.start);
|
|
104
|
+
|
|
105
|
+
let parts: string[] = [],
|
|
106
|
+
pos = 0;
|
|
107
|
+
|
|
108
|
+
for (let i = 0, n = replacements.length; i < n; i++) {
|
|
109
|
+
let r = replacements[i];
|
|
110
|
+
|
|
111
|
+
if (r.start > pos) {
|
|
112
|
+
parts.push(code.substring(pos, r.start));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
parts.push(r.newText);
|
|
116
|
+
pos = r.end;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (pos < code.length) {
|
|
120
|
+
parts.push(code.substring(pos));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return parts.join('');
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const applyReplacementsReverse = (code: string, replacements: Replacement[]): string => {
|
|
127
|
+
if (replacements.length === 0) {
|
|
128
|
+
return code;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
132
|
+
|
|
133
|
+
let result = code;
|
|
134
|
+
|
|
135
|
+
for (let i = 0, n = replacements.length; i < n; i++) {
|
|
136
|
+
let r = replacements[i];
|
|
137
|
+
|
|
138
|
+
result = result.substring(0, r.start) + r.newText + result.substring(r.end);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return result;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const collectNodes = <T>(sourceFile: ts.SourceFile, predicate: (node: ts.Node) => T | null): NodeMatch<T>[] => {
|
|
145
|
+
let matches: NodeMatch<T>[] = [];
|
|
146
|
+
|
|
147
|
+
function visit(node: ts.Node): void {
|
|
148
|
+
let data = predicate(node);
|
|
149
|
+
|
|
150
|
+
if (data !== null) {
|
|
151
|
+
matches.push({
|
|
152
|
+
data,
|
|
153
|
+
end: node.end,
|
|
154
|
+
node,
|
|
155
|
+
start: node.getStart(sourceFile)
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
ts.forEachChild(node, visit);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
visit(sourceFile);
|
|
163
|
+
|
|
164
|
+
return matches;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const mightNeedTransform = (code: string, check: QuickCheckPattern): boolean => {
|
|
168
|
+
if (check.regex) {
|
|
169
|
+
return check.regex.test(code);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (check.patterns) {
|
|
173
|
+
for (let i = 0, n = check.patterns.length; i < n; i++) {
|
|
174
|
+
if (code.indexOf(check.patterns[i]) !== -1) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return false;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const uid = (prefix?: string): string => {
|
|
184
|
+
return (prefix ? prefix + '_' : '_') + uuid().replace(UUID_DASH_REGEX, '_');
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const updateImports = (code: string, modification: ImportModification): string => {
|
|
188
|
+
let { module, specifiers } = modification;
|
|
189
|
+
|
|
190
|
+
if (specifiers.size === 0) {
|
|
191
|
+
return code;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let escapedModule = module.replace(REGEX_ESCAPE_PATTERN, '\\$&'),
|
|
195
|
+
importRegex = buildImportRegex(escapedModule);
|
|
196
|
+
|
|
197
|
+
return updateImportsWithRegex(code, specifiers, importRegex);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const visitAst = <T>(
|
|
201
|
+
sourceFile: ts.SourceFile,
|
|
202
|
+
callback: VisitorCallback<T>,
|
|
203
|
+
state: T,
|
|
204
|
+
predicate?: VisitorPredicate
|
|
205
|
+
): T => {
|
|
206
|
+
function visit(node: ts.Node): void {
|
|
207
|
+
if (!predicate || predicate(node)) {
|
|
208
|
+
callback(node, state);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
ts.forEachChild(node, visit);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
visit(sourceFile);
|
|
215
|
+
|
|
216
|
+
return state;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const visitAstWithDepth = <T>(
|
|
220
|
+
sourceFile: ts.SourceFile,
|
|
221
|
+
callback: (node: ts.Node, depth: number, state: T) => void,
|
|
222
|
+
state: T,
|
|
223
|
+
depthTrigger: (node: ts.Node) => boolean
|
|
224
|
+
): T => {
|
|
225
|
+
let depthStack: number[] = [0];
|
|
226
|
+
|
|
227
|
+
function visit(node: ts.Node): void {
|
|
228
|
+
let depth = depthStack[depthStack.length - 1],
|
|
229
|
+
nextDepth = depthTrigger(node) ? depth + 1 : depth;
|
|
230
|
+
|
|
231
|
+
callback(node, depth, state);
|
|
232
|
+
depthStack.push(nextDepth);
|
|
233
|
+
ts.forEachChild(node, visit);
|
|
234
|
+
depthStack.pop();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
visit(sourceFile);
|
|
238
|
+
|
|
239
|
+
return state;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
export { addImport, applyReplacements, applyReplacementsReverse, collectNodes, mightNeedTransform, uid, updateImports, visitAst, visitAstWithDepth };
|
|
244
|
+
export type { ImportModification, NodeMatch, QuickCheckPattern, Replacement, VisitorCallback, VisitorPredicate };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
type ImportModification = {
|
|
5
|
+
module: string;
|
|
6
|
+
specifiers: Set<string>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type NodeMatch<T> = {
|
|
10
|
+
data: T;
|
|
11
|
+
end: number;
|
|
12
|
+
node: ts.Node;
|
|
13
|
+
start: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type QuickCheckPattern = {
|
|
17
|
+
patterns?: string[];
|
|
18
|
+
regex?: RegExp;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type Replacement = {
|
|
22
|
+
end: number;
|
|
23
|
+
newText: string;
|
|
24
|
+
start: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type VisitorCallback<T> = (node: ts.Node, state: T) => void;
|
|
28
|
+
|
|
29
|
+
type VisitorPredicate = (node: ts.Node) => boolean;
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
export type {
|
|
33
|
+
ImportModification,
|
|
34
|
+
NodeMatch,
|
|
35
|
+
QuickCheckPattern,
|
|
36
|
+
Replacement,
|
|
37
|
+
VisitorCallback, VisitorPredicate
|
|
38
|
+
};
|