babel-plugin-module-resolver icon indicating copy to clipboard operation
babel-plugin-module-resolver copied to clipboard

Typescript paths + babel 7

Open axelnormand opened this issue 6 years ago • 11 comments

I'm loving using the new babel 7 typescript preset in my react monorepo, so nice and simple.

However thought I'd open a discussion of possible improvements to babel-plugin-module-resolver (that I could create a PR for) to help with resolving tsconfig.json "paths" setting automatically. Or maybe this is time to create a typescript specific module resolve plugin instead?

The premise is that I have a monorepo with "app1", "app2", and "components" projects. App1 i can do import Foo from '@components/Foo'. I also prefer absolute imports over relative imports so all projects can import Foo from 'src/Foo' over import Foo from '../../Foo' within themselves.

I had to write some code to make babel-plugin-module-resolver resolve those imports by reading in the tsconfig.json file and translating the "paths" setting to suitable format for "alias" setting in babel-plugin-module-resolver.

Then also i overrode resolvePath with a special case catching the "src" absolute imports. If app1 imports @components/Foo which in turn imports src/theme, it now correctly returns say c:\git\monorepo\components\src\theme instead of c:\git\monorepo\app1\src\theme

Here is app1 tsconfig.json snippet with the paths setting:

{
  "extends": "../tsconfig.base.json",
  "compilerOptions": {
    "baseUrl": ".",
    "rootDir": "src",
    "outDir": "build/tsc",
    "paths": {
      "src/*": ["src/*"],
      "@components/*": ["../components/src/*"],
    },
  },
  "references": [{ "path": "../components/" }]
}

I can provide my code if needed to explain things better.

Perhaps I make a PR to help future people wanting to use this plugin with typescript paths. There could be a setting saying useTsConfigPaths: true for instance to automatically resolve them.

Not sure how one would fix the "src" alias in all projects problem too?

Thanks

axelnormand avatar Dec 05 '18 16:12 axelnormand

Had the same use-case (but using babel to compile), but did not manage to make the src alias working inside a monorepo yet, in any case (webpack config or this babel plugin) I'm missing the way to tell to use a path relative to the root of a package (and not the root of the mono-repo, __dirname, etc.).

However it's working with the moduleNameMapper option of jest:

  moduleNameMapper: {
    '^src/(.*)': '<rootDir>/src/$1'
  }

Would be great to have something like <rootDir> for this plugin, could solve the src issue.

mgcrea avatar Jan 30 '19 00:01 mgcrea

@axelnormand - Maybe I'm missing something, but what's the benefit from using both the plugin and the config in typescript? I feel like you can do pretty much the same thing only by using the typescript options.

tleunen avatar Jan 30 '19 01:01 tleunen

Hi @tleunen and thanks for the cool plugin.

The tsconfig paths is for tsc to successfully compile the imports. I only use tsc as a linting step.

This resolver plugin is for the outputted JS . I'm now using babel typescript plugin which does no typechecking. Using babel means dont need special tools for typescript compilation in other parts of the stack like jest. Also can use other babel plugins (styled components) easily

So I believe i need both or am i missing something?

axelnormand avatar Jan 30 '19 10:01 axelnormand

For reference here's my code for reading tsconfig paths to set the aliases in this plugin. Also a resolve fix so the correct src/foo path followed in my monorepo.

Yarn Workspaces + Lerna monorepo structure has a common "components" project and an "app1" and "app2" project which import those common components

// Part of common babel config js file in my monorepo

/** 
 * Create alias setting for module-resolver plugin based off tsconfig.json paths 
 * 
 * Before in tsconfig.json in project:
 * 
 "paths": {
      "src/*": ["src/*"],
      "@blah/components/*": ["../components/src/*"],
    },
 * 
 *
 * After to pass as `alias` key in 'module-resolver' plugin:
 * 
 "alias": {
      "src": ["./src"],
      "@blah/components": ["./../components/src"],
    },
 * 
 */
const getResolverAlias = projectDir => {
  const tsConfigFile = path.join(projectDir, 'tsconfig.json');
  const tsConfig = require(tsConfigFile);

  const tsConfigPaths =
    (tsConfig.compilerOptions && tsConfig.compilerOptions.paths) || {};

  // remove the "/*" at end of tsConfig paths key and values array
  const pathAlias = Object.keys(tsConfigPaths)
    .map(tsKey => {
      const pathArray = tsConfigPaths[tsKey];
      const key = tsKey.replace('/*', '');
      // make sure path starts with "./"
      const paths = pathArray.map(p => `./${p.replace('/*', '')}`);
      return { key, paths };
    })
    .reduce((obj, cur) => {
      obj[cur.key] = cur.paths; // eslint-disable-line no-param-reassign
      return obj;
    }, {});

  return pathAlias;
};



/**
 * Also add special resolving of the "src" tsconfig paths.
 * This is so "src" used within the common projects (eg within components) correctly resolves
 *
 * eg In app1 project if you import `@blah/components/Foo` which in turn imports `src/theme`
 * then for `@blah/components/Foo/Foo.tsx` existing module resolver incorrectly looks for src/theme`
 * within `app1` folder not `components`
*
 * This now returns:`c:\git\Monorepo\components\src\theme`
 * Instead of: `c:\git\Monorepo\app1\src\theme`
 */
