Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create React App + Typescript In monorepo code sharing

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"
  ]
}

like image 302
BrandonM Avatar asked Jan 22 '20 20:01

BrandonM


People also ask

Can I use TypeScript with create react app?

New App From Scratch If you're building a new app and using create-react-app , the docs are great: You can start a new TypeScript app using templates. To use our provided TypeScript template, append --template typescript to the creation command.

What is monorepo TypeScript?

Monorepos enable you to put all of your apps for a project in a single repository, share code between them, and more! And while this is not a new concept, modern tooling makes it easy to get one setup. In this article, I'll show you how you can setup a monorepo for a Node project using pnpm and TypeScript.

Can we create a mono repo workspace for a react app?

With just a few CLI commands you can setup an entire monorepo with React apps, a component library, packages, and super-performant build and test pipelines that work with just about any tool possible for testing etc.


2 Answers

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

like image 87
Jose A Avatar answered Oct 19 '22 21:10

Jose A


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"]
}
like image 31
Garbanas Avatar answered Oct 19 '22 19:10

Garbanas