tleunen / babel-plugin-module-resolver

Custom module resolver plugin for Babel
MIT License
3.45k stars 204 forks source link

Typescript paths + babel 7 #336

Open axelnormand opened 5 years ago

axelnormand commented 5 years ago

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

mgcrea commented 5 years ago

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.

tleunen commented 5 years ago

@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.

axelnormand commented 5 years ago

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 commented 5 years ago

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,
};
tleunen commented 5 years ago

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.

ackvf commented 5 years ago

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

tleunen commented 5 years ago

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.

miraage commented 4 years ago

@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

ricokahler commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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