@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.
@@ -17,7 +17,7 @@ jobs:
17
17
  runs-on: ubuntu-latest
18
18
  steps:
19
19
  - name: checkout repo
20
- uses: actions/checkout@v4
20
+ uses: actions/checkout@v7
21
21
  with:
22
22
  fetch-depth: 0
23
23
  - name: bump version
@@ -20,13 +20,13 @@ jobs:
20
20
  build:
21
21
  runs-on: ubuntu-latest
22
22
  steps:
23
- - uses: actions/checkout@v4
24
- - uses: pnpm/action-setup@v4
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@v4
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@v2
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@v4
28
- - uses: pnpm/action-setup@v4
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@v4
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
- specifiers.add(name === alias ? name : `${name} as ${alias}`);
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
  }
@@ -2,6 +2,7 @@ import { ts } from '../index.js';
2
2
  type ImportInfo = {
3
3
  end: number;
4
4
  specifiers: Map<string, string>;
5
+ typeOnly: Set<string>;
5
6
  start: number;
6
7
  };
7
8
  type ModifyOptions = {
@@ -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.27.3",
12
- "@types/node": "^25.6.0",
13
- "tsc-alias": "^1.8.16",
14
- "typescript": "^6.0.2"
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.8",
18
- "vitest": "^4.1.4"
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.1",
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
- specifiers.add(name === alias ? name : `${name} as ${alias}`);
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
  }
@@ -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
- specifiers = new Map<string, string>();
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', () => {
@@ -1,3 +1,6 @@
1
1
  {
2
+ "compilerOptions": {
3
+ "types": ["node"]
4
+ },
2
5
  "extends": "./tsconfig.base.json"
3
6
  }