20

I currently have a monorepo where I have two (+) packages using yarn workspaces:

root
  /packages
    /common
    /web
  ...

root/package.json

  ...
  "workspaces": {
    "packages": [
      "packages/*"
    ],
    "nohoist": [
      "**"
    ],
  },

web is a plain create-react-app with the typescript template. I have some TS code in common I want to use in web, but it seems that CRA does not support compiling code outside of src, giving me an error when I try to import from common in web:

../common/state/user/actions.ts 4:66
Module parse failed: Unexpected token (4:66)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| import { UpdateUserParams } from './reducer';
| 
> export const login = createAction('USER/LOGIN')<{ username: string; password: string }>();

Thus, I followed the instructions at this comment after ejecting to include paths in /common, but it seems like babel-loader loads the files, but now isn't compiling them from typescript:

./common/state/user/actions.ts
SyntaxError: /Users/brandon/Code/apps/packages/common/state/user/actions.ts: Unexpected token, expected "," (4:66)

  2 | import { UpdateUserParams } from './reducer';
  3 | 
> 4 | export const login = createAction('USER/LOGIN')<{ username: string; password: string }>();
    |                                                                   ^
  5 | 
  6 | export const logout = createAction('USER/LOGOUT')();

How can I get webpack / babel to compile code in common correctly? I know I can have common compile it's own code, but I'd rather not have to recompile code in common every time I want to make a change. Having each app do it itself is ideal.

Webpack.config.js:

