Skip to content

Commit 7dc3a4b

Browse files
committed
fix: Numerous issues with elision due to new TS features in v5+ (fixes #184)
1 parent 0bb558e commit 7dc3a4b

File tree

11 files changed

+3756
-2376
lines changed

11 files changed

+3756
-2376
lines changed

package.json

+11-8
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
"release": "standard-version",
1212
"--------------": "",
1313
"format": "prettier --write \"{src,test}/**/{*.js,!(*.d).ts}\"",
14-
"clean": "npx -y rimraf dist **/*.tsbuildinfo ./test/projects/nx/dist",
15-
"clean:all": "yarn run clean && npx -y rimraf node_modules **/node_modules **/yarn.lock yarn.lock",
14+
"clean": "npx -y rimraf -g dist **/*.tsbuildinfo ./test/projects/nx/dist",
15+
"clean:all": "yarn run clean && npx -y rimraf -g node_modules **/node_modules **/yarn.lock yarn.lock",
1616
"reset": "yarn run clean:all && yarn install",
1717
"-------------- ": "",
18-
"prebuild": "npx -y rimraf dist",
18+
"prebuild": "npx -y rimraf -g dist",
1919
"install:tests": "cd test && yarn install",
2020
"prepare": "yarn run install:tests"
2121
},
@@ -40,9 +40,12 @@
4040
},
4141
"license": "MIT",
4242
"contributors": [
43-
"Daniel Perez Alvarez <danielpza@protonmail.com>",
44-
"Ron S. <ron@nonara.com>"
43+
"Daniel Perez Alvarez <danielpza@protonmail.com>"
4544
],
45+
"author": {
46+
"name": "Ron S.",
47+
"url": "https://twitter.com/Ron"
48+
},
4649
"files": [
4750
"dist",
4851
"types",
@@ -57,13 +60,13 @@
5760
"@types/node": "^18.11.2",
5861
"jest": "^29.3.1",
5962
"prettier": "^2.7.1",
60-
"rimraf": "^3.0.2",
63+
"rimraf": "^5.0.5",
6164
"standard-version": "^9.5.0",
6265
"@types/ts-expose-internals": "npm:ts-expose-internals@4.9.4",
6366
"ts-jest": "^29.0.3",
6467
"ts-node": "^10.9.1",
65-
"ts-patch": "^2.1.0",
66-
"typescript": "^4.9.4"
68+
"ts-patch": "^3.1.2",
69+
"typescript": "^5.3.3"
6770
},
6871
"peerDependencies": {
6972
"typescript": ">=3.6.5"

src/utils/elide-import-export.ts

+127-25
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
/**
2+
* ----------------------------------------------------------------------------
3+
* UPDATE:
4+
*
5+
* TODO - In next major version, we can remove this file entirely due to TS PR 57223
6+
* https://github.com/microsoft/TypeScript/pull/57223
7+
* ----------------------------------------------------------------------------
8+
*
29
* This file and its contents are due to an issue in TypeScript (affecting *at least* up to 4.1) which causes type
310
* elision to break during emit for nodes which have been transformed. Specifically, if the 'original' property is set,
411
* elision functionality no longer works.
@@ -9,6 +16,7 @@
916
* the clause with the properly elided information
1017
*
1118
* Issues:
19+
* @see https://github.com/LeDDGroup/typescript-transform-paths/issues/184
1220
* @see https://github.com/microsoft/TypeScript/issues/40603
1321
* @see https://github.com/microsoft/TypeScript/issues/31446
1422
*
@@ -28,15 +36,21 @@
2836
* import { A, B } from './b'
2937
* export { A } from './b'
3038
*/
31-
import { ImportOrExportClause, ImportOrExportDeclaration, VisitorContext } from "../types";
39+
import { ImportOrExportDeclaration, VisitorContext } from "../types";
3240
import {
33-
ExportDeclaration,
41+
Debug,
42+
EmitResolver,
3443
ExportSpecifier,
3544
ImportClause,
36-
ImportDeclaration,
45+
ImportsNotUsedAsValues,
3746
ImportSpecifier,
47+
isInJSFile,
48+
NamedExportBindings,
3849
NamedExports,
3950
NamedImportBindings,
51+
NamespaceExport,
52+
Node,
53+
StringLiteral,
4054
Visitor,
4155
VisitResult,
4256
} from "typescript";
@@ -51,19 +65,21 @@ import {
5165
*
5266
* @returns import or export clause or undefined if it entire declaration should be elided
5367
*/
54-
export function elideImportOrExportClause<T extends ImportOrExportDeclaration>(
68+
export function elideImportOrExportDeclaration<T extends ImportOrExportDeclaration>(
5569
context: VisitorContext,
56-
node: T
57-
): (T extends ImportDeclaration ? ImportDeclaration["importClause"] : ExportDeclaration["exportClause"]) | undefined;
70+
node: T,
71+
newModuleSpecifier: StringLiteral,
72+
resolver: EmitResolver
73+
): T | undefined;
5874

59-
export function elideImportOrExportClause(
75+
export function elideImportOrExportDeclaration(
6076
context: VisitorContext,
61-
node: ImportOrExportDeclaration
62-
): ImportOrExportClause | undefined {
63-
const { tsInstance, transformationContext, factory } = context;
64-
const resolver = transformationContext.getEmitResolver();
65-
// Resolver may not be present if run manually (without Program)
66-
if (!resolver) return tsInstance.isImportDeclaration(node) ? node.importClause : node.exportClause;
77+
node: ImportOrExportDeclaration,
78+
newModuleSpecifier: StringLiteral,
79+
resolver: EmitResolver
80+
): ImportOrExportDeclaration | undefined {
81+
const { tsInstance, factory } = context;
82+
const { compilerOptions } = context;
6783

6884
const {
6985
visitNode,
@@ -72,20 +88,77 @@ export function elideImportOrExportClause(
7288
SyntaxKind,
7389
visitNodes,
7490
isNamedExportBindings,
91+
// 3.8 does not have this, so we have to define it ourselves
92+
// isNamespaceExport,
93+
isIdentifier,
7594
isExportSpecifier,
7695
} = tsInstance;
7796

97+
const isNamespaceExport = tsInstance.isNamespaceExport ?? ((node: Node): node is NamespaceExport => node.kind === SyntaxKind.NamespaceExport);
98+
7899
if (tsInstance.isImportDeclaration(node)) {
79-
if (node.importClause!.isTypeOnly) return undefined;
80-
return visitNode(node.importClause, <Visitor>visitImportClause);
100+
// Do not elide a side-effect only import declaration.
101+
// import "foo";
102+
if (!node.importClause) return node.importClause;
103+
104+
// Always elide type-only imports
105+
if (node.importClause.isTypeOnly) return undefined;
106+
107+
const importClause = visitNode(node.importClause, <Visitor>visitImportClause);
108+
109+
if (
110+
importClause ||
111+
compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Preserve ||
112+
compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error
113+
)
114+
return factory.updateImportDeclaration(
115+
node,
116+
/*modifiers*/ undefined,
117+
importClause,
118+
newModuleSpecifier,
119+
// This will be changed in the next release of TypeScript, but by that point we can drop elision entirely
120+
(node as any).attributes || node.assertClause
121+
);
122+
else return undefined;
81123
} else {
82124
if (node.isTypeOnly) return undefined;
83-
return visitNode(node.exportClause, <Visitor>visitNamedExports, isNamedExportBindings);
125+
126+
if (!node.exportClause || node.exportClause.kind === SyntaxKind.NamespaceExport) {
127+
// never elide `export <whatever> from <whereever>` declarations -
128+
// they should be kept for sideffects/untyped exports, even when the
129+
// type checker doesn't know about any exports
130+
return node;
131+
}
132+
133+
const allowEmpty =
134+
!!compilerOptions.verbatimModuleSyntax ||
135+
(!!node.moduleSpecifier &&
136+
(compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Preserve ||
137+
compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error));
138+
139+
const exportClause = visitNode(
140+
node.exportClause,
141+
<Visitor>((bindings: NamedExportBindings) => visitNamedExportBindings(bindings, allowEmpty)),
142+
isNamedExportBindings
143+
);
144+
145+
return exportClause
146+
? factory.updateExportDeclaration(
147+
node,
148+
/*modifiers*/ undefined,
149+
node.isTypeOnly,
150+
exportClause,
151+
newModuleSpecifier,
152+
// This will be changed in the next release of TypeScript, but by that point we can drop elision entirely
153+
(node as any).attributes || node.assertClause
154+
)
155+
: undefined;
84156
}
85157

86158
/* ********************************************************* *
87159
* Helpers
88160
* ********************************************************* */
161+
89162
// The following visitors are adapted from the TS source-base src/compiler/transformers/ts
90163

91164
/**
@@ -95,7 +168,7 @@ export function elideImportOrExportClause(
95168
*/
96169
function visitImportClause(node: ImportClause): VisitResult<ImportClause> {
97170
// Elide the import clause if we elide both its name and its named bindings.
98-
const name = resolver.isReferencedAliasDeclaration(node) ? node.name : undefined;
171+
const name = shouldEmitAliasDeclaration(node) ? node.name : undefined;
99172
const namedBindings = visitNode(node.namedBindings, <Visitor>visitNamedImportBindings, isNamedImportBindings);
100173
return name || namedBindings
101174
? factory.updateImportClause(node, /*isTypeOnly*/ false, name, namedBindings)
@@ -110,11 +183,17 @@ export function elideImportOrExportClause(
110183
function visitNamedImportBindings(node: NamedImportBindings): VisitResult<NamedImportBindings> {
111184
if (node.kind === SyntaxKind.NamespaceImport) {
112185
// Elide a namespace import if it is not referenced.
113-
return resolver.isReferencedAliasDeclaration(node) ? node : undefined;
186+
return shouldEmitAliasDeclaration(node) ? node : undefined;
114187
} else {
115188
// Elide named imports if all of its import specifiers are elided.
189+
const allowEmpty =
190+
compilerOptions.verbatimModuleSyntax ||
191+
(compilerOptions.preserveValueImports &&
192+
(compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Preserve ||
193+
compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error));
194+
116195
const elements = visitNodes(node.elements, <Visitor>visitImportSpecifier, isImportSpecifier);
117-
return tsInstance.some(elements) ? factory.updateNamedImports(node, elements) : undefined;
196+
return allowEmpty || tsInstance.some(elements) ? factory.updateNamedImports(node, elements) : undefined;
118197
}
119198
}
120199

@@ -125,19 +204,30 @@ export function elideImportOrExportClause(
125204
*/
126205
function visitImportSpecifier(node: ImportSpecifier): VisitResult<ImportSpecifier> {
127206
// Elide an import specifier if it is not referenced.
128-
return resolver.isReferencedAliasDeclaration(node) ? node : undefined;
207+
return !node.isTypeOnly && shouldEmitAliasDeclaration(node) ? node : undefined;
129208
}
130209

131210
/**
132211
* Visits named exports, eliding it if it does not contain an export specifier that
133212
* resolves to a value.
134-
*
135-
* @param node The named exports node.
136213
*/
137-
function visitNamedExports(node: NamedExports): VisitResult<NamedExports> {
214+
function visitNamedExports(node: NamedExports, allowEmpty: boolean): VisitResult<NamedExports> | undefined {
138215
// Elide the named exports if all of its export specifiers were elided.
139216
const elements = visitNodes(node.elements, <Visitor>visitExportSpecifier, isExportSpecifier);
140-
return tsInstance.some(elements) ? factory.updateNamedExports(node, elements) : undefined;
217+
return allowEmpty || tsInstance.some(elements) ? factory.updateNamedExports(node, elements) : undefined;
218+
}
219+
220+
function visitNamedExportBindings(
221+
node: NamedExportBindings,
222+
allowEmpty: boolean
223+
): VisitResult<NamedExportBindings> | undefined {
224+
return isNamespaceExport(node) ? visitNamespaceExports(node) : visitNamedExports(node, allowEmpty);
225+
}
226+
227+
function visitNamespaceExports(node: NamespaceExport): VisitResult<NamespaceExport> {
228+
// Note: This may not work entirely properly, more likely it's just extraneous, but this won't matter soon,
229+
// as we'll be removing elision entirely
230+
return factory.updateNamespaceExport(node, Debug.checkDefined(visitNode(node.name, (n) => n, isIdentifier)));
141231
}
142232

143233
/**
@@ -147,7 +237,19 @@ export function elideImportOrExportClause(
147237
*/
148238
function visitExportSpecifier(node: ExportSpecifier): VisitResult<ExportSpecifier> {
149239
// Elide an export specifier if it does not reference a value.
150-
return resolver.isValueAliasDeclaration(node) ? node : undefined;
240+
return !node.isTypeOnly && (compilerOptions.verbatimModuleSyntax || resolver.isValueAliasDeclaration(node))
241+
? node
242+
: undefined;
243+
}
244+
245+
function shouldEmitAliasDeclaration(node: Node): boolean {
246+
return (
247+
!!compilerOptions.verbatimModuleSyntax ||
248+
isInJSFile(node) ||
249+
(compilerOptions.preserveValueImports
250+
? resolver.isValueAliasDeclaration(node)
251+
: resolver.isReferencedAliasDeclaration(node))
252+
);
151253
}
152254
}
153255

src/visitor.ts

+24-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import ts from "typescript";
22
import { VisitorContext } from "./types";
3-
import { elideImportOrExportClause, resolvePathAndUpdateNode } from "./utils";
3+
import { elideImportOrExportDeclaration, resolvePathAndUpdateNode } from "./utils";
44

55
/* ****************************************************************************************************************** *
66
* Helpers
@@ -108,15 +108,16 @@ export function nodeVisitor(this: VisitorContext, node: ts.Node): ts.Node | unde
108108
*/
109109
if (tsInstance.isImportDeclaration(node) && node.moduleSpecifier && tsInstance.isStringLiteral(node.moduleSpecifier))
110110
return resolvePathAndUpdateNode(this, node, node.moduleSpecifier.text, (p) => {
111-
let importClause = node.importClause;
112-
113-
if (!this.isDeclarationFile && importClause?.namedBindings) {
114-
const updatedImportClause = elideImportOrExportClause(this, node);
115-
if (!updatedImportClause) return undefined; // No imports left, elide entire declaration
116-
importClause = updatedImportClause;
111+
// TODO - In next major version, we can remove this entirely due to TS PR 57223
112+
// see: https://github.com/microsoft/TypeScript/pull/57223
113+
// We should at least skip this if doing a minor version update if the ts version is high enough to not need it
114+
if (!this.isDeclarationFile && node.importClause?.namedBindings) {
115+
const resolver = transformationContext.getEmitResolver();
116+
// If run in "manual" mode without a Program, we won't have a resolver, so we can't elide
117+
if (resolver) return elideImportOrExportDeclaration(this, node, p, resolver);
117118
}
118119

119-
return factory.updateImportDeclaration(node, node.modifiers, importClause, p, node.assertClause);
120+
return factory.updateImportDeclaration(node, node.modifiers, node.importClause, p, node.assertClause);
120121
});
121122

122123
/**
@@ -126,15 +127,23 @@ export function nodeVisitor(this: VisitorContext, node: ts.Node): ts.Node | unde
126127
*/
127128
if (tsInstance.isExportDeclaration(node) && node.moduleSpecifier && tsInstance.isStringLiteral(node.moduleSpecifier))
128129
return resolvePathAndUpdateNode(this, node, node.moduleSpecifier.text, (p) => {
129-
let exportClause = node.exportClause;
130-
131-
if (!this.isDeclarationFile && exportClause && tsInstance.isNamedExports(exportClause)) {
132-
const updatedExportClause = elideImportOrExportClause(this, node);
133-
if (!updatedExportClause) return undefined; // No export left, elide entire declaration
134-
exportClause = updatedExportClause;
130+
// TODO - In next major version, we can remove this entirely due to TS PR 57223
131+
// see: https://github.com/microsoft/TypeScript/pull/57223
132+
// We should at least skip this if doing a minor version update if the ts version is high enough to not need it
133+
if (!this.isDeclarationFile && node.exportClause && tsInstance.isNamedExports(node.exportClause)) {
134+
const resolver = transformationContext.getEmitResolver();
135+
// If run in "manual" mode without a Program, we won't have a resolver, so we can't elide
136+
if (resolver) return elideImportOrExportDeclaration(this, node, p, resolver);
135137
}
136138

137-
return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, exportClause, p, node.assertClause);
139+
return factory.updateExportDeclaration(
140+
node,
141+
node.modifiers,
142+
node.isTypeOnly,
143+
node.exportClause,
144+
p,
145+
node.assertClause
146+
);
138147
});
139148

140149
/**

test/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
"ts-patch": "link:../node_modules/ts-patch",
1414
"ts-node": "link:../node_modues/ts-node",
1515
"tsp1": "npm:ts-patch@1.*.*",
16+
"tsp2": "npm:ts-patch@2.*.*",
1617
"@nrwl/cli": "^15.0.0",
1718
"@nrwl/js": "^15.0.0",
1819
"@nrwl/node": "^15.0.0",
1920
"@nrwl/workspace": "^15.0.0",
20-
"nx": "^15.0.0"
21+
"nx": "^15.0.0",
22+
"strip-ansi": "^6.0.1"
2123
},
2224
"workspaces": [
2325
"projects/*"

test/prepare.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@ const fs = require("fs");
22
const path = require("path");
33
const tsPatch = require("ts-patch");
44
const tsp1 = require("tsp1");
5+
const tsp2 = require("tsp2");
56

67
/* ****************************************************************************************************************** *
78
* Config
89
* ****************************************************************************************************************** */
910

1011
const rootDir = __dirname;
11-
const tsDirs = ["typescript-three", "typescript-four-seven", "typescript"];
12+
const tsDirs = [
13+
"typescript-three",
14+
"typescript-four-seven",
15+
"typescript",
16+
];
1217

1318
/* ****************************************************************************************************************** *
1419
* Patch TS Modules
@@ -24,4 +29,5 @@ for (const tsDirName of tsDirs) {
2429
// Patch discovered modules
2530
for (const [dirName, dir] of baseDirs)
2631
if (dirName === "typescript-three") tsp1.patch(["tsc.js", "typescript.js"], { basedir: dir });
32+
else if (dirName === "typescript-four-seven") tsp2.patch(["tsc.js", "typescript.js"], { dir });
2733
else tsPatch.patch(["tsc.js", "typescript.js"], { dir });

0 commit comments

Comments
 (0)