@esportsplus/typescript 0.29.1 → 0.29.6
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 +1 -1
- package/.github/workflows/dependabot.yml +4 -4
- package/.github/workflows/publish.yml +3 -4
- package/README.md +146 -0
- package/build/compiler/coordinator.js +2 -1
- package/build/compiler/imports.d.ts +1 -0
- package/build/compiler/imports.js +5 -2
- package/package.json +10 -7
- package/pnpm-workspace.yaml +6 -0
- package/src/compiler/coordinator.ts +6 -1
- package/src/compiler/imports.ts +12 -2
- package/tests/compiler/coordinator.test.ts +17 -0
- package/tsconfig.node.json +3 -0
|
@@ -20,13 +20,13 @@ jobs:
|
|
|
20
20
|
build:
|
|
21
21
|
runs-on: ubuntu-latest
|
|
22
22
|
steps:
|
|
23
|
-
- uses: actions/checkout@
|
|
24
|
-
- uses: pnpm/action-setup@
|
|
23
|
+
- uses: actions/checkout@v7
|
|
24
|
+
- uses: pnpm/action-setup@v6
|
|
25
25
|
name: Install pnpm
|
|
26
26
|
with:
|
|
27
27
|
run_install: false
|
|
28
28
|
version: latest
|
|
29
|
-
- uses: actions/setup-node@
|
|
29
|
+
- uses: actions/setup-node@v6
|
|
30
30
|
with:
|
|
31
31
|
cache: 'pnpm'
|
|
32
32
|
node-version: 'latest'
|
|
@@ -43,7 +43,7 @@ jobs:
|
|
|
43
43
|
runs-on: ubuntu-latest
|
|
44
44
|
if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request' }}
|
|
45
45
|
steps:
|
|
46
|
-
- uses: dependabot/fetch-metadata@
|
|
46
|
+
- uses: dependabot/fetch-metadata@v3
|
|
47
47
|
id: metadata
|
|
48
48
|
with:
|
|
49
49
|
alert-lookup: true
|
|
@@ -24,18 +24,17 @@ jobs:
|
|
|
24
24
|
contents: read
|
|
25
25
|
id-token: write
|
|
26
26
|
steps:
|
|
27
|
-
- uses: actions/checkout@
|
|
28
|
-
- uses: pnpm/action-setup@
|
|
27
|
+
- uses: actions/checkout@v7
|
|
28
|
+
- uses: pnpm/action-setup@v6
|
|
29
29
|
name: Install pnpm
|
|
30
30
|
with:
|
|
31
31
|
run_install: false
|
|
32
32
|
version: latest
|
|
33
|
-
- uses: actions/setup-node@
|
|
33
|
+
- uses: actions/setup-node@v6
|
|
34
34
|
with:
|
|
35
35
|
cache: 'pnpm'
|
|
36
36
|
node-version: 'latest'
|
|
37
37
|
registry-url: 'https://registry.npmjs.org'
|
|
38
|
-
- run: pnpm config delete always-auth
|
|
39
38
|
- run: pnpm i
|
|
40
39
|
env:
|
|
41
40
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# @esportsplus/typescript
|
|
2
|
+
|
|
3
|
+
TypeScript compiler plugin framework with coordinated AST transformations, import management, and build tool integration.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @esportsplus/typescript
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Extends the TypeScript compiler with a plugin architecture for custom AST transformations. Plugins receive type-checked AST nodes with accurate positions at every stage, return declarative intents (replacements, imports, prepends), and the coordinator applies them in the correct order.
|
|
14
|
+
|
|
15
|
+
**Key features:**
|
|
16
|
+
- Multi-plugin coordination with fresh AST positions between stages
|
|
17
|
+
- Declarative import management (add/remove specifiers, namespace imports)
|
|
18
|
+
- Vite plugin for dev/build integration
|
|
19
|
+
- CLI wrapper for `tsc` with automatic plugin detection
|
|
20
|
+
- Language service caching for incremental compilation
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Plugin
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import type { Plugin } from '@esportsplus/typescript/compiler';
|
|
28
|
+
|
|
29
|
+
let plugin: Plugin = {
|
|
30
|
+
// Optional: skip files that don't contain these strings
|
|
31
|
+
patterns: ['myFunction'],
|
|
32
|
+
|
|
33
|
+
transform({ checker, code, sourceFile, shared }) {
|
|
34
|
+
// Return declarative transformation intents
|
|
35
|
+
return {
|
|
36
|
+
imports: [
|
|
37
|
+
{ package: 'my-lib', add: ['helper'], remove: ['deprecated'] }
|
|
38
|
+
],
|
|
39
|
+
prepend: [
|
|
40
|
+
'let __cache = new Map();'
|
|
41
|
+
],
|
|
42
|
+
replacements: [
|
|
43
|
+
{
|
|
44
|
+
node: someAstNode,
|
|
45
|
+
generate: (sf) => `transformedCode()`
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Vite
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { plugin } from '@esportsplus/typescript/compiler';
|
|
57
|
+
|
|
58
|
+
export default defineConfig({
|
|
59
|
+
plugins: [
|
|
60
|
+
plugin.vite({
|
|
61
|
+
name: 'my-transforms',
|
|
62
|
+
plugins: [myPlugin]
|
|
63
|
+
})
|
|
64
|
+
]
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### CLI
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Compiles with plugins from tsconfig.json, then resolves path aliases
|
|
72
|
+
tsc
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The CLI detects plugins in `tsconfig.json` `compilerOptions.plugins`, loads them, runs coordinated compilation, and automatically calls `tsc-alias` afterward.
|
|
76
|
+
|
|
77
|
+
## API
|
|
78
|
+
|
|
79
|
+
### `@esportsplus/typescript`
|
|
80
|
+
|
|
81
|
+
Re-exports the TypeScript compiler API (`ts`).
|
|
82
|
+
|
|
83
|
+
### `@esportsplus/typescript/compiler`
|
|
84
|
+
|
|
85
|
+
| Export | Description |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `ast` | AST utilities — expression names, property paths, node testing |
|
|
88
|
+
| `code` | Template literal code generation with escaping |
|
|
89
|
+
| `coordinator` | Multi-plugin transformation orchestrator |
|
|
90
|
+
| `imports` | Import detection and modification (WeakMap cached) |
|
|
91
|
+
| `plugin` | Built-in plugins (`tsc`, `vite`) |
|
|
92
|
+
| `uid` | Unique identifier generation |
|
|
93
|
+
| `languageService` | Cached TypeScript language service |
|
|
94
|
+
|
|
95
|
+
### Types
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
type Plugin = {
|
|
99
|
+
patterns?: string[];
|
|
100
|
+
transform: (ctx: TransformContext) => TransformResult;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
type TransformContext = {
|
|
104
|
+
checker: ts.TypeChecker;
|
|
105
|
+
code: string;
|
|
106
|
+
program: ts.Program;
|
|
107
|
+
shared: SharedContext;
|
|
108
|
+
sourceFile: ts.SourceFile;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
type TransformResult = {
|
|
112
|
+
imports?: ImportIntent[];
|
|
113
|
+
prepend?: string[];
|
|
114
|
+
replacements?: ReplacementIntent[];
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
type ImportIntent = {
|
|
118
|
+
add?: string[];
|
|
119
|
+
namespace?: string;
|
|
120
|
+
package: string;
|
|
121
|
+
remove?: string[];
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
type ReplacementIntent = {
|
|
125
|
+
generate: (sourceFile: ts.SourceFile) => string;
|
|
126
|
+
node: ts.Node;
|
|
127
|
+
};
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Shared Config
|
|
131
|
+
|
|
132
|
+
Importable base tsconfig files:
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{ "extends": "@esportsplus/typescript/tsconfig.browser.json" }
|
|
136
|
+
{ "extends": "@esportsplus/typescript/tsconfig.node.json" }
|
|
137
|
+
{ "extends": "@esportsplus/typescript/tsconfig.package.json" }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Scripts
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
pnpm build # tsc && tsc-alias
|
|
144
|
+
pnpm test # vitest run
|
|
145
|
+
pnpm bench:run # vitest bench --run
|
|
146
|
+
```
|
|
@@ -92,7 +92,8 @@ function modify(code, file, pkg, options) {
|
|
|
92
92
|
for (let i = 0, n = found.length; i < n; i++) {
|
|
93
93
|
for (let [name, alias] of found[i].specifiers) {
|
|
94
94
|
if (!remove || (!remove.has(name) && !remove.has(alias))) {
|
|
95
|
-
|
|
95
|
+
let base = name === alias ? name : `${name} as ${alias}`;
|
|
96
|
+
specifiers.add(found[i].typeOnly.has(name) ? `type ${base}` : base);
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
}
|
|
@@ -15,14 +15,17 @@ const all = (file, pkg) => {
|
|
|
15
15
|
if (!ts.isStringLiteral(moduleSpecifier) || moduleSpecifier.text !== pkg) {
|
|
16
16
|
continue;
|
|
17
17
|
}
|
|
18
|
-
let bindings = stmt.importClause?.namedBindings, specifiers = new Map();
|
|
18
|
+
let bindings = stmt.importClause?.namedBindings, declTypeOnly = stmt.importClause?.isTypeOnly ?? false, specifiers = new Map(), typeOnly = new Set();
|
|
19
19
|
if (bindings && ts.isNamedImports(bindings)) {
|
|
20
20
|
for (let j = 0, m = bindings.elements.length; j < m; j++) {
|
|
21
21
|
let element = bindings.elements[j], name = element.name.text, propertyName = element.propertyName?.text || name;
|
|
22
22
|
specifiers.set(propertyName, name);
|
|
23
|
+
if (declTypeOnly || element.isTypeOnly) {
|
|
24
|
+
typeOnly.add(propertyName);
|
|
25
|
+
}
|
|
23
26
|
}
|
|
24
27
|
}
|
|
25
|
-
imports.push({ end: stmt.end, specifiers, start: stmt.getStart(file) });
|
|
28
|
+
imports.push({ end: stmt.end, specifiers, start: stmt.getStart(file), typeOnly });
|
|
26
29
|
}
|
|
27
30
|
return imports;
|
|
28
31
|
};
|
package/package.json
CHANGED
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@esportsplus/cli-passthrough": "^0.0.15",
|
|
11
|
-
"@esportsplus/utilities": "^0.
|
|
12
|
-
"@types/node": "^
|
|
13
|
-
"tsc-alias": "^1.8.
|
|
14
|
-
"typescript": "^6.0.
|
|
11
|
+
"@esportsplus/utilities": "^0.28.0",
|
|
12
|
+
"@types/node": "^26.0.1",
|
|
13
|
+
"tsc-alias": "^1.8.17",
|
|
14
|
+
"typescript": "^6.0.3"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"vite": "^8.0
|
|
18
|
-
"vitest": "^4.1.
|
|
17
|
+
"vite": "^8.1.0",
|
|
18
|
+
"vitest": "^4.1.9"
|
|
19
19
|
},
|
|
20
20
|
"exports": {
|
|
21
21
|
"./package.json": "./package.json",
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
},
|
|
34
34
|
"main": "build/index.js",
|
|
35
35
|
"name": "@esportsplus/typescript",
|
|
36
|
+
"overrides": {
|
|
37
|
+
"postcss": "8.5.15"
|
|
38
|
+
},
|
|
36
39
|
"private": false,
|
|
37
40
|
"repository": {
|
|
38
41
|
"type": "git",
|
|
@@ -40,7 +43,7 @@
|
|
|
40
43
|
},
|
|
41
44
|
"type": "module",
|
|
42
45
|
"types": "build/index.d.ts",
|
|
43
|
-
"version": "0.29.
|
|
46
|
+
"version": "0.29.6",
|
|
44
47
|
"scripts": {
|
|
45
48
|
"bench:run": "vitest bench --run",
|
|
46
49
|
"build": "tsc && tsc-alias",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Approve dependency build scripts (pnpm errors on unapproved ones during install/publish) and trust
|
|
2
|
+
# our own scope so freshly published first-party deps bypass the minimumReleaseAge gate.
|
|
3
|
+
allowBuilds:
|
|
4
|
+
esbuild: true
|
|
5
|
+
minimumReleaseAgeExclude:
|
|
6
|
+
- '@esportsplus/*'
|
|
@@ -136,7 +136,12 @@ function modify(code: string, file: ts.SourceFile, pkg: string, options: ModifyO
|
|
|
136
136
|
for (let i = 0, n = found.length; i < n; i++) {
|
|
137
137
|
for (let [name, alias] of found[i].specifiers) {
|
|
138
138
|
if (!remove || (!remove.has(name) && !remove.has(alias))) {
|
|
139
|
-
|
|
139
|
+
let base = name === alias ? name : `${name} as ${alias}`;
|
|
140
|
+
|
|
141
|
+
// Preserve type-only specifiers with an inline `type` modifier so the merged import
|
|
142
|
+
// stays erasable — otherwise `import type { X }` re-emits as a runtime `import { X }`
|
|
143
|
+
// and fails at load time when X has no runtime export.
|
|
144
|
+
specifiers.add(found[i].typeOnly.has(name) ? `type ${base}` : base);
|
|
140
145
|
}
|
|
141
146
|
}
|
|
142
147
|
}
|
package/src/compiler/imports.ts
CHANGED
|
@@ -4,6 +4,10 @@ import { ts } from '~/index';
|
|
|
4
4
|
type ImportInfo = {
|
|
5
5
|
end: number;
|
|
6
6
|
specifiers: Map<string, string>;
|
|
7
|
+
// propertyName keys that were imported type-only, whether via a type-only clause
|
|
8
|
+
// (`import type { A }`) or an inline specifier (`import { type A }`). Preserved so a rewrite
|
|
9
|
+
// re-emits them as type imports instead of runtime imports.
|
|
10
|
+
typeOnly: Set<string>;
|
|
7
11
|
start: number;
|
|
8
12
|
};
|
|
9
13
|
|
|
@@ -43,7 +47,9 @@ const all = (file: ts.SourceFile, pkg: string): ImportInfo[] => {
|
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
let bindings = stmt.importClause?.namedBindings,
|
|
46
|
-
|
|
50
|
+
declTypeOnly = stmt.importClause?.isTypeOnly ?? false,
|
|
51
|
+
specifiers = new Map<string, string>(),
|
|
52
|
+
typeOnly = new Set<string>();
|
|
47
53
|
|
|
48
54
|
if (bindings && ts.isNamedImports(bindings)) {
|
|
49
55
|
for (let j = 0, m = bindings.elements.length; j < m; j++) {
|
|
@@ -52,10 +58,14 @@ const all = (file: ts.SourceFile, pkg: string): ImportInfo[] => {
|
|
|
52
58
|
propertyName = element.propertyName?.text || name;
|
|
53
59
|
|
|
54
60
|
specifiers.set(propertyName, name);
|
|
61
|
+
|
|
62
|
+
if (declTypeOnly || element.isTypeOnly) {
|
|
63
|
+
typeOnly.add(propertyName);
|
|
64
|
+
}
|
|
55
65
|
}
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
imports.push({ end: stmt.end, specifiers, start: stmt.getStart(file) });
|
|
68
|
+
imports.push({ end: stmt.end, specifiers, start: stmt.getStart(file), typeOnly });
|
|
59
69
|
}
|
|
60
70
|
|
|
61
71
|
return imports;
|
|
@@ -346,6 +346,23 @@ describe('coordinator.transform', () => {
|
|
|
346
346
|
expect(result.code).not.toMatch(/import\s*\{[^}]*reactive[^}]*\}\s*from\s*'my-pkg'/);
|
|
347
347
|
});
|
|
348
348
|
|
|
349
|
+
it('preserves a type-only import when rewriting a package\'s imports', () => {
|
|
350
|
+
let code = "import { html } from 'my-pkg';\nimport type { Renderable } from 'my-pkg';\nlet x = 1;",
|
|
351
|
+
file = parse(code),
|
|
352
|
+
program = makeProgram(file),
|
|
353
|
+
plugin = makePlugin(() => ({
|
|
354
|
+
imports: [{ namespace: 'NS', package: 'my-pkg', remove: ['html'] }]
|
|
355
|
+
})),
|
|
356
|
+
result = coordinator.transform([plugin], code, file, program, '/root', new Map());
|
|
357
|
+
|
|
358
|
+
expect(result.changed).toBe(true);
|
|
359
|
+
expect(result.code).toContain("import * as NS from 'my-pkg';");
|
|
360
|
+
// Renderable must stay type-only (import type { ... } or inline `type Renderable`) so it is
|
|
361
|
+
// erased at runtime — a runtime `import { Renderable }` fails when it has no runtime export.
|
|
362
|
+
expect(result.code).toMatch(/import\s+type\s*\{[^}]*Renderable|import\s*\{[^}]*type\s+Renderable/);
|
|
363
|
+
expect(result.code).not.toMatch(/import\s*\{\s*Renderable\s*\}\s*from\s*'my-pkg'/);
|
|
364
|
+
});
|
|
365
|
+
|
|
349
366
|
// F-TEST-003: Import manipulation integration tests
|
|
350
367
|
|
|
351
368
|
it('adds specifiers to existing import', () => {
|