....
module: {
      strictExportPresence: true,
      rules: [
        // Disable require.ensure as it's not a standard language feature.
        { parser: { requireEnsure: false } },

        // First, run the linter.
        // It's important to do this before Babel processes the JS.
        // {
        //   test: /\.(js|mjs|jsx|ts|tsx)$/,
        //   enforce: 'pre',
        //   use: [
        //     {
        //       options: {
        //         cache: true,
        //         formatter: require.resolve('react-dev-utils/eslintFormatter'),
        //         eslintPath: require.resolve('eslint'),
        //         resolvePluginsRelativeTo: __dirname,

        //       },
        //       loader: require.resolve('eslint-loader'),
        //     },
        //   ],
        //   include: paths.appSrc,
        // },
        {
          // "oneOf" will traverse all following loaders until one will
          // match the requirements. When no loader matches it will fall
          // back to the "file" loader at the end of the loader list.
          oneOf: [
            // "url" loader works like "file" loader except that it embeds assets
            // smaller than specified limit in bytes as data URLs to avoid requests.
            // A missing `test` is equivalent to a match.
            {
              test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
              loader: require.resolve('url-loader'),
              options: {
                limit: imageInlineSizeLimit,
                name: 'static/media/[name].[hash:8].[ext]',
              },
            },
            // Process application JS with Babel.
            // The preset includes JSX, Flow, TypeScript, and some ESnext features.
            {
              test: /\.(js|mjs|jsx|ts|tsx)$/,
              include: paths.appLernaModules.concat(paths.appSrc), // MAIN CHANGE HERE!!!
              loader: require.resolve('babel-loader'),
              options: {
                customize: require.resolve('babel-preset-react-app/webpack-overrides'),

                plugins: [
                  [
                    require.resolve('babel-plugin-named-asset-import'),
                    {
                      loaderMap: {
                        svg: {
                          ReactComponent: '@svgr/webpack?-svgo,+titleProp,+ref![path]',
                        },
                      },
                    },
                  ],
                ],
                // This is a feature of `babel-loader` for webpack (not Babel itself).
                // It enables caching results in ./node_modules/.cache/babel-loader/
                // directory for faster rebuilds.
                cacheDirectory: true,
                // See #6846 for context on why cacheCompression is disabled
                cacheCompression: false,
                compact: isEnvProduction,
              },
            },
            // Process any JS outside of the app with Babel.
            // Unlike the application JS, we only compile the standard ES features.
            {
              test: /\.(js|mjs)$/,
              exclude: /@babel(?:\/|\\{1,2})runtime/,
              loader: require.resolve('babel-loader'),
              options: {
                babelrc: false,
                configFile: false,
                compact: false,
                presets: [
                  [require.resolve('babel-preset-react-app/dependencies'), { helpers: true }],
                ],
                cacheDirectory: true,
                // See #6846 for context on why cacheCompression is disabled
                cacheCompression: false,

                // Babel sourcemaps are needed for debugging into node_modules
                // code.  Without the options below, debuggers like VSCode
                // show incorrect code and set breakpoints on the wrong lines.
                sourceMaps: shouldUseSourceMap,
                inputSourceMap: shouldUseSourceMap,
              },
            },
            // "postcss" loader applies autoprefixer to our CSS.
            // "css" loader resolves paths in CSS and adds assets as dependencies.
            // "style" loader turns CSS into JS modules that inject <style> tags.
            // In production, we use MiniCSSExtractPlugin to extract that CSS
            // to a file, but in development "style" loader enables hot editing
            // of CSS.
            // By default we support CSS Modules with the extension .module.css
            {
              test: cssRegex,
              exclude: cssModuleRegex,
              use: getStyleLoaders({
                importLoaders: 1,
                sourceMap: isEnvProduction && shouldUseSourceMap,
              }),
              // Don't consider CSS imports dead code even if the
              // containing package claims to have no side effects.
              // Remove this when webpack adds a warning or an error for this.
              // See https://github.com/webpack/webpack/issues/6571
              sideEffects: true,
            },
            // Adds support for CSS Modules (https://github.com/css-modules/css-modules)
            // using the extension .module.css
            {
              test: cssModuleRegex,
              use: getStyleLoaders({
                importLoaders: 1,
                sourceMap: isEnvProduction && shouldUseSourceMap,
                modules: {
                  getLocalIdent: getCSSModuleLocalIdent,
                },
              }),
            },
            // Opt-in support for SASS (using .scss or .sass extensions).
            // By default we support SASS Modules with the
            // extensions .module.scss or .module.sass
            {
              test: sassRegex,
              exclude: sassModuleRegex,
              use: getStyleLoaders(
                {
                  importLoaders: 2,
                  sourceMap: isEnvProduction && shouldUseSourceMap,
                },
                'sass-loader',
              ),
              // Don't consider CSS imports dead code even if the
              // containing package claims to have no side effects.
              // Remove this when webpack adds a warning or an error for this.
              // See https://github.com/webpack/webpack/issues/6571
              sideEffects: true,
            },
            // Adds support for CSS Modules, but using SASS
            // using the extension .module.scss or .module.sass
            {
              test: sassModuleRegex,
              use: getStyleLoaders(
                {
                  importLoaders: 2,
                  sourceMap: isEnvProduction && shouldUseSourceMap,
                  modules: {
                    getLocalIdent: getCSSModuleLocalIdent,
                  },
                },
                'sass-loader',
              ),
            },
            // "file" loader makes sure those assets get served by WebpackDevServer.
            // When you `import` an asset, you get its (virtual) filename.
            // In production, they would get copied to the `build` folder.
            // This loader doesn't use a "test" so it will catch all modules
            // that fall through the other loaders.
            {
              loader: require.resolve('file-loader'),
              // Exclude `js` files to keep "css" loader working as it injects
              // its runtime that would otherwise be processed through "file" loader.
              // Also exclude `html` and `json` extensions so they get processed
              // by webpacks internal loaders.
              exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
              options: {
                name: 'static/media/[name].[hash:8].[ext]',
              },
            },
            // ** STOP ** Are you adding a new loader?
            // Make sure to add the new loader(s) before the "file" loader.
          ],
        },
      ],
    },
    plugins: [
      // Generates an `index.html` file with the <script> injected.
      new HtmlWebpackPlugin(
        Object.assign(
          {},
          {
            inject: true,
            template: paths.appHtml,
          },
          isEnvProduction
            ? {
                minify: {
                  removeComments: true,
                  collapseWhitespace: true,
                  removeRedundantAttributes: true,
                  useShortDoctype: true,
                  removeEmptyAttributes: true,
                  removeStyleLinkTypeAttributes: true,
                  keepClosingSlash: true,
                  minifyJS: true,
                  minifyCSS: true,
                  minifyURLs: true,
                },
              }
            : undefined,
        ),
      ),
      ...

paths.js

'use strict';

const path = require('path');
const fs = require('fs');
const url = require('url');

// Make sure any symlinks in the project folder are resolved:
// https://github.com/facebook/create-react-app/issues/637
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);

const envPublicUrl = process.env.PUBLIC_URL;

function ensureSlash(inputPath, needsSlash) {
  const hasSlash = inputPath.endsWith('/');
  if (hasSlash && !needsSlash) {
    return inputPath.substr(0, inputPath.length - 1);
  } else if (!hasSlash && needsSlash) {
    return `${inputPath}/`;
  } else {
    return inputPath;
  }
}

const getPublicUrl = appPackageJson => envPublicUrl || require(appPackageJson).homepage;

// We use `PUBLIC_URL` environment variable or "homepage" field to infer
// "public path" at which the app is served.
// Webpack needs to know it to put the right <script> hrefs into HTML even in
// single-page apps that may serve index.html for nested URLs like /todos/42.
// We can't use a relative path in HTML because we don't want to load something
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
function getServedPath(appPackageJson) {
  const publicUrl = getPublicUrl(appPackageJson);
  const servedUrl = envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/');
  return ensureSlash(servedUrl, true);
}

