Skip to content

Commit 8cab30d

Browse files
nonaraRon S
authored and
Ron S
committed
feat: Added @transform-path and @no-transform-path tags for custom statement level transformation
1 parent 29c43bb commit 8cab30d

File tree

3 files changed

+175
-60
lines changed

3 files changed

+175
-60
lines changed

src/utils/resolve-path-update-node.ts

Lines changed: 99 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -17,63 +17,114 @@ export function resolvePathAndUpdateNode(
1717
moduleName: string,
1818
updaterFn: (newPath: ts.StringLiteral) => ts.Node | tsThree.Node | undefined
1919
): ts.Node | undefined {
20-
const { sourceFile, compilerOptions, tsInstance, config, rootDirs, implicitExtensions, factory } = context;
21-
22-
/* Have Compiler API attempt to resolve */
23-
const { resolvedModule, failedLookupLocations } = tsInstance.resolveModuleName(
24-
moduleName,
25-
sourceFile.fileName,
26-
compilerOptions,
27-
tsInstance.sys
28-
);
29-
30-
if (resolvedModule?.isExternalLibraryImport) return node;
31-
32-
let outputPath: string;
33-
if (!resolvedModule) {
34-
const maybeURL = failedLookupLocations[0];
35-
if (!isURL(maybeURL)) return node;
36-
outputPath = maybeURL;
37-
} else {
38-
const { extension, resolvedFileName } = resolvedModule;
20+
const { sourceFile, compilerOptions, tsInstance, config, implicitExtensions, factory } = context;
21+
const tags = getStatementTags();
3922

40-
const fileName = sourceFile.fileName;
41-
let filePath = tsInstance.normalizePath(path.dirname(sourceFile.fileName));
42-
let modulePath = path.dirname(resolvedFileName);
43-
44-
/* Handle rootDirs mapping */
45-
if (config.useRootDirs && rootDirs) {
46-
let fileRootDir = "";
47-
let moduleRootDir = "";
48-
for (const rootDir of rootDirs) {
49-
if (isBaseDir(rootDir, resolvedFileName) && rootDir.length > moduleRootDir.length) moduleRootDir = rootDir;
50-
if (isBaseDir(rootDir, fileName) && rootDir.length > fileRootDir.length) fileRootDir = rootDir;
51-
}
23+
// Skip if @no-transform-path specified
24+
if (tags?.shouldSkip) return node;
5225

53-
/* Remove base dirs to make relative to root */
54-
if (fileRootDir && moduleRootDir) {
55-
filePath = path.relative(fileRootDir, filePath);
56-
modulePath = path.relative(moduleRootDir, modulePath);
57-
}
26+
const resolutionResult = resolvePath(tags?.overridePath);
27+
28+
// Skip if can't be resolved
29+
if (!resolutionResult || !resolutionResult.outputPath) return node;
30+
31+
const { outputPath, filePath } = resolutionResult;
32+
33+
// Check if matches exclusion
34+
if (filePath && context.excludeMatchers)
35+
for (const matcher of context.excludeMatchers) if (matcher.match(filePath)) return node;
36+
37+
return updaterFn(factory.createStringLiteral(outputPath)) as ts.Node | undefined;
38+
39+
/* ********************************************************* *
40+
* Helpers
41+
* ********************************************************* */
42+
43+
function resolvePath(overridePath: string | undefined): { outputPath: string; filePath?: string } | undefined {
44+
/* Handle overridden path -- ie. @transform-path ../my/path) */
45+
if (overridePath) {
46+
return {
47+
outputPath: filePathToOutputPath(overridePath, path.extname(overridePath)),
48+
filePath: overridePath,
49+
};
5850
}
5951

60-
outputPath = tsInstance.normalizePath(
61-
path.join(path.relative(filePath, modulePath), path.basename(resolvedFileName))
52+
/* Have Compiler API attempt to resolve */
53+
const { resolvedModule, failedLookupLocations } = tsInstance.resolveModuleName(
54+
moduleName,
55+
sourceFile.fileName,
56+
compilerOptions,
57+
tsInstance.sys
6258
);
6359

64-
/* Check if matches exclusion */
65-
if (context.excludeMatchers)
66-
for (const matcher of context.excludeMatchers)
67-
if (matcher.match(outputPath)) return node;
60+
// No transform for node-modules
61+
if (resolvedModule?.isExternalLibraryImport) return void 0;
6862

69-
// Remove extension if implicit
70-
if (extension && implicitExtensions.includes(extension)) outputPath = outputPath.slice(0, -extension.length);
63+
/* Handle non-resolvable module */
64+
if (!resolvedModule) {
65+
const maybeURL = failedLookupLocations[0];
66+
if (!isURL(maybeURL)) return void 0;
67+
return { outputPath: maybeURL };
68+
}
7169

72-
if (!outputPath) return node;
70+
/* Handle resolved module */
71+
const { extension, resolvedFileName } = resolvedModule;
72+
return {
73+
outputPath: filePathToOutputPath(resolvedFileName, extension),
74+
filePath: resolvedFileName,
75+
};
76+
}
7377

74-
outputPath = outputPath[0] === "." ? outputPath : `./${outputPath}`;
78+
function filePathToOutputPath(filePath: string, extension: string | undefined) {
79+
if (path.isAbsolute(filePath)) {
80+
let sourceFileDir = tsInstance.normalizePath(path.dirname(sourceFile.fileName));
81+
let moduleDir = path.dirname(filePath);
82+
83+
/* Handle rootDirs mapping */
84+
if (config.useRootDirs && context.rootDirs) {
85+
let fileRootDir = "";
86+
let moduleRootDir = "";
87+
for (const rootDir of context.rootDirs) {
88+
if (isBaseDir(rootDir, filePath) && rootDir.length > moduleRootDir.length) moduleRootDir = rootDir;
89+
if (isBaseDir(rootDir, sourceFile.fileName) && rootDir.length > fileRootDir.length) fileRootDir = rootDir;
90+
}
91+
92+
/* Remove base dirs to make relative to root */
93+
if (fileRootDir && moduleRootDir) {
94+
sourceFileDir = path.relative(fileRootDir, sourceFileDir);
95+
moduleDir = path.relative(moduleRootDir, moduleDir);
96+
}
97+
}
98+
99+
/* Make path relative */
100+
filePath = tsInstance.normalizePath(path.join(path.relative(sourceFileDir, moduleDir), path.basename(filePath)));
101+
}
102+
103+
// Remove extension if implicit
104+
if (extension && implicitExtensions.includes(extension)) filePath = filePath.slice(0, -extension.length);
105+
106+
return filePath[0] === "." || isURL(filePath) ? filePath : `./${filePath}`;
75107
}
76108

77-
const newStringLiteral = factory.createStringLiteral(outputPath);
78-
return updaterFn(newStringLiteral) as ts.Node | undefined;
109+
function getStatementTags() {
110+
const targetNode = tsInstance.isStatement(node)
111+
? node
112+
: tsInstance.findAncestor(node, tsInstance.isStatement) ?? node;
113+
const jsDocTags = tsInstance.getJSDocTags(targetNode);
114+
115+
const trivia = targetNode.getFullText(sourceFile).slice(0, targetNode.getLeadingTriviaWidth(sourceFile));
116+
const commentTags = new Map<string, string | undefined>();
117+
const regex = /^\s*\/\/\/?\s*@(transform-path|no-transform-path)(?:[^\S\r\n](.+?))?$/gm;
118+
119+
for (let match = regex.exec(trivia); match; match = regex.exec(trivia)) commentTags.set(match[1], match[2]);
120+
121+
return {
122+
overridePath:
123+
commentTags.get("transform-path") ??
124+
jsDocTags?.find((t) => t.tagName.text.toLowerCase() === "transform-path")?.comment,
125+
shouldSkip:
126+
commentTags.has("no-transform-path") ||
127+
!!jsDocTags?.find((t) => t.tagName.text.toLowerCase() === "no-transform-path"),
128+
};
129+
}
79130
}

test/projects/specific/src/tags.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/* ****************************************************************************************************************** *
2+
* JSDoc
3+
* ****************************************************************************************************************** */
4+
5+
/**
6+
* @no-transform-path
7+
*/
8+
import * as skipTransform1 from "#root/index";
9+
10+
/**
11+
* @multi-tag1
12+
* @no-transform-path
13+
* @multi-tag2
14+
*/
15+
import * as skipTransform2 from "#root/index";
16+
17+
/**
18+
* @multi-tag1
19+
* @transform-path ./dir/src-file
20+
* @multi-tag2
21+
*/
22+
import * as explicitTransform1 from "./index";
23+
24+
/**
25+
* @multi-tag1
26+
* @transform-path http://www.go.com/react.js
27+
* @multi-tag2
28+
*/
29+
import * as explicitTransform2 from "./index";
30+
31+
/* ****************************************************************************************************************** *
32+
* JS Tag
33+
* ****************************************************************************************************************** */
34+
35+
// @no-transform-path
36+
import * as skipTransform3 from "#root/index";
37+
38+
// @multi-tag1
39+
// @no-transform-path
40+
// @multi-tag2
41+
import * as skipTransform4 from "#root/index";
42+
43+
// @multi-tag1
44+
// @transform-path ./dir/src-file
45+
// @multi-tag2
46+
import * as explicitTransform3 from "./index";
47+
48+
// @multi-tag1
49+
// @transform-path http://www.go.com/react.js
50+
// @multi-tag2
51+
import * as explicitTransform4 from "./index";
52+
53+
export {
54+
skipTransform1,
55+
skipTransform2,
56+
skipTransform3,
57+
skipTransform4,
58+
explicitTransform1,
59+
explicitTransform2,
60+
explicitTransform3,
61+
explicitTransform4
62+
}

test/tests/transformer/specific.test.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ describe(`Transformer -> Specific Cases`, () => {
2323
const genFile = ts.normalizePath(path.join(projectRoot, "generated/dir/gen-file.ts"));
2424
const srcFile = ts.normalizePath(path.join(projectRoot, "src/dir/src-file.ts"));
2525
const indexFile = ts.normalizePath(path.join(projectRoot, "src/index.ts"));
26+
const tagFile = ts.normalizePath(path.join(projectRoot, "src/tags.ts"));
2627
const typeElisionIndex = ts.normalizePath(path.join(projectRoot, "src/type-elision/index.ts"));
2728
const baseConfig: TsTransformPathsConfig = { exclude: ["**/excluded/**", "excluded-file.*"] };
2829

@@ -53,19 +54,20 @@ describe(`Transformer -> Specific Cases`, () => {
5354
rootDirsEmit = getEmitResult(rootDirsProgram);
5455
});
5556

56-
test(`(useRootDirs: true) Re-maps for rootDirs`, () => {
57-
expect(rootDirsEmit[genFile].dts).toMatch(`import "./src-file"`);
58-
expect(rootDirsEmit[srcFile].dts).toMatch(`import "./gen-file"`);
59-
expect(rootDirsEmit[indexFile].dts).toMatch(`export { B } from "./dir/gen-file"`);
60-
expect(rootDirsEmit[indexFile].dts).toMatch(`export { A } from "./dir/src-file"`);
61-
});
57+
describe(`Options`, () => {
58+
test(`(useRootDirs: true) Re-maps for rootDirs`, () => {
59+
expect(rootDirsEmit[genFile].dts).toMatch(`import "./src-file"`);
60+
expect(rootDirsEmit[srcFile].dts).toMatch(`import "./gen-file"`);
61+
expect(rootDirsEmit[indexFile].dts).toMatch(`export { B } from "./dir/gen-file"`);
62+
expect(rootDirsEmit[indexFile].dts).toMatch(`export { A } from "./dir/src-file"`);
63+
});
6264

63-
test(`(useRootDirs: false) Ignores rootDirs`, () => {
64-
expect(normalEmit[genFile].dts).toMatch(`import "../../src/dir/src-file"`);
65-
expect(normalEmit[srcFile].dts).toMatch(`import "../../generated/dir/gen-file"`);
66-
expect(normalEmit[indexFile].dts).toMatch(`export { B } from "../generated/dir/gen-file"`);
67-
expect(normalEmit[indexFile].dts).toMatch(`export { A } from "./dir/src-file"`);
68-
});
65+
test(`(useRootDirs: false) Ignores rootDirs`, () => {
66+
expect(normalEmit[genFile].dts).toMatch(`import "../../src/dir/src-file"`);
67+
expect(normalEmit[srcFile].dts).toMatch(`import "../../generated/dir/gen-file"`);
68+
expect(normalEmit[indexFile].dts).toMatch(`export { B } from "../generated/dir/gen-file"`);
69+
expect(normalEmit[indexFile].dts).toMatch(`export { A } from "./dir/src-file"`);
70+
});
6971

7072
test(`(exclude) Doesn't transform for exclusion patterns`, () => {
7173
expect(rootDirsEmit[indexFile].dts).toMatch(

0 commit comments

Comments
 (0)