Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typescript paths + babel 7 #336

Open
axelnormand opened this issue Dec 5, 2018 · 11 comments
Open

Typescript paths + babel 7 #336

axelnormand opened this issue Dec 5, 2018 · 11 comments

Comments

@axelnormand
Copy link

axelnormand commented Dec 5, 2018

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
Copy link

mgcrea commented Jan 30, 2019

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
Copy link
Owner

tleunen commented Jan 30, 2019

@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
Copy link
Author

axelnormand commented Jan 30, 2019

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
Copy link
Author

axelnormand commented Jan 30, 2019

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
Copy link
Owner

tleunen commented Jan 30, 2019

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
Copy link

ackvf commented Feb 13, 2019

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

@tleunen
Copy link
Owner

tleunen commented Feb 13, 2019

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
Copy link

miraage commented Mar 24, 2020

@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
Copy link

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
Copy link

ricokahler commented Jan 26, 2021

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
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants