diff --git a/package.json b/package.json index 847d4e5..fa6e813 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,10 @@ "dependencies": { "@babel/parser": "^7.27.2", "cli-highlight": "^2.1.11", + "enhanced-resolve": "^5.16.0", + "get-tsconfig": "^4.7.5", "glob": "^11.0.2", + "is-builtin-module": "^4.0.0", "recast": "^0.23.11", "yargs": "^17.7.2" }, diff --git a/src/astUtils/index.js b/src/astUtils/index.js index 67aa866..ab5274c 100644 --- a/src/astUtils/index.js +++ b/src/astUtils/index.js @@ -351,6 +351,22 @@ const removeTypeFromTSUnionType = (node, typeToRemove) => { node.types = node.types.filter(type => type?.literal?.value !== typeToRemove); }; +/** + * Give an ExportNamedDeclaration and a specifier, check if the specifier is in the export statement + * + * @typedef {Object} hasSpecifierParams + * @property {import("ast-types/gen/kinds").ExportNamedDeclarationKind} exportNode - the export node + * @property {import("ast-types/gen/kinds").ImportSpecifierKind} specifier - the specifier to find + * + * @param {hasSpecifierParams} param0 + * @returns {boolean} - true if the specifier is in the export statement + */ +const exportNamedDeclarationHasSpecifier = ({ exportNode, specifier }) => { + return exportNode.specifiers?.some( + exportSpecifier => exportSpecifier.exported.name === specifier?.imported?.name + ); +}; + export { collapseConditionalExpressionIfMatchingIdentifier, isCallExpressionWithName, @@ -378,4 +394,5 @@ export { removeElementsStartingAtIndexAndReplaceWithNewBody, removeImportSpecifier, removeTypeFromTSUnionType, + exportNamedDeclarationHasSpecifier, }; diff --git a/src/index.js b/src/index.js index 098e502..8e29c2b 100755 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,10 @@ #!/usr/bin/env node -import { readFileSync, writeFile } from "fs"; import { sync } from "glob"; import { print } from "recast"; +import { readFileSync, writeFileSync } from "node:fs"; + import { parseCode } from "./parser.js"; import { AVAILABLE_TRANSFORMS } from "./availableTransforms.js"; import { validTransformName } from "./validTransformName.js"; @@ -37,6 +38,7 @@ filePaths.forEach(filePath => { const node = transformer({ ast, transformToRun: transform, + filePath, options, }); @@ -47,10 +49,6 @@ filePaths.forEach(filePath => { if (dryRun) { dryRunOutput(transformedCode, filePath); } else { - writeFile(filePath, transformedCode, writeError => { - if (writeError) { - throw writeError(); - } - }); + writeFileSync(filePath, transformedCode); } }); diff --git a/src/transformer.js b/src/transformer.js index f97d8b8..a4fe7dc 100644 --- a/src/transformer.js +++ b/src/transformer.js @@ -2,8 +2,8 @@ import { visit, types } from "recast"; const builder = types.builders; -const transformer = ({ ast, transformToRun, options }) => { - const visitMethods = transformToRun({ ast, builder, options }); +const transformer = ({ ast, transformToRun, filePath, options }) => { + const visitMethods = transformToRun({ ast, builder, filePath, options }); const visitMethodsWithTraverse = Object.keys(visitMethods).reduce((acc, methodName) => { // using function here for this binding diff --git a/src/transforms/deBarrelify/barrelFileUtils.js b/src/transforms/deBarrelify/barrelFileUtils.js new file mode 100644 index 0000000..f910b61 --- /dev/null +++ b/src/transforms/deBarrelify/barrelFileUtils.js @@ -0,0 +1,29 @@ +/** + * Given path, determines if it is a barrel file (index file) + * + * @param {string} path + * @returns {boolean} + */ +const isBarrelFile = path => { + // TODO: add support for specifying a list of barrel file names + return ( + path.endsWith("/index.ts") || + path.endsWith("/index.tsx") || + path.endsWith("/helpers.ts") || + path.endsWith("/helpers.tsx") + ); +}; + +/** + * Given a path, remove the barrel file name from it + * ex: src/components/index.ts -> src/components/ + * + * @param {string} path + * @returns {string} - the path with the barrel file name removed + */ +const removeBarrelFileFromPath = path => { + // TODO: also need to modify this to account for different barrel file names + return path.replace(/(index|helpers)\.tsx?/, ""); +}; + +export { isBarrelFile, removeBarrelFileFromPath }; diff --git a/src/transforms/deBarrelify/deBarrelify.js b/src/transforms/deBarrelify/deBarrelify.js new file mode 100644 index 0000000..442ce5e --- /dev/null +++ b/src/transforms/deBarrelify/deBarrelify.js @@ -0,0 +1,247 @@ +import { readFileSync } from "node:fs"; +import { parseCode } from "../../parser.js"; +import { visit } from "recast"; +import isBuiltinModule from "is-builtin-module"; + +import { resolveAbsolutePath } from "./resolver.js"; +import { isBarrelFile, removeBarrelFileFromPath } from "./barrelFileUtils.js"; + +import { replaceImportDeclarationWithDeepImport } from "./replaceImportDeclarationWithDeepImport/replaceImportDeclarationWithDeepImport.js"; +import { exportNamedDeclarationHasSpecifier } from "../../astUtils/index.js"; + +/** + * Type definitions for VSCode autocompletion! + * + * @typedef {Object} TransformParams + * @property {*} ast - The resulting AST as parsed by babel + * @property {import("ast-types/gen/builders").builders} builder - Recast builder for transforming the AST + * @property {*} options - Options passed into the transform from the CLI (if any) + */ + +/** + * @typedef {Object} findSpecifierSourceParams + * @property {string} filePath - the file path + * @property {import("ast-types/gen/kinds").ImportSpecifierKind} specifier - the specifier to find + * + * @typedef {Object} findSpecifierSourceResult + * @property {string|undefined} specifierSource - the found specifier + * @property {boolean} importAs - true if the specifier was imported as wildcard + * @property {string[]} potentialSpecifierSources - the potential specifier sources + * + * @param {findSpecifierSourceParams} + * @returns {findSpecifierSourceResult} + */ +const findSpecifierSource = ({ filePath, specifier }) => { + let specifierSource; + let importAs = false; + let potentialSpecifierSources = []; + + const code = readFileSync(filePath, { encoding: "utf-8", flag: "r" }); + const barrelFileAst = parseCode(code); + + visit(barrelFileAst, { + visitImportDeclaration: function (importPath) { + if (importPath.node.specifiers.some(spec => spec.local.name === specifier?.imported?.name)) { + // DONE: identify the import name + specifierSource = importPath.node.source.value; + importAs = true; + return false; // stop parsing, found what we needed + } + + this.traverse(importPath); + }, + visitExportNamedDeclaration: function (exportPath) { + if (exportNamedDeclarationHasSpecifier({ exportNode: exportPath.node, specifier })) { + if (exportPath.node.source) { + // DONE: identify the export name + specifierSource = exportPath.node?.source?.value; + } else if (!specifierSource) { + // prevent conflict with visitImportDeclaration if the specifierSource is already set + // This can conflict in cases where an import statement is used to load a module which is then exported as a separate statement + // ex: app/javascript/styles/index.ts + // If the export statement doesn't have a source, it's likely defined in the current filePath, set that as the returned source + specifierSource = filePath; + } + return false; // stop parsing, found what we needed + } + + this.traverse(exportPath); + }, + visitExportAllDeclaration: function (exportPath) { + potentialSpecifierSources.push(exportPath.node.source.value); + this.traverse(exportPath); + }, + + // Look at adding support for visitExportNamedDeclaration with wildcard + }); + + return { specifierSource, importAs, potentialSpecifierSources }; +}; + +/** + * @typedef {Object} transformImportParams + * @property {import("ast-types/gen/builders").builders} builder - Recast builder for transforming the AST + * @property {import("ast-types/lib/node-path").NodePath} path - the path to replace + * @property {string} importSource - import source + * @property {import("ast-types/gen/kinds").ImportSpecifierKind} specifier - the specifier to import + * + * @param {transformImportParams} param0 + * @returns {boolean} - true if the import was transformed + */ +const transformImport = ({ builder, path, importSource, specifier }) => { + // DONE: visit the barrel file and parse it's contents into an AST + const { specifierSource, importAs, potentialSpecifierSources } = findSpecifierSource({ + filePath: importSource, + specifier, + }); + + // If there's no found specifier, we don't do anything more + // If specifier if the same as the importSource, we don't do anything more + if (specifierSource && specifierSource !== importSource) { + const deeperResolvedPath = resolveAbsolutePath({ + context: {}, + resolveContext: {}, + folderPath: removeBarrelFileFromPath(`./${importSource}`), + importSource: specifierSource, + }); + + // DONE: if it's a not a barrel file, create a new importDeclaration for this specifier with the path to this file + if (!isBarrelFile(deeperResolvedPath)) { + replaceImportDeclarationWithDeepImport({ + builder, + path, + newImportSource: deeperResolvedPath, + specifier, + importAs, + }); + + return true; + } else { + // DONE: if it's a barrel file, go down again + return transformImport({ + builder, + path, + importSource: deeperResolvedPath, + specifier, + }); + } + } else if (potentialSpecifierSources.length > 0) { + // loop through each potentialSpecifierSource, dive into if further and check if the specifier is found + // if it is, replace the import with the deeperResolvedPath + // if it's not, continue to the next potentialSpecifierSource + // if nothing is found, do nothing + + for (let i = 0; i < potentialSpecifierSources.length; i++) { + const deeperResolvedPath = resolveAbsolutePath({ + context: {}, + resolveContext: {}, + folderPath: removeBarrelFileFromPath(`./${importSource}`), + importSource: potentialSpecifierSources[i], + }); + + if (isBarrelFile(deeperResolvedPath)) { + const wasImportTransformed = transformImport({ + builder, + path, + importSource: deeperResolvedPath, + specifier, + }); + + if (wasImportTransformed) { + return true; + } + } else { + replaceImportDeclarationWithDeepImport({ + builder, + path, + newImportSource: deeperResolvedPath, + specifier, + importAs, + }); + + return true; + } + } + } + + return false; +}; + +/** + * @typedef {Object} isSpecifiedNamesToIgnoreParams + * @property {import("ast-types/lib/node-path").NodePath} path - the path to replace + * @property {string} specifiedNamespace - import namespace to operate on + * + * @param {isSpecifiedNamesToIgnoreParams} param0 + * @returns {boolean} - true if the import should be transformed + */ +const isSpecifiedNamespaceToTransform = ({ path, specifiedNamespace }) => { + return specifiedNamespace && path.node.source.value.startsWith(specifiedNamespace); +}; + +/** + * @typedef {Object} isSpecifiedNamesToIgnoreParams + * @property {import("ast-types/lib/node-path").NodePath} path - the path to replace + * @property {string} specifiedNamespace - import namespace to operate on + * + * @param {isSpecifiedNamesToIgnoreParams} param0 + * @returns {boolean} - true if the import should be ignored + */ +const isNotSpecifiedNamespaceToTransform = ({ path, specifiedNamespace }) => { + return specifiedNamespace && !path.node.source.value.startsWith(specifiedNamespace); +}; + +/** + * @param {TransformParams} param0 + * @returns {import("ast-types").Visitor} + */ +const transform = ({ builder, filePath, options }) => { + const folderPath = filePath.split("/").slice(0, -1).join("/"); + const specifiedNamespace = options?.[0]; + const ignoreSpecifiedNamespace = !!options?.[1]; + + return { + visitImportDeclaration: path => { + let importTransformed = false; + + // Verify that the import is not a node builtin module + if ( + ((isSpecifiedNamespaceToTransform({ path, specifiedNamespace }) && + !ignoreSpecifiedNamespace) || + !specifiedNamespace || + (isNotSpecifiedNamespaceToTransform({ path, specifiedNamespace }) && + ignoreSpecifiedNamespace)) && + !isBuiltinModule(path.node.source.value) + ) { + const resolvedPath = resolveAbsolutePath({ + context: {}, + resolveContext: {}, + folderPath, + importSource: path.node.source.value, + }); + + if (resolvedPath && isBarrelFile(resolvedPath)) { + path.node.specifiers.forEach(specifier => { + const wasImportTransformed = transformImport({ + builder, + path, + importSource: resolvedPath, + specifier, + }); + + if (wasImportTransformed && !importTransformed) { + importTransformed = true; + } + }); + } + + // DONE: If the import was transformed into a deep import, delete the original import + if (importTransformed) { + path.prune(); + } + } + }, + }; +}; + +export { transform }; diff --git a/src/transforms/deBarrelify/replaceImportDeclarationWithDeepImport/replaceImportDeclarationWithDeepImport.js b/src/transforms/deBarrelify/replaceImportDeclarationWithDeepImport/replaceImportDeclarationWithDeepImport.js new file mode 100644 index 0000000..825c48e --- /dev/null +++ b/src/transforms/deBarrelify/replaceImportDeclarationWithDeepImport/replaceImportDeclarationWithDeepImport.js @@ -0,0 +1,117 @@ +import { getBaseUrl } from "../../../tsconfigUtils/getBaseUrl.js"; +import { getAlias } from "../resolver.js"; + +const srcDirectory = getBaseUrl(); +const aliases = getAlias(); + +/** + * Removes the file extension from a path + * + * @param {string} path + * @returns {string} + */ +const removeFileExtension = path => { + return path.replace(/\.[^/.]+$/, ""); +}; + +/** + * Removes the src directory from a path + * + * @param {string} path + * @returns {stirng} + */ +const removeSrcDirectory = path => { + return path.replace(`${srcDirectory}/`, ""); +}; + +/** + * Given a path, determines if it should start with an alias + * + * @param {string} path + * @returns {string | undefined} + */ +const findMatchingAlias = path => { + return Object.keys(aliases).find(alias => path.startsWith(`${srcDirectory}/${aliases[alias]}`)); +}; + +/** + * Given a path, if it doesn't start with a ".", add "./" to the beginning + * + * @param {string} path + * @returns {string} + */ +const makeRelativePath = path => { + if (path.startsWith(".")) { + return path; + } + + return `./${path}`; +}; + +/** + * Given a specifier, importSource and path + * replace the existing ImportDeclaration with a new one that deeply imports the specifier + * + * @typedef {Object} replaceImportDeclarationWithDeepImportParams + * @property {import("ast-types/gen/builders").builders} builder - Recast builder for transforming the AST + * @property {import("ast-types/lib/node-path").NodePath} path - the path to replace + * @property {string} newImportSource - import source + * @property {import("ast-types/gen/kinds").ImportSpecifierKind} specifier - the specifier to import + * @property {boolean} importAs - whether or not to import as wildcard + * + * @param {replaceImportDeclarationWithDeepImportParams} + * @returns {void} + */ +const replaceImportDeclarationWithDeepImport = ({ + builder, + path, + newImportSource, + specifier, + importAs, +}) => { + // If the specifier doesn't have an imported name, we don't do anything + // e.g.:import "@testing-library/jest-native/extend-expect"; + if (!specifier.imported?.name) { + return; + } + + // DONE: make sure to add the @shared alias to the import source + // using insertAfter so that imports with multiple specifiers don't overwrite one another + let newSpecifier = builder.importSpecifier(builder.identifier(specifier.imported.name)); + + // Some imports have a local name, so we need to account for that + if (specifier.local && !importAs) { + // type based import specifiers aren't supported by recast, these will need to be manually fixed + newSpecifier = builder.importSpecifier( + builder.identifier(specifier.imported.name), + builder.identifier(specifier.local.name) + ); + } else if (specifier.local && importAs) { + // Some imports are imported as a wildcard, so we need to account for that + // e.g: import * as React from "react"; + newSpecifier = builder.importNamespaceSpecifier(builder.identifier(specifier.local.name)); + } + + const matchingAlias = findMatchingAlias(newImportSource); + const importSourceWithoutExtension = removeFileExtension(newImportSource); + const importSourceWithoutSrcDirectory = removeSrcDirectory(importSourceWithoutExtension); + let finalizedImportSource = makeRelativePath(importSourceWithoutSrcDirectory); + + // if source matches a tsconfig alias, replace the path with the alias + if (matchingAlias) { + finalizedImportSource = importSourceWithoutSrcDirectory.replace( + aliases[matchingAlias], + matchingAlias + ); + } + + path.insertAfter( + builder.importDeclaration( + [newSpecifier], + builder.stringLiteral(finalizedImportSource), + path.node.importKind + ) + ); +}; + +export { replaceImportDeclarationWithDeepImport }; diff --git a/src/transforms/deBarrelify/replaceImportDeclarationWithDeepImport/replaceImportDeclarationWithDeepImport.spec.js b/src/transforms/deBarrelify/replaceImportDeclarationWithDeepImport/replaceImportDeclarationWithDeepImport.spec.js new file mode 100644 index 0000000..7e80d4e --- /dev/null +++ b/src/transforms/deBarrelify/replaceImportDeclarationWithDeepImport/replaceImportDeclarationWithDeepImport.spec.js @@ -0,0 +1,276 @@ +import { beforeEach, describe, it, vi, expect } from "vitest"; +import { replaceImportDeclarationWithDeepImport } from "./replaceImportDeclarationWithDeepImport.js"; +import { types } from "recast"; + +const builder = types.builders; + +vi.mock("../../../tsconfigUtils/getBaseUrl.js", () => ({ + getBaseUrl: () => "./src", +})); + +vi.mock("../resolver.js", () => ({ + getAlias: () => ({ + "@components": "components", + }), +})); + +describe("replaceImportDeclarationWithDeepImport", () => { + let path; + + beforeEach(() => { + path = { + insertAfter: vi.fn(), + node: { + importKind: "value", + }, + }; + }); + + it("replaces the existing ImportDeclaration with a new one that deeply imports the specifier", () => { + replaceImportDeclarationWithDeepImport({ + builder, + path, + newImportSource: "./src/utils/useDebounce.ts", + specifier: { imported: { name: "useDebounce" } }, + importAs: false, + }); + + expect(path.insertAfter).toHaveBeenCalledWith( + expect.objectContaining({ + importKind: "value", + source: expect.objectContaining({ + extra: { + raw: '"./utils/useDebounce"', + rawValue: "./utils/useDebounce", + }, + type: "StringLiteral", + value: "./utils/useDebounce", + }), + specifiers: [ + expect.objectContaining({ + imported: { + comments: null, + loc: null, + name: "useDebounce", + optional: false, + type: "Identifier", + typeAnnotation: null, + }, + }), + ], + type: "ImportDeclaration", + }) + ); + }); + + describe("when the specifier has a local name", () => { + it("replaces the existing ImportDeclaration with a new one with the same local name as the existing declaration", () => { + replaceImportDeclarationWithDeepImport({ + builder, + path, + newImportSource: "./src/Button/Button.tsx", + specifier: { imported: { name: "Props" }, local: { name: "ButtonProps" } }, + importAs: false, + }); + + expect(path.insertAfter).toHaveBeenCalledWith( + expect.objectContaining({ + importKind: "value", + source: expect.objectContaining({ + extra: { + raw: '"./Button/Button"', + rawValue: "./Button/Button", + }, + type: "StringLiteral", + value: "./Button/Button", + }), + specifiers: [ + expect.objectContaining({ + imported: { + comments: null, + loc: null, + name: "Props", + optional: false, + type: "Identifier", + typeAnnotation: null, + }, + local: { + comments: null, + loc: null, + name: "ButtonProps", + optional: false, + type: "Identifier", + typeAnnotation: null, + }, + }), + ], + type: "ImportDeclaration", + }) + ); + }); + }); + + describe("when importDeclaration is importKind type", () => { + it("creates a new ImportDeclaration with importKind type", () => { + let path = { + insertAfter: vi.fn(), + node: { + importKind: "type", + }, + }; + + replaceImportDeclarationWithDeepImport({ + builder, + path, + newImportSource: "./src/Button/Button.tsx", + specifier: { imported: { name: "Props" }, local: { name: "ButtonProps" } }, + importAs: false, + }); + + expect(path.insertAfter).toHaveBeenCalledWith( + expect.objectContaining({ importKind: "type" }) + ); + }); + }); + + describe("when importDeclaration is a relative parent path", () => { + it("creates a new ImportDeclaration with importKind type", () => { + let path = { + insertAfter: vi.fn(), + node: { + importKind: "type", + }, + }; + + replaceImportDeclarationWithDeepImport({ + builder, + path, + newImportSource: "../foo/bar.tsx", + specifier: { imported: { name: "Props" }, local: { name: "BarProps" } }, + importAs: false, + }); + + expect(path.insertAfter).toHaveBeenCalledWith( + expect.objectContaining({ + importKind: "type", + source: expect.objectContaining({ + extra: { + raw: '"../foo/bar"', + rawValue: "../foo/bar", + }, + type: "StringLiteral", + value: "../foo/bar", + }), + specifiers: [ + expect.objectContaining({ + imported: { + comments: null, + loc: null, + name: "Props", + optional: false, + type: "Identifier", + typeAnnotation: null, + }, + local: { + comments: null, + loc: null, + name: "BarProps", + optional: false, + type: "Identifier", + typeAnnotation: null, + }, + }), + ], + type: "ImportDeclaration", + }) + ); + }); + }); + + describe("when importDeclaration is an aliased path", () => { + it("creates a new ImportDeclaration with aliased source", () => { + replaceImportDeclarationWithDeepImport({ + builder, + path, + newImportSource: "./src/components/Button/Button.tsx", + specifier: { imported: { name: "Props" }, local: { name: "ButtonProps" } }, + importAs: false, + }); + + expect(path.insertAfter).toHaveBeenCalledWith( + expect.objectContaining({ + importKind: "value", + source: expect.objectContaining({ + extra: { + raw: '"@components/Button/Button"', + rawValue: "@components/Button/Button", + }, + type: "StringLiteral", + value: "@components/Button/Button", + }), + specifiers: [ + expect.objectContaining({ + imported: { + comments: null, + loc: null, + name: "Props", + optional: false, + type: "Identifier", + typeAnnotation: null, + }, + local: { + comments: null, + loc: null, + name: "ButtonProps", + optional: false, + type: "Identifier", + typeAnnotation: null, + }, + }), + ], + type: "ImportDeclaration", + }) + ); + }); + }); + + describe("when importDeclaration is a wildcard import", () => { + it("creates a new ImportDeclaration with aliased source", () => { + replaceImportDeclarationWithDeepImport({ + builder, + path, + newImportSource: "./src/styles/dimensions.ts", + specifier: { imported: { name: "dimensions" } }, + importAs: true, + }); + + expect(path.insertAfter).toHaveBeenCalledWith( + expect.objectContaining({ + importKind: "value", + source: expect.objectContaining({ + extra: { + raw: '"./styles/dimensions"', + rawValue: "./styles/dimensions", + }, + type: "StringLiteral", + value: "./styles/dimensions", + }), + specifiers: [ + expect.objectContaining({ + imported: { + comments: null, + loc: null, + name: "dimensions", + optional: false, + type: "Identifier", + typeAnnotation: null, + }, + local: null, + }), + ], + type: "ImportDeclaration", + }) + ); + }); + }); +}); diff --git a/src/transforms/deBarrelify/resolver.js b/src/transforms/deBarrelify/resolver.js new file mode 100644 index 0000000..b692b1f --- /dev/null +++ b/src/transforms/deBarrelify/resolver.js @@ -0,0 +1,88 @@ +import resolver from "enhanced-resolve"; +import { getBaseUrl } from "../../tsconfigUtils/getBaseUrl.js"; +import { getPaths } from "../../tsconfigUtils/getPaths.js"; + +/** + * @typedef {object} getAliasParams + * @property {import("get-tsconfig").TsConfigJsonResolved} tsconfig - The resolved tsconfig as an object + * @param {getAliasParams} param + * + * @return {object} - Resolver alias + */ +const getAlias = () => { + let alias = {}; + const tsconfigPaths = getPaths(); + + // Convert tsconfig paths to alias that is understood by enhanced-resolve + Object.keys(tsconfigPaths).forEach(key => { + alias[key.replace("/*", "")] = tsconfigPaths[key][0].replace("/*", ""); + }); + + return alias; +}; + +/** + * @typedef {object} getResolverParams + * @param {getResolverParams} param + * + * @returns {import("enhanced-resolve/types").ResolveFunction} + */ +const getResolver = () => { + const baseUrl = getBaseUrl(); + + return resolver.create.sync({ + preferRelative: true, + modules: ["node_modules", baseUrl], + extensions: [ + ".tsx", + ".ts", + ".d.ts", + ".mjs", + ".json", + ".js", + ".jsx", + ".sass", + ".scss", + ".css", + ".module.sass", + ".module.scss", + ".module.css", + ".png", + ".gif", + ".jpeg", + ".jpg", + ], + alias: getAlias(), + }); +}; + +/** + * Given a import source, returns the deep absolute source + * + * @typedef {Object} resolveAbsolutePathParams + * @property {*} context - a context? + * @property {string} folderPath - relative project path + * @property {resolveContext} - resolver context + * @property {string} importSource - import source + * @param {resolveAbsolutePathParams} + * + * @returns {string} - the resolved deep import path + */ +const resolveAbsolutePath = ({ context, folderPath, resolveContext, importSource }) => { + try { + const resolveSync = getResolver(); + const resolvedPath = resolveSync(context, folderPath, importSource, resolveContext); + + if (!resolvedPath.startsWith("node_modules")) { + return resolvedPath; + } + } catch (e) { + if (!e.message.includes("is not exported from package node_modules")) { + throw e; + } + } + + return ""; +}; + +export { resolveAbsolutePath, getAlias }; diff --git a/src/tsconfigUtils/getBaseUrl.js b/src/tsconfigUtils/getBaseUrl.js new file mode 100644 index 0000000..f6ce781 --- /dev/null +++ b/src/tsconfigUtils/getBaseUrl.js @@ -0,0 +1,18 @@ +import { getTsconfig } from "get-tsconfig"; + +/** + * Returns the baseUrl from the tsconfig file + * If no source directory is specified, returns the current directory . + * + * @returns {string} - the source directory + */ +const getBaseUrl = () => { + const { config: tsconfig } = getTsconfig() ?? {}; + + const srcDirectory = tsconfig?.compilerOptions?.baseUrl; + if (!srcDirectory) return "."; + + return srcDirectory.replace("./", ""); +}; + +export { getBaseUrl }; diff --git a/src/tsconfigUtils/getPaths.js b/src/tsconfigUtils/getPaths.js new file mode 100644 index 0000000..718131a --- /dev/null +++ b/src/tsconfigUtils/getPaths.js @@ -0,0 +1,14 @@ +import { getTsconfig } from "get-tsconfig"; /** + + * Returns paths specified in the tsconfig file + * If no paths are specified, returns an empty object + * + * @returns {Record} - the paths + */ +const getPaths = () => { + const { config: tsconfig } = getTsconfig() ?? {}; + + return tsconfig?.compilerOptions?.paths ?? {}; +}; + +export { getPaths }; diff --git a/vitest.config.ts b/vitest.config.ts index 712ab5a..9abcf9c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,9 +1,12 @@ /// -import { defineConfig } from 'vite' +import { defineConfig } from "vite"; export default defineConfig({ test: { passWithNoTests: false, - setupFiles: './testSetup/setupFiles.js', + setupFiles: "./testSetup/setupFiles.js", + coverage: { + provider: "v8", + }, }, -}) \ No newline at end of file +}); diff --git a/yarn.lock b/yarn.lock index 3e9ac51..791838e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -225,7 +225,10 @@ __metadata: "@babel/parser": "npm:^7.27.2" "@vitest/coverage-v8": "npm:^2.1.9" cli-highlight: "npm:^2.1.11" + enhanced-resolve: "npm:^5.16.0" + get-tsconfig: "npm:^4.7.5" glob: "npm:^11.0.2" + is-builtin-module: "npm:^4.0.0" prettier: "npm:^3.5.3" recast: "npm:^0.23.11" vitest: "npm:^2.1.9" @@ -673,6 +676,13 @@ __metadata: languageName: node linkType: hard +"builtin-modules@npm:^4.0.0": + version: 4.0.0 + resolution: "builtin-modules@npm:4.0.0" + checksum: 10c0/c10c71c35a1a9b2c5fbb58c1b7eed3f38f16ec0903de55dfa54604ff19895dd7f35b79e5bb0756fc09642714d637ca905653b35ba76c00ae29310ca5c9668bf5 + languageName: node + linkType: hard + "cac@npm:^6.7.14": version: 6.7.14 resolution: "cac@npm:6.7.14" @@ -851,6 +861,16 @@ __metadata: languageName: node linkType: hard +"enhanced-resolve@npm:^5.16.0": + version: 5.18.1 + resolution: "enhanced-resolve@npm:5.18.1" + dependencies: + graceful-fs: "npm:^4.2.4" + tapable: "npm:^2.2.0" + checksum: 10c0/4cffd9b125225184e2abed9fdf0ed3dbd2224c873b165d0838fd066cde32e0918626cba2f1f4bf6860762f13a7e2364fd89a82b99566be2873d813573ac71846 + languageName: node + linkType: hard + "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -1049,6 +1069,15 @@ __metadata: languageName: node linkType: hard +"get-tsconfig@npm:^4.7.5": + version: 4.10.0 + resolution: "get-tsconfig@npm:4.10.0" + dependencies: + resolve-pkg-maps: "npm:^1.0.0" + checksum: 10c0/c9b5572c5118923c491c04285c73bd55b19e214992af957c502a3be0fc0043bb421386ffd45ca3433c0a7fba81221ca300479e8393960acf15d0ed4563f38a86 + languageName: node + linkType: hard + "glob@npm:^10.2.2, glob@npm:^10.4.1": version: 10.4.5 resolution: "glob@npm:10.4.5" @@ -1081,7 +1110,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.2.6": +"graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 @@ -1162,6 +1191,15 @@ __metadata: languageName: node linkType: hard +"is-builtin-module@npm:^4.0.0": + version: 4.0.0 + resolution: "is-builtin-module@npm:4.0.0" + dependencies: + builtin-modules: "npm:^4.0.0" + checksum: 10c0/828754b76beb35aceca9d90e67b55cefbc0a25b706c67a020eecdf8eb84d65cf323d08bb3f99b6c83aab6f9dee20fbf34bb36a9c63de8be14f2af815a681a50c + languageName: node + linkType: hard + "is-fullwidth-code-point@npm:^3.0.0": version: 3.0.0 resolution: "is-fullwidth-code-point@npm:3.0.0" @@ -1644,6 +1682,13 @@ __metadata: languageName: node linkType: hard +"resolve-pkg-maps@npm:^1.0.0": + version: 1.0.0 + resolution: "resolve-pkg-maps@npm:1.0.0" + checksum: 10c0/fb8f7bbe2ca281a73b7ef423a1cbc786fb244bd7a95cbe5c3fba25b27d327150beca8ba02f622baea65919a57e061eb5005204daa5f93ed590d9b77463a567ab + languageName: node + linkType: hard + "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" @@ -1893,6 +1938,13 @@ __metadata: languageName: node linkType: hard +"tapable@npm:^2.2.0": + version: 2.2.1 + resolution: "tapable@npm:2.2.1" + checksum: 10c0/bc40e6efe1e554d075469cedaba69a30eeb373552aaf41caeaaa45bf56ffacc2674261b106245bd566b35d8f3329b52d838e851ee0a852120acae26e622925c9 + languageName: node + linkType: hard + "tar@npm:^7.4.3": version: 7.4.3 resolution: "tar@npm:7.4.3"