const moduleFileExtensions = [
  'web.mjs',
  'mjs',
  'web.js',
  'js',
  'web.ts',
  'ts',
  'web.tsx',
  'tsx',
  'json',
  'web.jsx',
  'jsx',
];

// Resolve file paths in the same order as webpack
const resolveModule = (resolveFn, filePath) => {
  const extension = moduleFileExtensions.find(extension =>
    fs.existsSync(resolveFn(`${filePath}.${extension}`)),
  );

  if (extension) {
    return resolveFn(`${filePath}.${extension}`);
  }

  return resolveFn(`${filePath}.js`);
};

// config after eject: we're in ./config/
module.exports = {
  dotenv: resolveApp('.env'),
  appPath: resolveApp('.'),
  appBuild: resolveApp('build'),
  appPublic: resolveApp('public'),
  appHtml: resolveApp('public/index.html'),
  appIndexJs: resolveModule(resolveApp, 'src/index'),
  appPackageJson: resolveApp('package.json'),
  appSrc: resolveApp('src'),
  appTsConfig: resolveApp('tsconfig.json'),
  appJsConfig: resolveApp('jsconfig.json'),
  yarnLockFile: resolveApp('yarn.lock'),
  testsSetup: resolveModule(resolveApp, 'src/setupTests'),
  proxySetup: resolveApp('src/setupProxy.js'),
  appNodeModules: resolveApp('node_modules'),
  publicUrl: getPublicUrl(resolveApp('package.json')),
  servedPath: getServedPath(resolveApp('package.json')),
};

module.exports.moduleFileExtensions = moduleFileExtensions;

module.exports.lernaRoot = path.resolve(module.exports.appPath, '../../');

module.exports.appLernaModules = [];
module.exports.allLernaModules = fs.readdirSync(path.join(module.exports.lernaRoot, 'packages'));

fs.readdirSync(module.exports.appNodeModules).forEach(folderName => {
  if (folderName === 'react-scripts') {
    return;
  }
  const fullName = path.join(module.exports.appNodeModules, folderName);
  if (fs.lstatSync(fullName).isSymbolicLink()) {
    module.exports.appLernaModules.push(fs.realpathSync(fullName));
  }
});

tsconfig:

// base in root
{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": false,
    "esModuleInterop": true,
    "downlevelIteration": true,
    "lib": ["esnext", "dom"],
    "noUnusedLocals": true,
    "skipLibCheck": true,
    "strict": true,
    "target": "esnext",
    "noEmit": true,
    "moduleResolution": "node",
    "resolveJsonModule": true
  }
}

//packages/web/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "jsx": "react",
    "baseUrl": "src",
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "isolatedModules": true,
    "typeRoots": ["./node_modules/@types"],
  },
  "include": [
    "src"
  ]
}

3 Answers 3

8

I wanted to add my 2 cents, as I was having this problem. I did try initially to compile the shared components folder, but it was awful... It took too long.

Here I am showing you how to do this with react-scripts v4 and TypeScript without ejecting (We will need to use craco, otherwise it's not possible).

Here's the step by step.

  1. Point your shared component folder, in this case, common to the main .tsx or .ts file.
  2. Create a craco.config.js file that loads the babel-loader and transpiles it. This file needs to be copied in every individual application that common is trying to be loaded to.

  1. Pointing your shared component folder. Open your common's package.json (Not the root!):

Step 1

Check that you have the name you'd like to import it from (I'm using in the picture an example of my actual application), have the private set as true, and the source, main, and module entries, have them pointed to your entry file. In my case it was the index.ts

{
  "name": "common",
  "version": "0.1.0",
  "private": true,
  "source": "index.ts",
  "main": "./index.ts",
  "module": "./index.ts",
// The common's package.json content
...
}
  1. In every other repo that you'd like to reference your common package from, create a craco.config.js file, and include the following:
const path = require('path');
/**
 * ALlows us to edit create-react-app configuration
 * without ejecting.
 *
 *
 */
const { getLoader, loaderByName } = require('@craco/craco');
// Replace `components` with your folder's structure.
// Again, Here I'm showcasing my current project.
const absolutePath = path.join(__dirname, '../components');
/**
 * This is extremely important if you're using a CI/CD to build and deploy 
 * your code!!! Read!
 * 
 * If you create a package with a namespace, such as name is @schon/components, you need
 * to add what I've commented down below. Otherwise Yarn will fail in CI/CD environments!!
 */
// const schonComponents = path.join(
//   __dirname,
//   './node_modules/@schon/components',
// );

