lingui / js-lingui

🌍 📖 A readable, automated, and optimized (3 kb) internationalization for JavaScript
https://lingui.dev
MIT License
4.49k stars 378 forks source link

Monorepo: Error: Can't resolve 'module' in resolve-from #933

Closed StupidIncarnate closed 3 years ago

StupidIncarnate commented 3 years ago

Describe the bug We have a monorepo where our ui projects ref a common ui package. I can:

However when I use t/Trans from @lingui/macro in a @common/ui component, reference it in project1, and go to start project1 (nextjs) or build it, I'm getting an error from the common ui project: Error: Can't resolve 'module' in projects/repo/common/ui/node_modules/import-fresh/node_modules/resolve-from

If I webpack alias @lingui/macro inside the ui project, the error changes to: Error: Can't resolve 'module' in project/repo/project1/node_modules/import-fresh/node_modules/resolve-from

If I only have lingui in project1, it works as expected.

So something breaks when lingui is uses in a common folder and ref'd in the project. It seems like its something to do with config loading but I'm not sure. Any help would be appreciative.

To Reproduce Steps to reproduce the behavior, possibly with minimal code sample, e.g:

Expected behavior I'd like to be able to use lingui in a common pkg that other apps can use and that lingui will know how to pull translations from both the project and the common ui.

Additional context We have a monorepo setup that looks like this:

/common
    /ui
        /src
            /components
        lingui.config.js
        package.json

/project1
    /src
        /components
    lingui.config.js
    package.json
    .babelrc

babel.config.js
lingui.config.js
package.json

project1 lingui.config.js

module.exports = {
  catalogs: [
    {
      path: 'src/locales/{locale}/messages',
      include: ['src'],
      exclude: ['**/node_modules/**'],
    },
    {
       path: '../@common/ui/src/locales/{locale}/messages',
       include: ['../@common/ui'],
       exclude: ['**/node_modules/**'],
    },
  ],
  extractBabelOptions: {
    rootMode: 'upward',
  },
  locales: ['en', 'de'],
  format: 'po',
  sourceLocale: 'en',
};

@common/ui lingui.config.js

module.exports = {
  catalogs: [
    {
      path: 'src/locales/{locale}/messages',
      include: ['src'],
      exclude: ['**/node_modules/**'],
    },
  ],
  extractBabelOptions: {
    rootMode: 'upward',
  },
  locales: ['en', 'de'],
  format: 'po',
  sourceLocale: 'en',
};

root lingui.config.js

module.exports = {
  rootDir: '.',
  locales: ['en', 'de'],
  format: 'po',
  sourceLocale: 'en',
};

project1/.babelrc

{
  "presets": [
    "@babel/preset-react",
    "next"
  ],
  "plugins": [
    "babel-plugin-macros",
    ["babel-plugin-styled-components", {
      "ssr": true,
      "displayName": true,
      "preprocess": false
    }],
    "@babel/plugin-syntax-dynamic-import",
    "@babel/plugin-proposal-class-properties"
  ],
  "env": {
    "test": {
      "plugins": [
        "dynamic-import-node"
      ]
    }
  }
}

root babel.config.js

const { readdirSync } = require('fs');
const path = require('path');

const folderName = '@common';
const hasFolder = source =>
  readdirSync(source, { withFileTypes: true })
    .filter(dirent => dirent.isDirectory())
    .find(dirent => dirent.name === FolderName);

const findRoot = filePath => {
  if (!filePath) {
    return false;
  }

  if (hasFolder(filePath)) {
    return filePath;
  }
  return findRoot(filePath.substr(0, filePath.lastIndexOf(path.sep)));
};

module.exports = () => {
  const currentDir = findRoot(process.cwd());

  // console.log('What art thou path, bro?', currentDir);
  const presets = [
    [
      '@babel/preset-env',
      {
        targets: {
          node: true,
        },
      },
    ],
    '@babel/preset-react',
    '@babel/preset-typescript',
  ];
  const plugins = [
    [
      'module-resolver',
      {
        // eslint-disable-next-line no-unused-vars
        resolvePath(sourcePath, currentFile, opts) {
          // console.log(sourcePath, currentFile);
          if (!sourcePath.startsWith(folderName)) {
            return undefined; // Says use current resolve logic
          }

          const splitPath = sourcePath.split('/');
          const folderName = splitPath.shift();
          const pkgName = splitPath.shift();

          if (splitPath.length === 0 || splitPath[0] !== 'src') {
            splitPath.unshift('src');
          }

          const resolvePath = path.join(
            currentDir,
            folderName,
            pkgName,
            splitPath.join(path.sep)
          );
          // console.log(`${sourcePath} <= ${resolvePath}`);

          return resolvePath;
        },
      },
    ],
    '@babel/plugin-transform-react-jsx',
    '@babel/plugin-proposal-export-default-from',
    [
      '@babel/plugin-proposal-decorators',
      {
        legacy: true,
      },
    ],
    '@babel/plugin-proposal-class-properties',
    '@babel/plugin-transform-classes',
    '@babel/plugin-proposal-object-rest-spread',
  ];

  return {
    presets,
    plugins,
    ignore: ['node_modules'],
  };
};

next.config.js

const path = require('path');

const withCustomBabelConfigFile = require('next-plugin-custom-babel-config');

const babelBase = require('../babel.config');

module.exports = withCustomBabelConfigFile({
  webpack: config => {
    // Fixes npm packages that depend on `fs` module
    config.node = {
      fs: 'empty',
    };
    config.module.rules.push({
      test: /\.m?(js|jsx|ts|tsx)$/,
      exclude: /(node_modules)/,
      use: {
        loader: 'babel-loader',
        options: babelBase(),
      },
    });
    // config.module.rules.push({
    //   test: /\.po/,
    //   use: [
    //     {
    //       loader: '@lingui/loader',
    //     },
    //   ],
    // });
    config.resolve = {
      ...config.resolve,
      extensions: [...config.resolve.extensions, '.ts', '.tsx'],
      alias: {
        ...config.resolve.alias,
        'styled-components': path.resolve(
          __dirname,
          'node_modules',
          'styled-components'
        ),
        // '@lingui/macro': path.resolve(
        //   __dirname,
        //   'node_modules',
        //   '@lingui/macro'
        // ),
        // '@lingui/react': path.resolve(
        //   __dirname,
        //   'node_modules',
        //   '@lingui/react'
        // ),
        react: path.resolve(__dirname, 'node_modules', 'react'),
        'react-dom': path.resolve(__dirname, 'node_modules', 'react-dom'),
      },
    };
    return config;
  },
  babelConfigFile: path.resolve('../babel.config.js'),
});
StupidIncarnate commented 3 years ago

Looks like the fix ended up being that I needed 'babel-plugin-macros' declared in the root babel.config.js as well.

semoal commented 3 years ago

Super!! that you found the issue that easy.

If you're interested in contributing to Lingui we're looking for people that used lingui v3 within a monorepo for improve the documentation about that.

https://github.com/lingui/js-lingui/issues/810