const fixResolvePath = (projectDir) => (
  sourcePath,
  currentFile,
  opts,
) => {
  const ret = resolvePath(sourcePath, currentFile, opts);
  if (!sourcePath.startsWith('src')) return ret; // ignore non "src" dirs

  // common root folder of all apps (ie "c:\git\Monorepo")
  const basePath = path.join(projectDir, '../');

  // currentFile is of form "c:\git\Monorepo\components\src\comps\Foo\Foo.tsx"
  // extract which project this file is in, eg "components"
  const currentFileEndPath = currentFile.substring(basePath.length); 
  const currentProject = currentFileEndPath.split(path.sep)[0]; 

  // sourcePath is the path in the import statement, eg "src/theme"
  // So return path to file in *this* project: eg "c:\git\Monorepo\components\src\theme"
  // out of the box module-resolver was previously returning the app folder eg "c:\git\Monorepo\app1\src\theme"
  const correctResolvedPath = path.join(
    basePath,
    currentProject,
    `./${sourcePath}`,
  );

  return correctResolvedPath;
};


const getBabelConfig = (projectDir) => {
  const isJest = process.env.NODE_ENV === 'test';

  const presets = [
    [
      '@babel/env',
      {
        // normally don't transpile import statements so webpack can do tree shaking
        // for jest however (NODE_ENV=test) need to transpile import statements
        modules: isJest ? 'auto' : false,
        // pull in bits you need from babel polyfill eg regeneratorRuntime etc
        useBuiltIns: 'usage',
        targets: '> 0.5%, last 2 versions, Firefox ESR, not dead',
      },
    ],
    '@babel/react',
    '@babel/typescript',
  ];

  
const plugins = [
    [
      // Create alias paths for module-resolver plugin based off tsconfig.json paths
      'module-resolver',
      {
        cwd: 'babelrc', // use the local babel.config.js in each project
        root: ['./'],
        alias: getResolverAlias(projectDir),
        resolvePath: fixResolvePath(projectDir),
      },
    ],
    'babel-plugin-styled-components',
    '@babel/proposal-class-properties',
    '@babel/proposal-object-rest-spread',
    '@babel/plugin-syntax-dynamic-import',
  ];

  return {
    presets,
    plugins,
  };
};

module.exports = {
  getBabelConfig,
};


axelnormand avatar Jan 30 '19 11:01 axelnormand

Yup, forgot about a webpack compilation. I'm away for the next couple days, but I'll come back to this thread shortly after. Thanks for sharing your config.

tleunen avatar Jan 30 '19 15:01 tleunen

Just came across the same issue. Are we out of luck for the time being or is there a non-invasive workaround?

ackvf avatar Feb 13 '19 15:02 ackvf

With typescript becoming more and more popular every day. I'd love seeing something like this by default in the plugin. If anyone is interested in making a PR.

tleunen avatar Feb 13 '19 15:02 tleunen

@tleunen interesting feature. How do you see it's implementation? Something like:

  1. read tsconfig.json (stop on read error / should the filepath be configurable?)
  2. grab baseUrl + paths (stop if they are empty)
  3. ensure no conflicts with the plugin config (we can pick either of options and warn user or merge or fail with error)
  4. PROFIT

miraage avatar Mar 24 '20 14:03 miraage

I recently discovered the project tsconfig-paths. It's almost perfect… but it's not a babel plugin 😅.

It seems very stable and has the right APIs to get this done pretty easily. I'm thinking it could be combined with this plugin's resolvePath API.

ricokahler avatar Jan 26 '21 03:01 ricokahler

I gave the above a try and it works!

// babelrc.js

const fs = require('fs');
const { createMatchPath, loadConfig } = require('tsconfig-paths');
const {
  resolvePath: defaultResolvePath,
} = require('babel-plugin-module-resolver');

const configLoaderResult = loadConfig();

const extensions = ['.js', '.jsx', '.ts', '.tsx'];

const configLoaderSuccessResult =
  configLoaderResult.resultType === 'failed' ? null : configLoaderResult;

const matchPath =
  configLoaderSuccessResult &&
  createMatchPath(
    configLoaderSuccessResult.absoluteBaseUrl,
    configLoaderSuccessResult.paths,
  );

const moduleResolver = configLoaderSuccessResult && [
  'module-resolver',
  {
    extensions,
    resolvePath: (sourcePath, currentFile, opts) => {
      if (matchPath) {
        return matchPath(sourcePath, require, fs.existsSync, extensions);
      }

      return defaultResolvePath(sourcePath, currentFile, opts);
    },
  },
];

module.exports = {
  presets: [
    ['@babel/preset-env', { targets: { node: true } }],
    '@babel/preset-typescript',
  ],
  plugins: [
    // optionally include
    ...(moduleResolver ? [moduleResolver] : []),
  ],
};

@tleunen if you provide an API spec, I can send out a PR for the above.

(props, btw for the very pluggable plugin, makes this crazy stuff possible 😎)

ricokahler avatar Jan 26 '21 04:01 ricokahler

I did eventually publish a babel plugin for the above: babel-plugin-tsconfig-paths-module-resolver

ricokahler avatar Aug 30 '21 05:08 ricokahler