Skip to content

Commit

Permalink
feat: findTypeImports for finding type imports (#163)
Browse files Browse the repository at this point in the history
  • Loading branch information
peterroe committed Jun 19, 2023
1 parent f0b120b commit acae578
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 12 deletions.
21 changes: 21 additions & 0 deletions src/_utils.ts
Expand Up @@ -43,3 +43,24 @@ export function matchAll(regex, string, addition) {
}
return matches;
}

export function clearImports(imports: string) {
return (imports || "")
.replace(/(\/\/[^\n]*\n|\/\*.*\*\/)/g, "")
.replace(/\s+/g, " ");
}

export function getImportNames(cleanedImports: string) {
const topLevelImports = cleanedImports.replace(/{([^}]*)}/, "");
const namespacedImport = topLevelImports.match(/\* as \s*(\S*)/)?.[1];
const defaultImport =
topLevelImports
.split(",")
.find((index) => !/[*{}]/.test(index))
?.trim() || undefined;

return {
namespacedImport,
defaultImport,
};
}
69 changes: 57 additions & 12 deletions src/analyze.ts
@@ -1,5 +1,5 @@
import { tokenizer } from "acorn";
import { matchAll } from "./_utils";
import { matchAll, clearImports, getImportNames } from "./_utils";
import { resolvePath, ResolveOptions } from "./resolve";
import { loadURL } from "./utils";

Expand Down Expand Up @@ -27,6 +27,12 @@ export interface DynamicImport extends ESMImport {
expression: string;
}

export interface TypeImport extends Omit<ESMImport, "type"> {
type: "type";
imports: string;
specifier: string;
}

export interface ESMExport {
_type?: "declaration" | "named" | "default" | "star";
type: "declaration" | "named" | "default" | "star";
Expand Down Expand Up @@ -59,6 +65,8 @@ export const ESM_STATIC_IMPORT_RE =
/(?<=\s|^|;)import\s*([\s"']*(?<imports>[\p{L}\p{M}\w\t\n\r $*,/{}]+)from\s*)?["']\s*(?<specifier>(?<="\s*)[^"]*[^\s"](?=\s*")|(?<='\s*)[^']*[^\s'](?=\s*'))\s*["'][\s;]*/gmu;
export const DYNAMIC_IMPORT_RE =
/import\s*\((?<expression>(?:[^()]+|\((?:[^()]+|\([^()]*\))*\))*)\)/gm;
const IMPORT_NAMED_TYPE_RE =
/(?<=\s|^|;)import\s*type\s+([\s"']*(?<imports>[\w\t\n\r $*,/{}]+)from\s*)?["']\s*(?<specifier>(?<="\s*)[^"]*[^\s"](?=\s*")|(?<='\s*)[^']*[^\s'](?=\s*'))\s*["'][\s;]*/gm;

export const EXPORT_DECAL_RE =
/\bexport\s+(?<declaration>(async function|function|let|const enum|const|enum|var|class))\s+(?<name>[\w$]+)/g;
Expand All @@ -83,10 +91,19 @@ export function findDynamicImports(code: string): DynamicImport[] {
return matchAll(DYNAMIC_IMPORT_RE, code, { type: "dynamic" });
}

export function parseStaticImport(matched: StaticImport): ParsedStaticImport {
const cleanedImports = (matched.imports || "")
.replace(/(\/\/[^\n]*\n|\/\*.*\*\/)/g, "")
.replace(/\s+/g, " ");
export function findTypeImports(code: string): TypeImport[] {
return [
...matchAll(IMPORT_NAMED_TYPE_RE, code, { type: "type" }),
...matchAll(ESM_STATIC_IMPORT_RE, code, { type: "static" }).filter(
(match) => /[^A-Za-z]type\s/.test(match.imports)
),
];
}

export function parseStaticImport(
matched: StaticImport | TypeImport
): ParsedStaticImport {
const cleanedImports = clearImports(matched.imports);

const namedImports = {};
for (const namedImport of cleanedImports
Expand All @@ -98,13 +115,41 @@ export function parseStaticImport(matched: StaticImport): ParsedStaticImport {
namedImports[source] = importName;
}
}
const topLevelImports = cleanedImports.replace(/{([^}]*)}/, "");
const namespacedImport = topLevelImports.match(/\* as \s*(\S*)/)?.[1];
const defaultImport =
topLevelImports
.split(",")
.find((index) => !/[*{}]/.test(index))
?.trim() || undefined;
const { namespacedImport, defaultImport } = getImportNames(cleanedImports);

return {
...matched,
defaultImport,
namespacedImport,
namedImports,
} as ParsedStaticImport;
}

export function parseTypeImport(
matched: TypeImport | StaticImport
): ParsedStaticImport {
if (matched.type === "type") {
return parseStaticImport(matched);
}

const cleanedImports = clearImports(matched.imports);

const namedImports = {};
for (const namedImport of cleanedImports
.match(/{([^}]*)}/)?.[1]
?.split(",") || []) {
const [, source = namedImport.trim(), importName = source] = (() => {
return /\s+as\s+/.test(namedImport)
? namedImport.match(/^\s*type\s+(\S*) as (\S*)\s*$/) || []
: namedImport.match(/^\s*type\s+(\S*)\s*$/) || [];
})();

if (source && TYPE_RE.test(namedImport)) {
namedImports[source] = importName;
}
}

const { namespacedImport, defaultImport } = getImportNames(cleanedImports);

return {
...matched,
Expand Down
1 change: 1 addition & 0 deletions src/utils.ts
@@ -1,6 +1,7 @@
import { fileURLToPath as _fileURLToPath } from "node:url";
import { promises as fsp } from "node:fs";
import { normalizeSlash, BUILTIN_MODULES } from "./_utils";
import { StaticImport, TypeImport } from "./analyze";

Check warning on line 4 in src/utils.ts

View workflow job for this annotation

GitHub Actions / ci

'StaticImport' is defined but never used

Check warning on line 4 in src/utils.ts

View workflow job for this annotation

GitHub Actions / ci

'TypeImport' is defined but never used

export function fileURLToPath(id: string): string {
if (typeof id === "string" && !id.startsWith("file://")) {
Expand Down
81 changes: 81 additions & 0 deletions test/imports.test.ts
Expand Up @@ -3,6 +3,8 @@ import {
findDynamicImports,
findStaticImports,
parseStaticImport,
findTypeImports,
parseTypeImport,
} from "../src";

// -- Static import --
Expand Down Expand Up @@ -153,6 +155,57 @@ const dynamicTests = {
},
};

const TypeTests = {
'import { type Foo, Bar } from "module-name";': {
specifier: "module-name",
namedImports: {
Foo: "Foo",
},
type: "static",
},
'import { member,/* hello */ type Foo as Baz, Bar } from "module-name";': {
specifier: "module-name",
namedImports: {
Foo: "Baz",
},
type: "static",
},
'import type { Foo, Bar } from "module-name";': {
specifier: "module-name",
namedImports: {
Foo: "Foo",
Bar: "Bar",
},
type: "type",
},
'import type Foo from "module-name";': {
specifier: "module-name",
defaultImport: "Foo",
type: "type",
},
'import type { Foo as Baz, Bar } from "module-name";': {
specifier: "module-name",
namedImports: {
Foo: "Baz",
Bar: "Bar",
},
type: "type",
},
'import { type member } from " module-name";': {
specifier: "module-name",
namedImports: { member: "member" },
type: "static",
},
'import { type member, type Foo as Bar } from " module-name";': {
specifier: "module-name",
namedImports: {
member: "member",
Foo: "Bar",
},
type: "static",
},
};

describe("findStaticImports", () => {
for (const [input, _results] of Object.entries(staticTests)) {
it(input.replace(/\n/g, "\\n"), () => {
Expand Down Expand Up @@ -191,3 +244,31 @@ describe("findDynamicImports", () => {
});
}
});

describe("findTypeImports", () => {
for (const [input, _results] of Object.entries(TypeTests)) {
it(input.replace(/\n/g, "\\n"), () => {
const matches = findTypeImports(input);
const results = Array.isArray(_results) ? _results : [_results];
expect(matches.length).toEqual(results.length);
for (const [index, test] of results.entries()) {
const match = matches[index];
expect(match.specifier).to.equal(test.specifier);

const parsed = parseTypeImport(match);
if (test.type) {
expect(parsed.type).to.equals(test.type);
}
if (test.defaultImport) {
expect(parsed.defaultImport).to.equals(test.defaultImport);
}
if (test.namedImports) {
expect(parsed.namedImports).to.eql(test.namedImports);
}
if (test.namespacedImport) {
expect(parsed.namespacedImport).to.eql(test.namespacedImport);
}
}
});
}
});

0 comments on commit acae578

Please sign in to comment.