How to dynamically import a module within a NodeJS script that contains path aliases defined in tsconfig.json

183 views Asked by At

Our project uses React / Typescript and Nx with WebPack and Babel. Our monorepo contains dozens of applications. As part of our i18n initiative, I am working on a git hook that will scan each application directory and create a default libs/pages/xyz/src/i18n/translations/messages-en-US.json file that our translation team can ingest into their custom built system in order to generate additional files for other languages like messages-fr-CA.json.

Our first approach is for the Node script to find each root application messages file (ex: libs/pages/xyz/src/i18n/messages.ts), transpile it to ES2022, write it to a temporary file, and then dynamically import that file via import(temp.js) so we can do multiple transformations to that that JSON before writing it to the default libs/pages/xyz/src/i18n/translations/messages-en-US.json.

However, I am running into "module not found" errors when trying to dynamically import these files because each messages.ts file contains aliased import paths...

(ex: messages.ts import's from common/foo/xyz but in our tsconfig.json paths it would be mapped to project-root/libs/shared/common/foo/src/xyz.ts.)

Transpiling the code works fine when using ts.transpileModule(filePath, compilerOptions).outputText but when I try to const module = await import(compiledJSFilePath) I get module resolution errors because the aliased paths as defined in my tsConfig.json are not used when dynamically importing a single file with aliased imports...

// my-app/src/i18n/messages.ts
import fooMessages from 'Common/foo/messages';
import barMessages from 'Common/bar/messages';
... other required imports ...

export default {
  ...fooMessages,
  ...barMessages,
  ... other message files ...
}

// extract-messages.mjs
...
import * as fsp from 'node:fs/promises';
import ts from 'typescript';
...
tsContent = await fsp.readFile(targetAppRootMessageFile, { encoding: 'utf8' });
jsContent = ts.transpileModule(tsContent, compilerOptions).outputText;
await fsp.writeFile(tempOutputFile, jsContent);

const { default: messages } = await import(tempOutputFile);
... do transformations here ...
await fsp.writeFile(defaulti18nEnUSFile, JSON.stringify(transformedMessages, null, 2));
... delete temp file ...
... iterate to next application ...

I am not a NodeJS / Typescript Compiler guru, so any suggestions or guidance would be greatly appreciated...

1

There are 1 answers

3
ghybs On

paths alias in tsconfig.json is only meant for the tsc compiler for types, not for the actually emitted JS code:

Note that this feature does not change how import paths are emitted by tsc, so paths should only be used to inform TypeScript that another tool has this mapping and will use it at runtime or when bundling.

I guess your projects have a specific webpack path alias configuration, and the tsconfig just replicates those, as described in above quote.

Now IIUC, you are writing a new tool script, that will import your compiled TS file as well. Therefore you need to implement the path alias in JS side again. You could e.g. use the tsconfig-paths library:

Use this to load modules whose location is specified in the paths section of tsconfig.json

Then when executing your new script, enable tsconfig-paths so that it adjusts node module resolution according to the tsconfig:

node -r tsconfig-paths/register extract-messages.mjs