Monday, May 20, 2024
22
rated 0 times [  26] [ 4]  / answers: 1 / hits: 8231  / 5 Years ago, wed, july 24, 2019, 12:00:00

In modular environments that use Webpack, TypeScript or other tools that transform ES module imports, path aliases are used, a common convention is @ for src.



It's a frequent problem for me to transform a project with aliased absolute paths:



src/foo/bar/index.js



import baz from '@/baz';


to relative paths:



src/foo/bar/index.js



import baz from '../../baz';


For instance, a project that uses aliases needs to be merged with another project that doesn't use aliases, configuring the latter to use aliases isn't an option due to style guide or other causes.



This cannot be solved with simple search and replace, and fixing import paths manually is tedious and prone to errors. I expect original JavaScript/TypeScript codebase to remain intact in other respects, so transforming it with a transpiler may be not an option.



I would like to achieve this kind of refactoring with IDE of my choice (Jetbrains IDEA/Webstorm/Phpstorm) but would accept a solution with any other IDE (VS Code) or plain Node.js.



How can this be achieved?


More From » visual-studio-code

 Answers
4

Three possible solutions that rewire aliased imports to relative paths:


1. babel-plugin-module-resolver


Use babel-plugin-module-resolver, while leaving out other babel plugins/presets.


.babelrc:
"plugins": [
[
"module-resolver",
{
"alias": {
"^@/(.+)": "./src/\1"
}
}
]
]

