23

I'm trying to achieve TypeScript code sharing in a create-react-app in non-eject mode, but I'm running into the infamous restriction that imports outside of src are not allowed:

You attempted to import ../../common/src/lib.ts which falls outside of the project src/ directory. [...]

For the non-TypeScript case this has been asked & answered here, but I can't get any of the solutions to work with TypeScript. Specifically the issues with the proposed solutions are:

  • Setting baseDir in ts-config.json: Here create-react-app complains about: Your project's baseUrl can only be set to src or node_modules. Create React App does not support other values at this time.

  • Approaches based on react-app-rewired: More promising. Disabling the ModuleScopePlugin gets me past the "attempted import outside src" error, but the problem now is that the loader of typescript files doesn't play along:

    enter image description here

    I have verified that the .ts itself is fine: Copying to ./src and importing it from there works fine.

    I have also added ../../common/src folder to the includes list in ts-config.json.

    My guess is that somehow the webpack loader configuration has a rule that prevents the TypeScript loader to transpile files that don't match its expected path patterns. How can this be fixed using react-app-rewired?

  • Symlinking sources doesn't work either -- again with the same problem. Probably because webpack internally resolves the symlinks and sees the file in a path where the normal loader rules don't apply.

  • Eject based solutions: I'd like not to eject for now, but I tried and I'm basically running into the same problem again.

I've also found other questions that sounded related but didn't answer the problem:


Re-using TypeScript typings in a mono repo seems to be a fairly reasonable pattern. Am I missing something or why is the create-react-app making this so difficult?


To reproduce: My code is basically 100% what you get from

npx create-react-app my-app --template typescript

with one import to external added.

bluenote10
  • 23,414
  • 14
  • 122
  • 178
  • I think the monorepo solution makes most sense, since it's symlinking under the hood and would work perfectly fine for a non ejected app. https://classic.yarnpkg.com/en/docs/workspaces/ you just have to make sure that you run `tsc` before you import your types – azium Apr 04 '20 at 21:30
  • @azium Hm, I tried various symlinking patterns, but since webpack internally resolves these symlinks I always ended up with the error above. – bluenote10 Apr 04 '20 at 21:35
  • instead of importing `../something` you would import the module as if it were in your node_modules folder `import { YourType } from 'your-local-module'` – azium Apr 04 '20 at 21:42
  • I have opened an upstream issue: [create-react-app should allow TypeScript imports outside `src`](https://github.com/facebook/create-react-app/issues/8785) – bluenote10 Nov 27 '20 at 15:04
  • This **[example](https://github.com/oklas/react-app-rewire-alias/tree/2f1888e56ae48057cda6ba87f0af2aab66514372/example/main)** using create-reat-app 4.0 with typescript imports file NearSrc.tsx outside of src demonstrate worked solution (**[answer](https://stackoverflow.com/a/66107598/2969544)**) – oklas Feb 08 '21 at 18:57

5 Answers5

17

You could use craco (create-react-app config override) to override the webpack config (abstracted as part of CRA) without ejecting.
Additionally you could use ts-loader to reference non-transpiled ts code directly in external projects (e.g. if you wanted to reference/use shared code/lib as part of a mono-repo).

Assuming your CRA app is in the client directory your project structure is like the following:

client/
|--src/
|--package.json
shared/
|--package.json
|--(ts files)
package.json

cd client
yarn add -D @craco/craco ts-loader
  1. create a craco.config.js file in the client/ (CRA) directory
  2. ensure the craco.config.js has the following content
const path = require("path");

module.exports = {
  webpack: {
    configure: webpackConfig => {

      // ts-loader is required to reference external typescript projects/files (non-transpiled)
      webpackConfig.module.rules.push({
        test: /\.tsx?$/,
        loader: 'ts-loader',
        exclude: /node_modules/,
        options: {
          transpileOnly: true,
          configFile: 'tsconfig.json',
        },
      })
      
      return webpackConfig;
    }
  }
};
  1. Replace react-scripts commands in client/package.json with craco
/* client/package.json */

"scripts": {
-   "start": "react-scripts start",
+   "start": "craco start",
-   "build": "react-scripts build",
+   "build": "craco build"
-   "test": "react-scripts test",
+   "test": "craco test"
}
Zalaboza
  • 8,899
  • 16
  • 77
  • 142
Zeeshan
  • 1,187
  • 1
  • 9
  • 12
  • Note that CRACO doesn't officially support create-react-app 4.0 and [its maintainer doesn't seem to be in the mood to maintain it long term](https://github.com/gsoft-inc/craco/issues/205#issuecomment-715528156). Its annoying that these work-arounds against CRA's limiting defaults break with every major release. – bluenote10 Nov 27 '20 at 14:41
  • 2
    CRACO now supports CRA 4.* but I had a conflict with ts-loader having a later webpack as a dependency. Instead of adding a rule and installing ts-loader, I modified CRA's own rule to not have the `include` field which limits it to `src` dir. You may want some additional checks in case the indexes change, but essentially do this instead of pushing new rule: `delete webpackConfig.module.rules[1].oneOf[2].include;` – ekuusela Jun 04 '21 at 13:14
9

UPDATE: Since react-app-rewired is only maintained passively and doesn't support CRA versions 2+ (we are are three major versions later at the time of writing), I would no longer recommend this approach.


After more hours of experimenting and reading up on GitHub issues, I finally have a working solution. Big thanks to BirukDmitry who made this very helpful post on GitHub. Step-by-step guide:

  1. Install react-app-rewired and customize-cra

     npm i react-app-rewired customize-cra --save-dev
    
  2. Configure react-app-rewird with a minimal config-overrides.js like this:

    const { removeModuleScopePlugin, override, babelInclude } = require("customize-cra");
    const path = require("path");
    
    module.exports = override(
      removeModuleScopePlugin(),        // (1)
      babelInclude([
        path.resolve("src"),
        path.resolve("../common/src"),  // (2)
      ])
    );
    

    Setting (1) is similar to what is needed for getting around the import-outside-src limitation in general (see linked question).

    Setting (2) is crucial though to enabled babel-transpilation (and thus, including TS type stripping I presume) for other paths as well. This is where you have to put add your paths from which you want to import.

  3. No adaptations needed for tsconfig.json.

  4. Import using relative paths, e.g., import * as mymodule from '../../common/src/mymodule'.

bluenote10
  • 23,414
  • 14
  • 122
  • 178
  • 1
    The `babelInclude` configuration was all that I was missing to resolve the aforementioned error. Thanks for sharing! – Doug Wilhelm Jul 29 '20 at 20:11
3

@bluenote10 solution did the trick!

There was only one issue in step #2 that prevents Babel from transpilling ts files from ../../common/src That is modified version of @bluenote10's code (that finally worked for me)


  1. Install react-app-rewired and customize-cra

     npm i react-app-rewired customize-cra --save-dev
    
  2. Configure react-app-rewird with a minimal config-overrides.js like this:

     const { removeModuleScopePlugin, override, babelInclude } = require("customize-cra");
     const path = require("path");
    
     module.exports = function (config, env) {
    
         return Object.assign( // We need Object.assign() to not to loose initial config 
           config,
           override(
             removeModuleScopePlugin(),       //1
             babelInclude([
               path.resolve('src'),
               path.resolve('../common/src'), //2
             ])
           )(config, env)
         )
       }
    
    

    Setting (1) is similar to what is needed for getting around the import-outside-src limitation in general (see linked question).

    Setting (2) is crucial though to enabled babel-transpilation (and thus, including TS type stripping I presume) for other paths as well. This is where you have to put add your paths from which you want to import.

  3. No adaptations needed for tsconfig.json.

  4. Import using relative paths, e.g., import * as mymodule from '../../common/src/mymodule'.

1

The alias solution for craco or rewired create-react-app is react-app-alias for systems as: craco, react-app-rewired, customize-cra

According docs of mentioned systems replace react-scripts in package.json and configure next:

react-app-rewired

// config-overrides.js
const {aliasWebpack, aliasJest} = require('react-app-alias')

const options = {} // default is empty for most cases

module.exports = aliasWebpack(options)
module.exports.jest = aliasJest(options)

craco

// craco.config.js
const {CracoAliasPlugin} = require('react-app-alias')

module.exports = {
  plugins: [
    {
      plugin: CracoAliasPlugin,
      options: {}
    }
  ]
}

all

Configure aliases in json like this:

// tsconfig.paths.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "example/*": ["example/src/*"],
      "@library/*": ["library/src/*"]
    }
  }
}