module.exports = {
  webpack: {
    configure: (webpackConfig, { env, paths }) => {
      // https://medium.com/frontend-digest/using-create-react-app-in-a-monorepo-a4e6f25be7aa
      const { isFound, match } = getLoader(
        webpackConfig,
        loaderByName('babel-loader'),
      );
      if (isFound) {
        const include = Array.isArray(match.loader.include)
          ? match.loader.include
          : [match.loader.include];
        // match.loader.include = include.concat(absolutePath, schonComponents);
        match.loader.include = include.concat(absolutePath);
      }
      return {
        ...webpackConfig,
        /**
         * Optionally, other webpack configuration details.
         */
        // optimization: {
        //   splitChunks: {
        //   },
        // },
      };
    },
  },
  
};

step 2

3
  • I've got a similar solution at the moment leveraging craco. However my intelisense tries to import from relative path when importing from common instead of using the package name. Is that true for you as well? Commented Jun 10, 2021 at 5:09
  • As an example: import {x} from '../common' instead of import {x} from '@scope/common'. Both of them seem to work fine just intellisense seems to only identify the relative import. Commented Jun 10, 2021 at 5:10
  • 1
    On a side note, const absolutePath = path.join(__dirname, '../components'); needs to be modified to the path appropriate for your project. In my case, I had to change ../components to ../ui. Commented Mar 28, 2023 at 19:15
3

Got it working eventually. To get TS to work, you need to explicitly add presets: ['@babel/preset-typescript', { allExtensions: true, isTSX: true }], ['@babel/preset-react'], to your loader in addition to the linked solution:

            // Process application JS with Babel.
            // The preset includes JSX, Flow, TypeScript, and some ESnext features.
            {
              test: /\.(js|mjs|jsx|ts|tsx)$/,
              include: [paths.appSrc, ...paths.appLernaModules],
              loader: require.resolve('babel-loader'),
              options: {
                customize: require.resolve('babel-preset-react-app/webpack-overrides'),
                presets: [// ADD THESE
                  ['@babel/preset-typescript', { allExtensions: true, isTSX: true }],
                  ['@babel/preset-react'],
                ],
                plugins: [
                  [
                    require.resolve('babel-plugin-named-asset-import'),
                    {
                      loaderMap: {
                        svg: {
                          ReactComponent: '@svgr/webpack?-svgo,+titleProp,+ref![path]',
                        },
                      },
                    },
                  ],
                ],
                // This is a feature of `babel-loader` for webpack (not Babel itself).
                // It enables caching results in ./node_modules/.cache/babel-loader/
                // directory for faster rebuilds.
                cacheDirectory: true,
                // See #6846 for context on why cacheCompression is disabled
                cacheCompression: false,
                compact: isEnvProduction,
              },
            },

4
  • Does this mean that you ejected, so you’re not really using create react app anymore? I can’t find any way to access these config files without ejecting. Commented May 23, 2020 at 11:36
  • could you please share your repo somewhere? I really hope to get it working using cra-override without ejecting
    – Shaddix
    Commented Jun 20, 2020 at 6:12
  • Yeah this is definitely ejected. If you go the monorepo route you will probably have to eject relatively quickly.
    – Brandon
    Commented Jun 26, 2020 at 18:00
  • 2
    @BrookJordan you could use craco or similar to override without ejecting github.com/gsoft-inc/craco/tree/master/packages/craco
    – Jon Gold
    Commented Aug 14, 2020 at 0:45
2

As @BrandonM mentioned in his answer, modifying the ejected webpack.config.js file works. This is due to Babel not having the necessary information when running in a different directory above the current project root, because that info is in the package.json file!

Creating a babel.config.json and moving the configuration over from package.json also solves the problem and is cleaner than modifying the webpack.config.js.

That did the trick for me:

{
  "presets": [
    "react-app"
  ]
}

My personal solution for TypeScript + React + Monorepo also includes the tsconfig-paths-webpack-plugin as mentioned in the blog post from Adrei Picus https://medium.com/@NiGhTTraX/making-typescript-monorepos-play-nice-with-other-tools-a8d197fdc680 , so that I can use my aliases in tsconfig.json without having to redefine them:

tsconfig.json
/packages
  /A
    /src
    tsconfig.json
  /B
    /src
    tsconfig.json

(root) tsconfig.json:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@my-app/*": ["packages/*/src"]
    }
  }
}

(A and B) tsconfig.json:

{
  "extends": "../../tsconfig",
  "compilerOptions": {
    // your config
    // do not add baseUrl or paths
  },
  "include": ["src"]
}
1
  • Can you provide the example config of your babel.config.json and .babelrc file in package. I have the same changes and it doesn't seem to be working for me. Commented Apr 1, 2022 at 14:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.