Build step: babel src --out-dir dist (output in dist, won't modify in-place)


Processed example file:
// input                                // output
import { helloWorld } from "@/sub/b" // import { helloWorld } from "./sub/b";
import "@/sub/b" // import "./sub/b";
export { helloWorld } from "@/sub/b" // export { helloWorld } from "./sub/b";
export * from "@/sub/b" // export * from "./sub/b";

For TS, you will also need @babel/preset-typescript and activate .ts extensions by babel src --out-dir dist --extensions ".ts".


2. Codemod jscodeshift with Regex


All relevant import/export variants from MDN docs should be supported. The algorithm is implemented like this:


1. Input: path aliases mapping in the form alias -> resolved path akin to TypeScript tsconfig.json paths or Webpack's resolve.alias:


const pathMapping = {
"@": "./custom/app/path",
...
};

2. Iterate over all source files, e.g. traverse src:


jscodeshift -t scripts/jscodeshift.js src # use -d -p options for dry-run + stdout
# or for TS
jscodeshift --extensions=ts --parser=ts -t scripts/jscodeshift.js src

3. For each source file, find all import and export declarations


function transform(file, api) {
const j = api.jscodeshift;
const root = j(file.source);

root.find(j.ImportDeclaration).forEach(replaceNodepathAliases);
root.find(j.ExportAllDeclaration).forEach(replaceNodepathAliases);
root
.find(j.ExportNamedDeclaration, node => node.source !== null)
.forEach(replaceNodepathAliases);
return root.toSource();
...
};

jscodeshift.js:



/**
* Corresponds to tsconfig.json paths or webpack aliases
* E.g. @/app/store/AppStore -> ./src/app/store/AppStore
*/
const pathMapping = {
@: ./src,
foo: bar,
};

const replacePathAlias = require(./replace-path-alias);

module.exports = function transform(file, api) {
const j = api.jscodeshift;
const root = j(file.source);

root.find(j.ImportDeclaration).forEach(replaceNodepathAliases);
root.find(j.ExportAllDeclaration).forEach(replaceNodepathAliases);

/**
* Filter out normal module exports, like export function foo(){ ...}
* Include export {a} from mymodule etc.
*/
root
.find(j.ExportNamedDeclaration, (node) => node.source !== null)
.forEach(replaceNodepathAliases);

return root.toSource();

function replaceNodepathAliases(impExpDeclNodePath) {
impExpDeclNodePath.value.source.value = replacePathAlias(
file.path,
impExpDeclNodePath.value.source.value,
pathMapping
);
}
};




Further illustration:


import { AppStore } from "@/app/store/appStore-types"

creates following AST, whose source.value of ImportDeclaration node can be modified:


AST


4. For each path declaration, test for a Regex pattern that includes one of the path aliases.


5. Get the resolved path of the alias and convert as path relative to the current file's location (credit to @Reijo)


replace-path-alias.js (4. + 5.):




const path = require(path);

function replacePathAlias(currentFilePath, importPath, pathMap) {
// if windows env, convert backslashes to / first
currentFilePath = path.posix.join(...currentFilePath.split(path.sep));

const regex = createRegex(pathMap);
return importPath.replace(regex, replacer);

function replacer(_, alias, rest) {
const mappedImportPath = pathMap[alias] + rest;

// use path.posix to also create foward slashes on windows environment
let mappedImportPathRelative = path.posix.relative(
path.dirname(currentFilePath),
mappedImportPath
);
// append ./ to make it a relative import path
if (!mappedImportPathRelative.startsWith(../)) {
mappedImportPathRelative = `./${mappedImportPathRelative}`;
}

logReplace(currentFilePath, mappedImportPathRelative);

return mappedImportPathRelative;
}
}

function createRegex(pathMap) {
const mapKeysStr = Object.keys(pathMap).reduce((acc, cur) => `${acc}|${cur}`);
const regexStr = `^(${mapKeysStr})(.*)$`;
return new RegExp(regexStr, g);
}

const log = true;
function logReplace(currentFilePath, mappedImportPathRelative) {
if (log)
console.log(
current processed file:,
currentFilePath,
; Mapped import path relative to current file:,
mappedImportPathRelative
);
}

module.exports = replacePathAlias;




3. Regex-only search and replace


Iterate throught all sources and apply a regex (not tested thoroughly):


^(import.*from\s+["|'])(${aliasesKeys})(.*)(["|'])$

, where ${aliasesKeys} contains path alias "@". The new import path can be processed by modifying the 2nd and 3rd capture group (path mapping + resolving to a relative path).


This variant cannot deal with AST, hence might considered to be not as stable as jscodeshift.


Currently, the Regex only supports imports. Side effect imports in the form import "module-name" are excluded, with the benefit of going safer with search/replace.


Sample:



const path = require(path);

// here sample file content of one file as hardcoded string for simplicity.
// For your project, read all files (e.g. fs.readFile in node.js)
// and foreach file replace content by the return string of replaceImportPathAliases function.
const fileContentSample = `
import { AppStore } from @/app/store/appStore-types
import { WidgetService } from @/app/WidgetService
import { AppStoreImpl } from @/app/store/AppStoreImpl
import { rootReducer } from @/app/store/root-reducer
export { appStoreFactory }
`;

// corresponds to tsconfig.json paths or webpack aliases
// e.g. @/app/store/AppStoreImpl -> ./custom/app/path/app/store/AppStoreImpl
const pathMappingSample = {
@: ./src,
foo: bar
};

const currentFilePathSample = ./src/sub/a.js;

function replaceImportPathAliases(currentFilePath, fileContent, pathMap) {
const regex = createRegex(pathMap);
return fileContent.replace(regex, replacer);

function replacer(_, g1, aliasGrp, restPathGrp, g4) {
const mappedImportPath = pathMap[aliasGrp] + restPathGrp;

let mappedImportPathRelative = path.posix.relative(
path.dirname(currentFilePath),
mappedImportPath
);
// append ./ to make it a relative import path
if (!mappedImportPathRelative.startsWith(../)) {
mappedImportPathRelative = `./${mappedImportPathRelative}`;
}
return g1 + mappedImportPathRelative + g4;
}
}

function createRegex(pathMap) {
const mapKeysStr = Object.keys(pathMap).reduce((acc, cur) => `${acc}|${cur}`);
const regexStr = `^(import.*from\s+[|'])(${mapKeysStr})(.*)([|'])$`;
return new RegExp(regexStr, gm);
}

console.log(
replaceImportPathAliases(
currentFilePathSample,
fileContentSample,
pathMappingSample
)
);




[#6804] Tuesday, July 23, 2019, 5 Years  [reply] [flag answer]
Only authorized users can answer the question. Please sign in first, or register a free account.
patienceannel

Total Points: 674
Total Questions: 101
Total Answers: 101

Location: Northern Mariana Islands
Member since Fri, Jan 15, 2021
3 Years ago
patienceannel questions
;