And add this file in extends section of main typescript config file:

// tsconfig.json
{
  "extends": "./tsconfig.paths.json",
  // ...
}
oklas
  • 7,935
  • 2
  • 26
  • 42
  • Weird, react-app-rewired and customize-cra don't officially support CRA 4, right? Does it only work by chance, or just with a limited feature set? – bluenote10 Feb 09 '21 at 10:31
  • 3
    This solution doesn't solve the issue. The issue is that we want to import shared code outside the root. not near src. If you have multiple projects and want to share components between those projects but you don't want to publish your components to npm – user158443 Jul 22 '21 at 15:03
  • @oklas v1.1.0 of cra or ts or what? – user158443 Oct 01 '21 at 09:11
  • @user158443, imports from outside of root folder for CRA (using craco or rewired cra) with typescript is available in *react-app-rewire-alias* from version **v1.1.0** – oklas Oct 01 '21 at 12:38
  • @bluenote10, that was some bug in CRA exact version v4.0, however it was worked also for v4.0 with some additional actions ([more details here](https://github.com/oklas/react-app-rewire-alias/issues/9)), it was fixed in CRA v4.1 and since that it is worked. – oklas Oct 01 '21 at 12:38
0

question where does ../../common/src/mymodule lives at ? is it another project ? If it is another project why dont you link'em

  • inside common code project run npm link

  • inside the project that will use the common code: npm link common-project

If there are not two different projects. why does it has to be outside of src ?

The link you posted The create-react-app imports restriction outside of src directory and your case/problem are two totally different thing, plus they be tripping so hard. let me give you and idea of that problem and yours.

As we already know CRA creates a SPA single page app. which means that there is only one single html file. everything happens at the index.html which is located at <project>/public/index.html. If we compile the code we'll notice something the end bundle might look some like this

build
--static
----js
----css
----media
--index.html
--...more hashed crap...
public
src

and by default process.env.PUBLIC_URL is set to "/" WHAT?? yes look let me show you. there is an old say a picture explains more than 1000 words. enter image description here

if we look at the image that according to its path it is located at ./src/styles/imageSlack.jpg or some. So what if we console.log it.

enter image description here

WHAAAT!! where they do that at ? one thing you can do to test my theory is if you console.log(process.env.PUBLIC_URL) any where in yow code. now here is the big diference between this and that.

Browsers natively do not know Typescript.

So either you set yow code inside src and we end happy or follow the separate of concerns principle and we create an npm package with the shared code and just imported as any other module.

Ernesto
  • 3,944
  • 1
  • 14
  • 29