diff --git a/README.md b/README.md
index bf86504..9233ffc 100644
--- a/README.md
+++ b/README.md
@@ -4,17 +4,21 @@
`Type-Strip` is a **super-fast** type-stripper: TypeScript code goes in, and JavaScript code with type annotations removed comes out.
-It also ensures **forward compatibility** with the TC39 [Type Annotation Proposal](https://tc39.es/proposal-type-annotations/). This means that when the proposal reaches stage 4, you'll be able to seamlessly change your file extensions to `.js`, and won't need a transpilation step anymore.
+It also ensures **forward compatibility** of your code with the TC39 [Type Annotation Proposal](https://tc39.es/proposal-type-annotations/).
If you're using modern TypeScript today, then `Type-Strip` might be the only build step you need.
## Features
- Strips type annotations
-- Optionally strips comments and rewrites .ts imports. See the [options](#options) below
+- (Optionally) strips comments
+- (Optionally) rewrites .ts imports to .js
+- (Optionally) remap aliases in import specifiers
- Fast. See the [benchmark](#benchmark)
- Throws when an [unsupported syntax](#unsupported-features) is detected.
+See the [options](https://jsr.io/@fcrozatier/type-strip/doc/~/TypeStripOptions) for more
+
## Installation
Depending on your runtime / package-manager:
@@ -31,18 +35,18 @@ yarn dlx jsr add @fcrozatier/type-strip
Strip a string of code, files etc.
```ts
-import strip from '@fcrozatier/type-strip';
+import stripTypes from '@fcrozatier/type-strip';
-console.log(strip("let num: number = 0;", {/* options */}));
-//-> let num = 0;
+stripTypes("let num: number = 0;", {/* options */});
+// let num = 0;
```
### Example
-Input
+Input file `./example.ts`
```ts
-import { capitalize } from './utils.ts';
+import { capitalize } from '$utils/strings.ts';
/**
* This class implements a Person
@@ -61,10 +65,25 @@ class Person {
}
```
-Output with the `removeComments` and `pathRewriting` options:
+Options:
+
+```ts
+{
+ removeComments: true,
+ pathRewriting: true,
+ remapSpecifiers: {
+ filePath: "./example.ts",
+ imports: {
+ "$utils/": "./lib/utils/",
+ },
+ },
+}
+```
+
+Output:
```ts
-import { capitalize } from './utils.js';
+import { capitalize } from './lib/utils/strings.js';
class Person {
name;
@@ -77,17 +96,7 @@ class Person {
}
```
-### Options
-
-
- removeComments?: boolean
- - Whether to strip comments
- - Default
false
-
- pathRewriting?: boolean
- - Whether to rewrite
.ts module imports to .js imports
- - Default
false
-
+### [Options](https://jsr.io/@fcrozatier/type-strip/doc/~/TypeStripOptions)
## Unsupported features
diff --git a/index.ts b/index.ts
index eefe68d..8555622 100644
--- a/index.ts
+++ b/index.ts
@@ -1,5 +1,8 @@
import ts from "typescript";
import { TypeStripError } from "./errors.ts";
+import { relative } from "@std/path/relative";
+import { dirname } from "@std/path/dirname";
+import { isAbsolute } from "@std/path/is-absolute";
const SyntaxKind = ts.SyntaxKind;
@@ -9,32 +12,98 @@ const SyntaxKind = ts.SyntaxKind;
export type TypeStripOptions = {
/**
* Whether to strip comments
+ *
+ * @default false
*/
removeComments?: boolean;
- /**
- * @deprecated Use pathRewriting instead
- */
- tsToJsModuleSpecifiers?: boolean;
/**
* Whether to rewrite .ts module imports to .js imports
+ *
+ * @default false
*/
pathRewriting?: boolean;
+ /**
+ * Whether and how to expand aliases in import specifiers
+ */
+ remapSpecifiers?: {
+ /**
+ * The path of the file being stripped.
+ */
+ filePath: string;
+ /**
+ * A map of specifiers to their remapped specifiers
+ *
+ * Relative paths for substitutions are specified with respect to the `deno.json` file
+ *
+ * @example
+ *
+ * Given the file structure:
+ *
+ * ```
+ * ├ deno.json
+ * ├ a.ts
+ * └ lib/
+ * └ foo/
+ * ├ b.ts
+ * └ bar.ts
+ * ```
+ *
+ * Where `a.ts` and `b.ts` both contain:
+ *
+ * ```ts
+ * "import '$foo/bar.ts'"
+ * ```
+ *
+ * With the option `{ imports: { '$foo/': "./lib/foo/" }` the aliases are substituted as expected:
+ *
+ * ```ts
+ * import stripTypes from '@fcrozatier/type-strip';
+ *
+ * stripTypes(fileA, {
+ * pathRewriting: true,
+ * remapSpecifiers: {
+ * filePath: './a.ts',
+ * imports: { '$foo/': "./lib/foo/" }
+ * }
+ * });
+ * // input: import '$foo/bar.ts'
+ * // output: import './lib/foo/bar.js'
+ *
+ * stripTypes(fileB, {
+ * pathRewriting: true,
+ * remapSpecifiers: {
+ * filePath: './lib/b.ts',
+ * imports: { '$foo/': "./lib/foo/" } });
+ * }
+ * // input: import '$foo/bar.ts'
+ * // output: import './bar.js'
+ * ```
+ */
+ imports: Record;
+ };
+ /**
+ * @deprecated Use pathRewriting instead
+ */
+ tsToJsModuleSpecifiers?: boolean;
};
-const defaults: Required = {
- removeComments: false,
- tsToJsModuleSpecifiers: false,
- pathRewriting: false,
-};
-
-type StripItem = { start: number; end: number; trailing?: RegExp };
-
let sourceFile: ts.SourceFile;
let sourceCode: string;
let outputCode: string;
let removeComments: boolean;
let pathRewriting: boolean;
-let strip: StripItem[] = [];
+let filePath: string;
+let imports: [string, string][] | undefined;
+
+type StripItem = { start: number; end: number; trailing?: RegExp };
+const strip: StripItem[] = [];
+
+type TransformSpecifier = {
+ pos: number;
+ alias: RegExp;
+ replacement: string;
+};
+const transformSpecifiers: TransformSpecifier[] = [];
/**
* Takes a TypeScript input source file and outputs JavaScript with types stripped
@@ -53,26 +122,51 @@ export default (
ts.ScriptKind.TS,
);
- removeComments = options?.removeComments ?? defaults.removeComments;
+ removeComments = options?.removeComments ?? false;
pathRewriting = (options?.pathRewriting || options?.tsToJsModuleSpecifiers) ??
- defaults.pathRewriting;
+ false;
+ const remapSpecifiers = options?.remapSpecifiers;
+
+ if (remapSpecifiers) {
+ imports = Object.entries(remapSpecifiers.imports);
+ filePath = remapSpecifiers.filePath;
+ }
sourceCode = sourceFile.getFullText();
outputCode = "";
- strip = [];
+ strip.length = 0;
+ transformSpecifiers.length = 0;
const statements = sourceFile.statements;
for (let i = 0; i < statements.length; i++) {
topLevelVisitor(statements[i]);
}
+ transformSpecifiers.reverse();
+ let currentSpecifierTransform = transformSpecifiers.pop();
+
let currentIndex = 0;
for (let i = 0; i < strip.length; i++) {
const { start, end, trailing } = strip[i];
if (start !== undefined && end !== undefined) {
if (currentIndex < start) {
- outputCode += sourceCode.slice(currentIndex, start);
+ let chunk = sourceCode.slice(currentIndex, start);
+
+ while (
+ currentSpecifierTransform &&
+ currentSpecifierTransform.pos >= currentIndex &&
+ currentSpecifierTransform.pos < start
+ ) {
+ chunk = chunk.replace(
+ currentSpecifierTransform.alias,
+ "$1" +
+ currentSpecifierTransform.replacement,
+ );
+ currentSpecifierTransform = transformSpecifiers.pop();
+ }
+
+ outputCode += chunk;
currentIndex = end;
}
if (currentIndex < end) {
@@ -87,7 +181,20 @@ export default (
}
}
if (currentIndex < sourceCode.length) {
- outputCode += sourceCode.slice(currentIndex);
+ let chunk = sourceCode.slice(currentIndex);
+
+ while (
+ currentSpecifierTransform &&
+ currentSpecifierTransform.pos >= currentIndex
+ ) {
+ chunk = chunk.replace(
+ currentSpecifierTransform.alias,
+ "$1" + currentSpecifierTransform.replacement,
+ );
+ currentSpecifierTransform = transformSpecifiers.pop();
+ }
+
+ outputCode += chunk;
}
return outputCode;
@@ -251,17 +358,41 @@ const visitImportDeclaration = (node: ts.ImportDeclaration) => {
strip.push({ start: node.pos, end: node.end });
} else if (node.importClause) {
const skipDeclaration = visitImportClause(node.importClause);
+
if (skipDeclaration) {
strip.push({ start: node.pos, end: node.end });
- }
- }
- if (pathRewriting) {
- if (ts.isStringLiteral(node.moduleSpecifier)) {
- const moduleSpecifier = node.moduleSpecifier;
+ } else {
+ if (ts.isStringLiteral(node.moduleSpecifier)) {
+ const moduleSpecifier = node.moduleSpecifier;
+
+ if (imports) {
+ const match = imports.find(([alias]) =>
+ moduleSpecifier.text.startsWith(alias)
+ );
+
+ if (match) {
+ let replacement = match[1];
+
+ // relative path: non absolute path that's not an @alias
+ if (!isAbsolute(replacement) && replacement.startsWith(".")) {
+ replacement = relative(dirname(filePath), replacement) +
+ (replacement.endsWith("/") ? "/" : "");
+ }
+
+ transformSpecifiers.push({
+ pos: moduleSpecifier.pos,
+ alias: new RegExp("(['\"])" + RegExp.escape(match[0])),
+ replacement,
+ });
+ }
+ }
- sourceCode = sourceCode.slice(0, moduleSpecifier.pos) +
- ` "${moduleSpecifier.text.replace(/\.ts$/, ".js")}"` +
- sourceCode.slice(moduleSpecifier.end);
+ if (pathRewriting) {
+ sourceCode = sourceCode.slice(0, moduleSpecifier.pos) +
+ ` "${moduleSpecifier.text.replace(/\.ts$/, ".js")}"` +
+ sourceCode.slice(moduleSpecifier.end);
+ }
+ }
}
}
};
diff --git a/tests/supported/cases/option-imports/input.ts b/tests/supported/cases/option-imports/input.ts
new file mode 100644
index 0000000..3292037
--- /dev/null
+++ b/tests/supported/cases/option-imports/input.ts
@@ -0,0 +1,5 @@
+import a from "$foo/bar.ts";
+import type A from "$foo/bar.d.ts";
+import { b } from "$fiz/bar.ts";
+import { type B } from "$foo/bar.d.ts";
+import { c, type C } from "$baz/bar";
\ No newline at end of file
diff --git a/tests/supported/cases/option-imports/options.ts b/tests/supported/cases/option-imports/options.ts
new file mode 100644
index 0000000..42bd24e
--- /dev/null
+++ b/tests/supported/cases/option-imports/options.ts
@@ -0,0 +1,13 @@
+import type { TypeStripOptions } from "@fcrozatier/type-strip";
+
+export const options: TypeStripOptions = {
+ pathRewriting: true,
+ remapSpecifiers: {
+ filePath: "./lib/a.ts",
+ imports: {
+ "$foo/": "./foo/",
+ "$fiz": "./fiz",
+ "$baz": "@baz",
+ },
+ },
+};
diff --git a/tests/supported/cases/option-imports/output.js b/tests/supported/cases/option-imports/output.js
new file mode 100644
index 0000000..aa8bbdc
--- /dev/null
+++ b/tests/supported/cases/option-imports/output.js
@@ -0,0 +1,3 @@
+import a from "../foo/bar.js";
+import { b } from "../fiz/bar.js";
+import { c, } from "@baz/bar";
\ No newline at end of file