Skip to content

Commit e38c622

Browse files
committed
feat: Add outputMode option for esm output
1 parent c70f195 commit e38c622

18 files changed

+643
-329
lines changed

.editorconfig

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ ij_javascript_align_multiline_extends_list = false
3434
ij_javascript_align_multiline_for = true
3535
ij_javascript_align_multiline_parameters = true
3636
ij_javascript_align_multiline_parameters_in_calls = false
37-
ij_javascript_align_multiline_ternary_operation = false
37+
ij_javascript_align_multiline_ternary_operation = true
3838
ij_javascript_align_object_properties = 0
3939
ij_javascript_align_union_types = false
4040
ij_javascript_align_var_statements = 0
@@ -218,7 +218,7 @@ ij_typescript_align_multiline_extends_list = false
218218
ij_typescript_align_multiline_for = true
219219
ij_typescript_align_multiline_parameters = true
220220
ij_typescript_align_multiline_parameters_in_calls = false
221-
ij_typescript_align_multiline_ternary_operation = false
221+
ij_typescript_align_multiline_ternary_operation = true
222222
ij_typescript_align_object_properties = 0
223223
ij_typescript_align_union_types = false
224224
ij_typescript_align_var_statements = 0
@@ -363,7 +363,7 @@ ij_typescript_ternary_operation_signs_on_next_line = false
363363
ij_typescript_ternary_operation_wrap = off
364364
ij_typescript_union_types_wrap = on_every_item
365365
ij_typescript_use_chained_calls_group_indents = false
366-
ij_typescript_use_double_quotes = false
366+
ij_typescript_use_double_quotes = true
367367
ij_typescript_use_explicit_js_extension = global
368368
ij_typescript_use_path_mapping = always
369369
ij_typescript_use_public_modifier = false

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
"--------------": "",
1313
"format": "prettier --write \"{src,test}/**/{*.js,!(*.d).ts}\"",
1414
"clean": "rimraf dist **/*.tsbuildinfo",
15-
"clean:all": "yarn run clean && rimraf node_modules test/node_modules test/.yarn-cache",
16-
"reset": "yarn run clean:all && yarn install",
15+
"clean:all": "yarn run clean && rimraf node_modules **/node_modules test/.yarn-cache",
16+
"reset": "yarn run clean:all && yarn install && yarn build",
1717
"-------------- ": "",
1818
"prebuild": "rimraf dist",
1919
"install:tests": "cd test && yarn install && cd projects/extras && yarn install",
@@ -61,7 +61,7 @@
6161
"ts-expose-internals": "^4.4.3",
6262
"ts-jest": "^27.0.4",
6363
"ts-node": "^10.1.0",
64-
"ts-patch": "^1.4.2",
64+
"ts-patch": "^1.4.4",
6565
"typescript": "^4.4.3"
6666
},
6767
"peerDependencies": {

src/transformer.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@ function getTsProperties(args: Parameters<typeof transformer>) {
2020
let fileNames: readonly string[] | undefined;
2121
let isTsNode = false;
2222

23-
const { 0: program, 2: extras, 3: manualTransformOptions } = args;
23+
const [program, pluginConfig, extras, manualTransformOptions] = args;
2424

2525
tsInstance = extras?.ts ?? ts;
2626
compilerOptions = manualTransformOptions?.compilerOptions!;
27+
const config = {
28+
...pluginConfig,
29+
outputMode: pluginConfig?.outputMode === "esm" ? <const>"esm" : <const>"commonjs",
30+
};
2731

2832
if (program) {
2933
compilerOptions ??= program.getCompilerOptions();
@@ -41,7 +45,7 @@ function getTsProperties(args: Parameters<typeof transformer>) {
4145
fileNames = tsNodeProps.fileNames;
4246
}
4347

44-
return { tsInstance, compilerOptions, fileNames, isTsNode };
48+
return { tsInstance, compilerOptions, fileNames, isTsNode, config };
4549
}
4650

4751
// endregion
@@ -68,11 +72,11 @@ export default function transformer(
6872
tsInstance,
6973
compilerOptions,
7074
fileNames,
71-
isTsNode
75+
isTsNode,
76+
config
7277
} = getTsProperties([ program, pluginConfig, transformerExtras, manualTransformOptions ]);
7378

7479
const rootDirs = compilerOptions.rootDirs?.filter(path.isAbsolute);
75-
const config: TsTransformPathsConfig = pluginConfig ?? {};
7680
const getCanonicalFileName = tsInstance.createGetCanonicalFileName(tsInstance.sys.useCaseSensitiveFileNames);
7781

7882
let emitHost = transformationContext.getEmitHost();
@@ -121,6 +125,7 @@ export default function transformer(
121125
return nodeVisitor.bind(this);
122126
},
123127
factory: createHarmonyFactory(tsTransformPathsContext),
128+
outputMode: config.outputMode === 'esm' && sourceFile.isDeclarationFile ? 'esm' : 'commonjs'
124129
};
125130

126131
return tsInstance.visitEachChild(sourceFile, visitorContext.getVisitor(), transformationContext);

src/types.ts

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
1-
import tsThree from "./declarations/typescript3";
21
import ts, { CompilerOptions, EmitHost, Pattern, SourceFile } from "typescript";
3-
import { PluginConfig } from "ts-patch";
42
import { HarmonyFactory } from "./utils/harmony-factory";
53
import { IMinimatch } from "minimatch";
4+
import { RequireSome } from "./utils";
65

76
/* ****************************************************************************************************************** */
87
// region: TS Types
98
/* ****************************************************************************************************************** */
109

11-
export type TypeScriptLatest = typeof ts;
12-
export type TypeScriptThree = typeof tsThree;
1310
export type ImportOrExportDeclaration = ts.ImportDeclaration | ts.ExportDeclaration;
1411
export type ImportOrExportClause = ts.ImportDeclaration["importClause"] | ts.ExportDeclaration["exportClause"];
12+
export type TransformerExtras = {
13+
ts: typeof ts;
14+
}
1515

1616
// endregion
1717

1818
/* ****************************************************************************************************************** */
1919
// region: Config
2020
/* ****************************************************************************************************************** */
2121

22-
export interface TsTransformPathsConfig extends PluginConfig {
22+
export interface TsTransformPathsConfig {
2323
readonly useRootDirs?: boolean;
2424
readonly exclude?: string[];
25+
readonly outputMode?: "commonjs" | "esm";
2526
}
2627

2728
// endregion
@@ -31,33 +32,37 @@ export interface TsTransformPathsConfig extends PluginConfig {
3132
/* ****************************************************************************************************************** */
3233

3334
export interface TsTransformPathsContext {
34-
/**
35-
* TS Instance passed from ts-patch / ttypescript with TS4+ typings
36-
*/
37-
readonly tsInstance: TypeScriptLatest;
38-
/**
39-
* TS Instance passed from ts-patch / ttypescript with TS3 typings
40-
*/
41-
readonly tsThreeInstance: TypeScriptThree;
35+
readonly tsInstance: typeof ts;
4236
readonly tsFactory?: ts.NodeFactory;
43-
readonly program?: ts.Program | tsThree.Program;
44-
readonly config: TsTransformPathsConfig;
37+
readonly program?: ts.Program;
38+
readonly config: RequireSome<TsTransformPathsConfig, "outputMode">;
4539
readonly compilerOptions: CompilerOptions;
46-
readonly elisionMap: Map<ts.SourceFile, Map<ImportOrExportDeclaration, ImportOrExportDeclaration>>;
4740
readonly transformationContext: ts.TransformationContext;
4841
readonly rootDirs?: string[];
49-
readonly excludeMatchers: IMinimatch[] | undefined;
50-
readonly outputFileNamesCache: Map<SourceFile, string>;
42+
readonly isTsNode: boolean;
43+
readonly isTranspileOnly: boolean;
44+
45+
/** @internal - Do not remove internal flag — this uses an internal TS type */
5146
readonly pathsPatterns: readonly (string | Pattern)[] | undefined;
47+
/** @internal - Do not remove internal flag — this uses an internal TS type */
5248
readonly emitHost: EmitHost;
53-
readonly isTsNode: boolean;
49+
50+
/** @internal */
51+
readonly elisionMap: Map<ts.SourceFile, Map<ImportOrExportDeclaration, ImportOrExportDeclaration>>;
52+
/** @internal */
53+
readonly excludeMatchers: IMinimatch[] | undefined;
54+
/** @internal */
55+
readonly outputFileNamesCache: Map<SourceFile, string>;
5456
}
5557

5658
export interface VisitorContext extends TsTransformPathsContext {
5759
readonly factory: HarmonyFactory;
5860
readonly sourceFile: ts.SourceFile;
5961
readonly isDeclarationFile: boolean;
6062
readonly originalSourceFile: ts.SourceFile;
63+
readonly outputMode: 'esm' | 'commonjs';
64+
65+
/** @internal */
6166
getVisitor(): (node: ts.Node) => ts.VisitResult<ts.Node>;
6267
}
6368

src/utils/general-utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ export const isBaseDir = (baseDir: string, testDir: string): boolean => {
1212
return relative ? !relative.startsWith("..") && !path.isAbsolute(relative) : true;
1313
};
1414
export const maybeAddRelativeLocalPrefix = (p: string) => (p[0] === "." ? p : `./${p}`);
15+
16+
export type RequireSome<T, K extends keyof T> = T & Pick<Required<T>, K>;

src/utils/path.ts

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// noinspection JSUnusedLocalSymbols,JSUnusedAssignment
2+
3+
import path from "path";
4+
import { normalizePath, Pattern, removePrefix, removeSuffix, ResolvedModuleFull } from "typescript";
5+
6+
/* ****************************************************************************************************************** */
7+
// region: Types
8+
/* ****************************************************************************************************************** */
9+
10+
export interface OutputPathDetail {
11+
isImplicitExtension: boolean
12+
isExternalLibraryImport: boolean
13+
resolvedExt: string | undefined;
14+
resolvedPath: string;
15+
outputPath: string | undefined;
16+
suppliedExt: string | undefined;
17+
implicitPath?: string;
18+
/** Package name from package.json */
19+
packageName?: string;
20+
/** Package name as written (could differ due to npm aliasing) */
21+
suppliedPackageName?: string
22+
suppliedPackagePath?: string
23+
packagePath?: string;
24+
tsPathMatch?: string;
25+
}
26+
27+
// endregion
28+
29+
/* ****************************************************************************************************************** */
30+
// region: Helpers
31+
/* ****************************************************************************************************************** */
32+
33+
const pkgRegex = /^((@[^/]+\/[^/@]+)|([^@/]+))(?:\/([^@]+?))?$/;
34+
35+
/** @internal */
36+
function getPaths(supplied: string, resolved: string) {
37+
let endMatchPos = 0;
38+
for (let i = 0; i < supplied.length && i < resolved.length; i++) {
39+
if (supplied[i] !== resolved[i]) {
40+
endMatchPos = i;
41+
break;
42+
}
43+
}
44+
45+
const outputPath = supplied.slice(0, endMatchPos);
46+
const implicitPath = resolved.slice(endMatchPos);
47+
48+
return { outputPath, implicitPath };
49+
}
50+
51+
function getPackagePaths(moduleName: string, suppliedExt: string, subModule: string) {
52+
const { 1: suppliedPackageName, 2: suppliedPackagePath } = pkgRegex.exec(moduleName)!;
53+
54+
const packagePathNoExt = fixupPartialPath(suppliedPackagePath, suppliedExt);
55+
56+
if (!subModule) return { outputPath: packagePathNoExt };
57+
58+
const subModuleNoExt = fixupPartialPath(subModule, path.extname(subModule));
59+
60+
return {
61+
...getPaths(packagePathNoExt, subModuleNoExt),
62+
suppliedPackageName,
63+
suppliedPackagePath
64+
};
65+
}
66+
67+
function getModulePaths(
68+
moduleName: string,
69+
suppliedExt: string,
70+
resolvedFileName: string,
71+
resolvedExt: string,
72+
tsPathMatch: string | Pattern | undefined
73+
) {
74+
// In this case, the file ending is a fixed path, so no further information needs to be determined
75+
if (typeof tsPathMatch === "string" || tsPathMatch?.suffix) return { outputPath: void 0 };
76+
77+
const resolvedFileNameNoExt = fixupPartialPath(resolvedFileName, resolvedExt);
78+
const modulePathNoExt = tsPathMatch?.prefix
79+
? fixupPartialPath(removePrefix(moduleName, tsPathMatch?.prefix), suppliedExt)
80+
: fixupPath(moduleName, suppliedExt);
81+
82+
return getPaths(modulePathNoExt, resolvedFileNameNoExt);
83+
}
84+
85+
function getTsPathMatch(match: string | Pattern): string {
86+
return typeof match === "string" ? match : [match.prefix, match.suffix].join("*");
87+
}
88+
89+
// endregion
90+
91+
/* ****************************************************************************************************************** */
92+
// region: Utils
93+
/* ****************************************************************************************************************** */
94+
95+
export function joinPaths(...paths: (string | undefined)[]): string {
96+
return normalizePath(path.join('', ...paths.filter(p => typeof p === 'string') as string[]));
97+
}
98+
99+
/**
100+
* Remove leading or trailing slashes
101+
* @p path to fix
102+
* @extName extname to remove
103+
*/
104+
export function fixupPartialPath(p: string, extName?: string): string {
105+
p = p.replace(/^[/\\]*(.+?)[/\\]*$/g, "$1");
106+
if (extName && p.slice(-extName.length) === extName) p = p.slice(0, p.length - extName.length);
107+
return p;
108+
}
109+
110+
/**
111+
* Remove trailing slashes
112+
* @p path to fix
113+
* @extName extname to remove
114+
*/
115+
export function fixupPath(p: string, extName?: string): string {
116+
p = p.replace(/^(.+?)[/\\]*$/g, "$1");
117+
if (extName && p.slice(-extName.length) === extName) p = p.slice(0, p.length - extName.length);
118+
return p;
119+
}
120+
121+
/** @internal — Uses internal TS type */
122+
export function getOutputPathDetail(
123+
moduleName: string,
124+
resolvedModule: ResolvedModuleFull,
125+
pathMatch: string | Pattern | undefined
126+
): OutputPathDetail {
127+
moduleName = fixupPath(moduleName);
128+
let suppliedExtName = path.extname(moduleName);
129+
const suppliedBaseName = path.basename(moduleName);
130+
let suppliedBaseNameNoExt = path.basename(moduleName, suppliedExtName);
131+
132+
const resolvedFileName = resolvedModule.originalPath ?? resolvedModule.resolvedFileName;
133+
const resolvedExtName = path.extname(resolvedFileName);
134+
const resolvedFileNameNoExt = removeSuffix(resolvedFileName, resolvedExtName);
135+
const resolvedBaseName = path.basename(resolvedFileName);
136+
const resolvedBaseNameNoExt = path.basename(resolvedFileName, suppliedExtName);
137+
138+
const tsPathMatch = pathMatch && getTsPathMatch(pathMatch);
139+
140+
const { isExternalLibraryImport, packageId } = resolvedModule;
141+
const packageName = packageId?.name;
142+
const packageFileName = packageId && fixupPartialPath(packageId.subModuleName);
143+
const packageExtName = packageFileName && path.extname(packageFileName);
144+
const packageFileNameNoExt = packageFileName && removeSuffix(packageFileName, resolvedExtName);
145+
const packageBaseName = packageFileName && path.basename(packageFileName);
146+
const packageBaseNameNoExt = packageFileName && path.basename(packageFileName, packageExtName);
147+
148+
const effectiveResolvedFileName = packageFileNameNoExt || resolvedFileNameNoExt;
149+
const effectiveResolvedBaseName = packageBaseName || resolvedBaseName;
150+
const effectiveResolvedBaseNameNoExt = packageBaseNameNoExt || resolvedBaseNameNoExt;
151+
const effectiveResolvedExtName = packageExtName || resolvedExtName;
152+
153+
// Detect and fix invalid extname due to implicit ext (ie. `file.accounting.ts` could decide `accounting` is the extension)
154+
if (suppliedExtName && effectiveResolvedBaseNameNoExt && suppliedBaseName && effectiveResolvedBaseNameNoExt === suppliedBaseName) {
155+
suppliedBaseNameNoExt = suppliedBaseName;
156+
suppliedExtName = "";
157+
}
158+
159+
const isImplicitExtension = !suppliedExtName;
160+
161+
const pathDetail = resolvedModule.isExternalLibraryImport
162+
? getPackagePaths(moduleName, suppliedExtName, packageId!.subModuleName)
163+
: getModulePaths(moduleName, suppliedExtName, resolvedFileName, resolvedExtName, tsPathMatch);
164+
165+
return {
166+
isImplicitExtension,
167+
isExternalLibraryImport: !!isExternalLibraryImport,
168+
resolvedExt: effectiveResolvedExtName,
169+
suppliedExt: suppliedExtName,
170+
resolvedPath: effectiveResolvedFileName,
171+
tsPathMatch,
172+
packageName,
173+
packagePath: packageFileName,
174+
...pathDetail,
175+
};
176+
}
177+
178+
// endregion

0 commit comments

Comments
 (0)