Skip to content

2.2.0 #88

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .idea/typescript-transform-paths.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,57 @@ import '#root/file2.ts' // resolves to '../file2'
import '#root/file1.ts' // resolves to '../file1'
```

## Custom Control

### Exclusion patterns

You can disable transformation for paths based on the resolved file path. The `exclude` option allows specifying glob
patterns to match against resolved file path.

For an example context in which this would be useful, see [Issue #83](https://github.com/LeDDGroup/typescript-transform-paths/issues/83)

Example:
```jsonc
{
"compilerOptions": {
"paths": {
"sub-module1/*": [ "../../node_modules/sub-module1/*" ],
"sub-module2/*": [ "../../node_modules/sub-module2/*" ],
},
"plugins": [
{
"transform": "typescript-transform-paths",
"useRootDirs": true,
exclude: [ "**/node_modules/**" ]
}
]
}
}
```

```ts
// This path will not be transformed
import * as sm1 from 'sub-module1/index'
```

### @transform-path tag

Use the `@transform-path` tag to explicitly specify the output path for a single statement.

```ts
// @transform-path https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js
import react from 'react' // Output path will be the url above
```

### @no-transform-path

Use the `@no-transform-path` tag to explicitly disable transformation for a single statement.

```ts
// @no-transform-path
import 'normally-transformed' // This will remain 'normally-transformed', even though it has a different value in paths config
```

## Articles

- [Node Consumable Modules With Typescript Paths](https://medium.com/@ole.ersoy/node-consumable-modules-with-typescript-paths-ed88a5f332fa?postPublishedType=initial) by [oleersoy](https://github.com/oleersoy")
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
},
"devDependencies": {
"@types/jest": "^24.0.15",
"@types/minimatch": "^3.0.3",
"@types/node": "^12.0.2",
"jest": "^24.8.0",
"prettier": "^2.1.2",
Expand All @@ -70,5 +71,8 @@
},
"peerDependencies": {
"typescript": ">=3.6.5"
},
"dependencies": {
"minimatch": "^3.0.4"
}
}
2 changes: 2 additions & 0 deletions src/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { cast, getImplicitExtensions } from "./utils";
import { TsTransformPathsConfig, TsTransformPathsContext, TypeScriptThree, VisitorContext } from "./types";
import { nodeVisitor } from "./visitor";
import { createHarmonyFactory } from "./utils/harmony-factory";
import { Minimatch } from "minimatch";

/* ****************************************************************************************************************** *
* Transformer
Expand Down Expand Up @@ -34,6 +35,7 @@ export default function transformer(
transformationContext,
tsInstance,
tsThreeInstance: cast<TypeScriptThree>(tsInstance),
excludeMatchers: config.exclude?.map((globPattern) => new Minimatch(globPattern, { matchBase: true })),
};

return (sourceFile: ts.SourceFile) => {
Expand Down
4 changes: 3 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import tsThree from "./declarations/typescript3";
import ts, { CompilerOptions } from "typescript";
import { PluginConfig } from "ts-patch";
import { HarmonyFactory } from "./utils/harmony-factory";
import { IMinimatch } from "minimatch";

/* ****************************************************************************************************************** */
// region: TS Types
Expand All @@ -20,7 +21,7 @@ export type ImportOrExportClause = ts.ImportDeclaration["importClause"] | ts.Exp

export interface TsTransformPathsConfig extends PluginConfig {
readonly useRootDirs?: boolean;
readonly overwriteNodeModules?: boolean;
readonly exclude?: string[];
}

// endregion
Expand All @@ -46,6 +47,7 @@ export interface TsTransformPathsContext {
readonly elisionMap: Map<ts.SourceFile, Map<ImportOrExportDeclaration, ImportOrExportDeclaration>>;
readonly transformationContext: ts.TransformationContext;
readonly rootDirs?: string[];
readonly excludeMatchers: IMinimatch[] | undefined;
}

export interface VisitorContext extends TsTransformPathsContext {
Expand Down
144 changes: 101 additions & 43 deletions src/utils/resolve-path-update-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,56 +17,114 @@ export function resolvePathAndUpdateNode(
moduleName: string,
updaterFn: (newPath: ts.StringLiteral) => ts.Node | tsThree.Node | undefined
): ts.Node | undefined {
const { sourceFile, compilerOptions, tsInstance, config, rootDirs, implicitExtensions, factory } = context;

/* Have Compiler API attempt to resolve */
const { resolvedModule, failedLookupLocations } = tsInstance.resolveModuleName(
moduleName,
sourceFile.fileName,
compilerOptions,
tsInstance.sys
);

if (!config.overwriteNodeModules && resolvedModule?.isExternalLibraryImport) return node;

let outputPath: string;
if (!resolvedModule || config.overwriteNodeModules) {
const maybeURL = failedLookupLocations[0];
if (!isURL(maybeURL)) return node;
outputPath = maybeURL;
} else {
const { sourceFile, compilerOptions, tsInstance, config, implicitExtensions, factory } = context;
const tags = getStatementTags();

// Skip if @no-transform-path specified
if (tags?.shouldSkip) return node;

const resolutionResult = resolvePath(tags?.overridePath);

// Skip if can't be resolved
if (!resolutionResult || !resolutionResult.outputPath) return node;

const { outputPath, filePath } = resolutionResult;

// Check if matches exclusion
if (filePath && context.excludeMatchers)
for (const matcher of context.excludeMatchers) if (matcher.match(filePath)) return node;

return updaterFn(factory.createStringLiteral(outputPath)) as ts.Node | undefined;

/* ********************************************************* *
* Helpers
* ********************************************************* */

function resolvePath(overridePath: string | undefined): { outputPath: string; filePath?: string } | undefined {
/* Handle overridden path -- ie. @transform-path ../my/path) */
if (overridePath) {
return {
outputPath: filePathToOutputPath(overridePath, path.extname(overridePath)),
filePath: overridePath,
};
}

/* Have Compiler API attempt to resolve */
const { resolvedModule, failedLookupLocations } = tsInstance.resolveModuleName(
moduleName,
sourceFile.fileName,
compilerOptions,
tsInstance.sys
);

// No transform for node-modules
if (resolvedModule?.isExternalLibraryImport) return void 0;

/* Handle non-resolvable module */
if (!resolvedModule) {
const maybeURL = failedLookupLocations[0];
if (!isURL(maybeURL)) return void 0;
return { outputPath: maybeURL };
}

/* Handle resolved module */
const { extension, resolvedFileName } = resolvedModule;
return {
outputPath: filePathToOutputPath(resolvedFileName, extension),
filePath: resolvedFileName,
};
}

const fileName = sourceFile.fileName;
let filePath = tsInstance.normalizePath(path.dirname(sourceFile.fileName));
let modulePath = path.dirname(resolvedFileName);

/* Handle rootDirs mapping */
if (config.useRootDirs && rootDirs) {
let fileRootDir = "";
let moduleRootDir = "";
for (const rootDir of rootDirs) {
if (isBaseDir(rootDir, resolvedFileName) && rootDir.length > moduleRootDir.length) moduleRootDir = rootDir;
if (isBaseDir(rootDir, fileName) && rootDir.length > fileRootDir.length) fileRootDir = rootDir;
}
function filePathToOutputPath(filePath: string, extension: string | undefined) {
if (path.isAbsolute(filePath)) {
let sourceFileDir = tsInstance.normalizePath(path.dirname(sourceFile.fileName));
let moduleDir = path.dirname(filePath);

/* Handle rootDirs mapping */
if (config.useRootDirs && context.rootDirs) {
let fileRootDir = "";
let moduleRootDir = "";
for (const rootDir of context.rootDirs) {
if (isBaseDir(rootDir, filePath) && rootDir.length > moduleRootDir.length) moduleRootDir = rootDir;
if (isBaseDir(rootDir, sourceFile.fileName) && rootDir.length > fileRootDir.length) fileRootDir = rootDir;
}

/* Remove base dirs to make relative to root */
if (fileRootDir && moduleRootDir) {
filePath = path.relative(fileRootDir, filePath);
modulePath = path.relative(moduleRootDir, modulePath);
/* Remove base dirs to make relative to root */
if (fileRootDir && moduleRootDir) {
sourceFileDir = path.relative(fileRootDir, sourceFileDir);
moduleDir = path.relative(moduleRootDir, moduleDir);
}
}

/* Make path relative */
filePath = tsInstance.normalizePath(path.join(path.relative(sourceFileDir, moduleDir), path.basename(filePath)));
}

/* Remove extension if implicit */
outputPath = tsInstance.normalizePath(
path.join(path.relative(filePath, modulePath), path.basename(resolvedFileName))
);
if (extension && implicitExtensions.includes(extension)) outputPath = outputPath.slice(0, -extension.length);
if (!outputPath) return node;
// Remove extension if implicit
if (extension && implicitExtensions.includes(extension)) filePath = filePath.slice(0, -extension.length);

outputPath = outputPath[0] === "." ? outputPath : `./${outputPath}`;
return filePath[0] === "." || isURL(filePath) ? filePath : `./${filePath}`;
}

const newStringLiteral = factory.createStringLiteral(outputPath);
return updaterFn(newStringLiteral) as ts.Node | undefined;
function getStatementTags() {
const targetNode = tsInstance.isStatement(node)
? node
: tsInstance.findAncestor(node, tsInstance.isStatement) ?? node;
const jsDocTags = tsInstance.getJSDocTags(targetNode);

const trivia = targetNode.getFullText(sourceFile).slice(0, targetNode.getLeadingTriviaWidth(sourceFile));
const commentTags = new Map<string, string | undefined>();
const regex = /^\s*\/\/\/?\s*@(transform-path|no-transform-path)(?:[^\S\r\n](.+?))?$/gm;

for (let match = regex.exec(trivia); match; match = regex.exec(trivia)) commentTags.set(match[1], match[2]);

return {
overridePath:
commentTags.get("transform-path") ??
jsDocTags?.find((t) => t.tagName.text.toLowerCase() === "transform-path")?.comment,
shouldSkip:
commentTags.has("no-transform-path") ||
!!jsDocTags?.find((t) => t.tagName.text.toLowerCase() === "no-transform-path"),
};
}
}
4 changes: 2 additions & 2 deletions src/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export function nodeVisitor(this: VisitorContext, node: ts.Node): ts.Node | unde
return resolvePathAndUpdateNode(this, node, node.moduleSpecifier.text, (p) => {
let importClause = node.importClause;

if (!this.isDeclarationFile && importClause) {
if (!this.isDeclarationFile && importClause?.namedBindings) {
const updatedImportClause = elideImportOrExportClause(this, node);
if (!updatedImportClause) return undefined; // No imports left, elide entire declaration
importClause = updatedImportClause;
Expand All @@ -127,7 +127,7 @@ export function nodeVisitor(this: VisitorContext, node: ts.Node): ts.Node | unde
return resolvePathAndUpdateNode(this, node, node.moduleSpecifier.text, (p) => {
let exportClause = node.exportClause;

if (!this.isDeclarationFile && exportClause) {
if (!this.isDeclarationFile && exportClause && tsInstance.isNamedExports(exportClause)) {
const updatedExportClause = elideImportOrExportClause(this, node);
if (!updatedExportClause) return undefined; // No export left, elide entire declaration
exportClause = updatedExportClause;
Expand Down
2 changes: 1 addition & 1 deletion test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
"ts-patch": "link:../node_modules/ts-patch"
},
"workspaces": [
"packages/*"
"projects/*"
]
}
1 change: 1 addition & 0 deletions test/projects/specific/src/excluded-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type DD = number;
1 change: 1 addition & 0 deletions test/projects/specific/src/excluded/ex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type BB = number;
5 changes: 5 additions & 0 deletions test/projects/specific/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ import(
*/
"#root/dir/gen-file"
);

export { BB } from "#exclusion/ex";
export { DD } from "#root/excluded-file";

export const b = 3;
62 changes: 62 additions & 0 deletions test/projects/specific/src/tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/* ****************************************************************************************************************** *
* JSDoc
* ****************************************************************************************************************** */

/**
* @no-transform-path
*/
import * as skipTransform1 from "#root/index";

/**
* @multi-tag1
* @no-transform-path
* @multi-tag2
*/
import * as skipTransform2 from "#root/index";

/**
* @multi-tag1
* @transform-path ./dir/src-file
* @multi-tag2
*/
import * as explicitTransform1 from "./index";

/**
* @multi-tag1
* @transform-path http://www.go.com/react.js
* @multi-tag2
*/
import * as explicitTransform2 from "./index";

/* ****************************************************************************************************************** *
* JS Tag
* ****************************************************************************************************************** */

// @no-transform-path
import * as skipTransform3 from "#root/index";

// @multi-tag1
// @no-transform-path
// @multi-tag2
import * as skipTransform4 from "#root/index";

// @multi-tag1
// @transform-path ./dir/src-file
// @multi-tag2
import * as explicitTransform3 from "./index";

// @multi-tag1
// @transform-path http://www.go.com/react.js
// @multi-tag2
import * as explicitTransform4 from "./index";

export {
skipTransform1,
skipTransform2,
skipTransform3,
skipTransform4,
explicitTransform1,
explicitTransform2,
explicitTransform3,
explicitTransform4,
};
4 changes: 3 additions & 1 deletion test/projects/specific/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
"rootDir": ".",
"module": "ESNext",
"esModuleInterop": true,
"moduleResolution": "node",
"declaration": true,

"rootDirs": [ "src", "generated" ],

"baseUrl": ".",
"paths": {
"#root/*": [ "./src/*", "./generated/*" ]
"#root/*": [ "./src/*", "./generated/*" ],
"#exclusion/*": [ "./src/excluded/*" ]
}
}
}
Loading