facebook / create-react-app

Set up a modern web app by running one command.
https://create-react-app.dev
MIT License
102.49k stars 26.77k forks source link

Symlink behaviour #3547

Open subhero24 opened 6 years ago

subhero24 commented 6 years ago

If I add a symlink in my src directory to another directory, and then include files from that path, create-react-app gives an error:

Module parse failed: Unexpected token You may need an appropriate loader to handle this file type.

I assume create-react-app wants to have this import already built/transpiled. I would expect this to be the case for imports outside the src directory (node_modules for example). But since the symlink resides in the src directory, I would assume create-react-app would fetch these files as if they were truly in src directory.

Is this a bug, or expected behaviour? It makes it really hard to extract common components.

Timer commented 6 years ago

It sounds like a bug, but this should be working. Can you give more details to reproduce this easily & some test files? A repo with README would be fantastic.

subhero24 commented 6 years ago

I created a repo here.

This was just initialisation with CRA under MacOS High Sierra and then symlinking in terminal with:

cd src && ln -s ../symlink

Timer commented 6 years ago

Thanks!

nuthinking commented 6 years ago

I actually have only problems with Symlinks when building for deployment: https://github.com/facebookincubator/create-react-app/issues/3650

On dev it works well.

bradfordlemley commented 6 years ago

@brunovandamme Can you elaborate on your use case? (Is it related to trying to share source between cra-apps and/or monorepo?)

subhero24 commented 6 years ago

I am trying to share components between projects, so I would like to have these components in another directory and symlink to them from all the cra-apps where i would like to use them.

bradfordlemley commented 6 years ago

@brunovandamme Have a look at https://github.com/facebookincubator/create-react-app/issues/1333 which is for supporting source sharing via monorepo manager (lerna and/or yarn workspace).

I proposed a more generic source-sharing solution in https://github.com/facebookincubator/create-react-app/issues/3436, but it has some downsides vs using a monorepo manager: (1) required some cra-configuration to specify allowable source paths, and (2) managing dependencies of those shared components is a job best left to some monorepo manager anyways...so, my favor has turned more toward using monorepo w/ managers for sharing source between apps. Curious if there's a reason for not using lerna and/or yarn workspace?

subhero24 commented 6 years ago

As I understand yarn workspaces uses symlinks in the node_modules directory to other locations. My understanding is that CRA does not process files from node_modules, so I would have to do the transpiling on these external components myself. Is this correct? I would expect CRA to process the files symlinked from src as these files appear as if they where in the project itself.

bradfordlemley commented 6 years ago

Correct, currently CRA does not process those files, but https://github.com/facebookincubator/create-react-app/issues/1333 tracks the feature request to make CRA process those files.

I think PR 3741 is pretty close to completing that feature, but there are some (minor?) open questions about how it should work. See questions in https://github.com/facebookincubator/create-react-app/issues/1333#issuecomment-356800381 -- any feedback you could provide in that thread would be awesome and helpful in building consensus as to how it should work.

gaearon commented 6 years ago

@bradfordlemley Thanks a lot for jumping on that issue btw. I'm sorry repo infra changes are being a bit disruptive right now; hopefully we can review them soon!

gaearon commented 6 years ago

We also merged Jest 22 into next branch so you might want to use that as a base.

subhero24 commented 6 years ago

@bradfordlemley Thanks for clarifying. I am not knowledgeable enough on this topic, but I don't understand where the technical difficulties are in handling these symlinks. (I can imagine processing the dependencies in node_modules is a whole different matter, as not all dependencies should be handled the same way). Why isn't just resolving these symlinks a simple solution to this problem?

bradfordlemley commented 6 years ago

I don't think it's really a technical issue (although the comments in https://github.com/facebookincubator/create-react-app/pull/3695 maybe show that it's not as simple as it seems), maybe more about encouraging maintainable practices.

If you symlink from under /src, you have to import via relative path (unless you do some other magic with NODE_MODULES) and then maintain various symlinks if you change locations, and then there's defining, installing, and resolving dependencies for your external source. These are things that can make your build fragile, but monorepo managers seem to do those things well.

There is a bit of a learning curve to the monorepo managers, but maybe worth it in the long run, seems like the direction many folks are headed (???), btw, they also help with de-duplicating dependencies.

bradfordlemley commented 6 years ago

All that said, it is really easy to just create a symlink from under /src and seems like a reasonable expectation that it should be treated as if it was in /src, so hope others will chime in on this discussion.

1st commented 6 years ago

I also interesting in building two separate ReactJS projects. Both of them are using the same shared library or UI Components. I created a symlink in both projects to this shared UI Components directory.

When I'm trying to import something from that UI Components directory - it tells the same error message as in the original message:

Module parse failed: Unexpected token (20:18)
You may need an appropriate loader to handle this file type.

It happening because code isn't viable by a watcher from create-react-app that is watching for changes in one of my my projects.

Can someone give me a hint how to solve this problem for now? Because I potentially can copy he entire directory to both projects, and write some watcher that will do it for me on the regular basis. But it looks ugly solution. I wish to use the same shared directory between two projects and include "raw" files that then will be compiled.

Need your help.

Thank you, Anton

1st commented 6 years ago

BTW, here https://github.com/webpack/webpack/issues/1643 I found some information about webpack and symlinks. It looks like webpack can find our files but doesn't run a compilation process for them.

bradfordlemley commented 6 years ago

@1st Have you looked at monorepo support in 2.0? See 2.0 roadmap for updates and on how to use the alpha builds.

Sharing code that way has several advantages over manual symlinks, but mainly allows your shared code to be truly modularized with its own dependencies, etc. Manual symlinks like you're requesting aren't supported in 1.0 or 2.0 yet -- if you find manual symlinks preferable, it'd be great to give the reasons here.

1st commented 6 years ago

Finally I found a workaround. You can run a watch process to compile your shared library and use it as symlink in your project. Here is my gist.

vladp commented 6 years ago

was suggested by @bradfordlemley to note my temporary (modification to cra scripts required). I also opened a bug on the same topic, very significant issue.. in my mind.

Here is the explanation of the temporary workaround: To reiterate, I have symlink subdirectory under ./src. The symlink name is: js.app.

The webpack configuration of create-react-app is in node_modules/react-script/config/webpack.config.dev.js I modified it.

// Process JS with Babel.
          {
            test: /\.(js|jsx|mjs)$/,
            include: paths.appSrc,
            loader: require.resolve('babel-loader'),

the use of include: paths.appSrc is what's preventing me using the symlinked directory underneath my ./src

I changed the above to

const fs = require('fs'); //include somewhere on top of the webpack.config.dev.js

...

 {
            test: /\.(js|jsx|mjs)$/,
            include: [paths.appSrc,
                      fs.realpathSync(paths.appSrc+'/js.app')],

And this works (obviously temporarily, till next module upgrade...).. but I am a single person shop, and just needed to move past this... Sorry I do not have any better & cleaner suggestion, but hope this can help others

Clearly, this is a bug, so I am keeping the issue open with the hope that it will get addressed (or at least users of the create-react-app will be able to override behavior for this).

sheepsy90 commented 6 years ago

For some reason the realpathSync destroys the possibility for me to use refs in my project.

vladp commented 6 years ago

@sheepsy90 you mean refs as, say, string refs or the new rews https://reactjs.org/docs/refs-and-the-dom.html ? Or did you mean something else? (also I made the above hack work through rewire, so that I do not have override the node_modules/react-scripts/config... Hope there is a CRA solution for symlinked directories under ./src though.

thaddeus-k commented 6 years ago

Adding that I just ran into this while trying to get hold of the version number out of package.json for use in display. Hoping to find a workaround soon.

vladp commented 6 years ago

@thaddeus-k , In the mean time (until CRA allows symlinked directories underneath src), I use rewire react app to make get the symlink paths into babel-loader rules.

Another problem I ran into, is that I needed to transply a node module (react native vector icons).

And, finally, I also needed to provide my own certificate files for starting webpack webbrowser in HTTPS. Rewire let me do that as well... I closed those 3 issues with the below.

my config-overrides.js for the rewire is below:

// https://github.com/timarney/react-app-rewired#extended-configuration-options

const path = require('path');
const resolve=path.resolve;
const fs=require('fs'); 
const paths= require ('react-scripts/config/paths');

const wpmerge= require('webpack-merge');
var treeify = require('treeify');

module.exports = {

  webpack: function(baseConfig, env) {

    const config = Object.assign({}, baseConfig);

    config.resolve.alias.web_common = path.resolve('./src/wc.src');
    config.resolve.alias.app_src = path.resolve('./src/app.src');
    config.resolve.alias.rnjs_common = path.resolve('./src/js.app');

    /* get rid of facebooks plugin that causes error if something
       outside of the src folder...
       I am not sure actually why it is causing
       an error.... if my style sheets within src, but symlinked folder.
       In other words, the plugin that forces all to be within src does not
       appear to understand symlinks either.

       TODO: detect explicitly that plugin (rather than removing all of the
       resolve plugins...)

     */
    config.resolve.plugins=[];

    //find existing js formater rule
    let jsRulesJSFormaterIdx = -1;
    jsRulesJSFormaterIdx=config.module.rules.findIndex(
      (rule) =>{
        if (rule.test // rule.test is a reg expr 
            && rule.test.exec
            && rule.test.exec('./something.js')) {
          return true;
        }
      }
    );

    //find index of existing webpack js rule,
    //we need it to add our include path to babel

    /*
       in CRA's webmodule config, the babel loader is hidden
       under 2nd (idx=1) rule within 'oneOf' property.
       I can find the rule with oneOf property, of course, but will
       hardcode for now, and then dynamically will find the js (babel)
       rule within the oneOf property 
     */
    let oneOfIdx=1;
    let jsRulesBabelLoaderIdx = -1;
    jsRulesBabelLoaderIdx=config.module.rules[oneOfIdx].oneOf.findIndex(
      (rule) =>{
        if (rule.test // rule.test is a reg expr 
            && rule.test.exec
            && rule.test.exec('./something.js')) {
          return true;
        }
      }
    );

    const pathsToMySourcesArr=
      [paths.appSrc,
       fs.realpathSync(paths.appSrc+'/js.app'),
       fs.realpathSync(paths.appSrc+'/app.src'),
       fs.realpathSync(paths.appSrc+'/wc.src'),
       fs.realpathSync(paths.appNodeModules+'/react-native-vector-icons')
      ];

    console.log(paths);

    let addtlIncludeConfig={
      include:pathsToMySourcesArr
    };

    config.module
          .rules[oneOfIdx].oneOf[jsRulesBabelLoaderIdx]=
            wpmerge( config.module.rules[oneOfIdx].oneOf[jsRulesBabelLoaderIdx],
                     addtlIncludeConfig);

    config.module
          .rules[jsRulesJSFormaterIdx]
          .include=pathsToMySourcesArr;

    return config;
  },

  jest: function(config) {

    if (!config.testPathIgnorePatterns) {
      config.testPathIgnorePatterns = [];
    }
    if (!process.env.RUN_COMPONENT_TESTS) {
      config.testPathIgnorePatterns.push('<rootDir>/src/components/**/*.test.js');
    }
    if (!process.env.RUN_REDUCER_TESTS) {
      config.testPathIgnorePatterns.push('<rootDir>/src/reducers/**/*.test.js');
    }
    return config;
  },

  devServer: function(configFunction) {
    return function(proxy, allowedHost) {

      const config = configFunction(proxy, allowedHost);

      config.https = {
        cert: ""
        key:  ""
        passphrase:""

      };

      // Return your customised Webpack Development Server config.
      return config;
    }
  }
}
jannikbuschke commented 5 years ago

I also felt the need of sharing code between projects while developing both. I tried npm link and creating symlinks by myself. Both had gotchas I couldn't really live with.

What worked out in the end for me is using npms local "file" project references (npm install ..\..\shared-project) and running a watcher there (tsc --watch in my case, as it is a typescript project).

My shared-project is based on create-react-app (the typescript version) with some modifications, as it is not setup for a library project.

OKNoah commented 5 years ago

Just sharing a solution I worked out for including a folder called common in a monorepo-type setup.

This uses react-app-rewired along with customize-cra. There's some extras here, like decorators, but here you go. This goes in the config-overrides.js.

const {
  override,
  addDecoratorsLegacy,
  addBabelPlugin,
  babelInclude
} = require("customize-cra")

const path = require("path")

module.exports = (config, ...rest) => {
  /* Simply clones the object */
  const overriddenConfig = Object.assign(config, {})

  /* Remove the last item from the resolve plugins array. This should be ModuleScopePlugin */
  overriddenConfig.resolve.plugins.pop()

  return Object.assign(overriddenConfig, override(
      /* Makes sure Babel compiles the stuff in the common folder */
      babelInclude([
        path.resolve('src'), // don't forget this
        path.resolve(__dirname, '../common')
      ]),
      addDecoratorsLegacy(),
      addBabelPlugin(['module-resolver', {
        root: '../packages',
        alias: {
          common: '../common'
        }
      }])
    )(overriddenConfig, ...rest)
  )
}

No symlinks required.

jiayihu commented 5 years ago

@OKNoah solution worked for me, here is the minimal version, without aliases:

const { override, babelInclude } = require('customize-cra');
const path = require('path');

module.exports = override(
  babelInclude([path.resolve('src'), path.resolve(__dirname, '../uikit')])
);

Here the version with aliases, I used webpack alias instead of babel-plugin-module-resolver to avoid installing another dependency and besides it's more flexible because supports also CSS imports:

const { override, babelInclude, addWebpackAlias, removeModuleScopePlugin } = require('customize-cra');
const path = require('path');

module.exports = override(
  removeModuleScopePlugin(),
  babelInclude([path.resolve('src'), path.resolve(__dirname, '../uikit')]),
  addWebpackAlias({
    '@my-proj/uikit': path.resolve(__dirname, '../uikit/src/'),
  }),
);
SampsonCrowley commented 5 years ago

just incase it helps someone, I took my own approach using cpx to synchronize files: https://github.com/SampsonCrowley/mega-repo-template

iamnader commented 5 years ago

@jiayihu & @OKNoah your solutions have got me on the right path, but I'm still seeing one issue. I have a common directory as a peer to my CRA project. Using removeModuleScopePlugin and babelInclude I'm able to load files from the common project, but if that file includes a module from the main project's node_modules, it doesn't resolve. Any idea on how to get that resolution to work? Thanks

jiayihu commented 5 years ago

Do you mean that a file in the shared package imports a file in the main project? In that case you should avoid it, otherwise you'll also have a circular dependency. Besides shared packages should be kept independent from main projects

iamnader commented 5 years ago

@jiayihu let me give a little more detail. I have a monorepo with multiple CRA projects. There is also a common directory with shared components. The common directory is just a directory, not a package. I didn't want to make it a separate package as it complicates versioning, testing, dev process, etc. A component in the common directory is not importing source files from the projects, but it is trying to import project dependencies that are included in all the projects that rely on the common file. Does that make sense?

jiayihu commented 5 years ago

By avoiding to have a package you already complicating the things unfortunately. Due to how module resolution works in Node, once you import a file in the shared directory it will look for dependencies in the latter node_modules. In my case I also have a monorepo but the shared project is a package, which allows me to declare and install its dependencies. I then install the projects using lerna bootstrap --hoist to have all the dependencies at the root of the monorepo, which will allow all the packages to import react for instance.

iamnader commented 5 years ago

Thanks @jiayihu . This pattern works great for other projects, including Expo projects importing shared components. It's just CRA apps that I can't get configured correctly...

jannikbuschke commented 5 years ago

@iamnader CRA totally works with lerna (http://jannikbuschke.de/blog/monorepo-with-lerna-react-and-typescript/ if you are interested in a walkthrough with cra and a typescript/react module)

liuhx1027 commented 4 years ago

The issue comes from webpack which uses real path of the symlinked files. see https://webpack.js.org/configuration/resolve/#resolvesymlinks for details. to solve it, simplely set the resolve.symlinks = false. the following is the code I use to make it work. (be aware this is a config-overrides.js file which is from react-app-rewired)

module.exports = (config, ...rest) => {
  return { ...config, resolve: { ...config.resolve, symlinks: false } };
};
ali-aka-ahmed commented 4 years ago

All that said, it is really easy to just create a symlink from under /src and seems like a reasonable expectation that it should be treated as if it was in /src, so hope others will chime in on this discussion.

It seems I've found an edge case? I think this is where to put it and hopefully it'll inform the PR but let me know if I'm spamming and I'll just create a bug ticket 🙂

Having an issue with enums when symlinking a common enums and interfaces directory (named common, symlinked within src/ folder). For some reason, importing interfaces doesn't trigger an error, but importing enums does! And they're NOT const enums.

ERROR

Screen Shot 2020-03-02 at 6 40 00 PM

DIRECTORY STRUCTURE

Screen Shot 2020-03-02 at 8 29 03 PM

Notice, using a monorepo with typescript, create-react-app for the frontend. I can import interfaces from the symlinked common folder with no errors, only when I import the enums do I have a problem.

Here's the code for the ../common/enums/Subject.ts file:

enum Subject {
  Biology = 'BIOLOGY',
  Chemistry = 'CHEMISTRY',
  Earth = 'EARTH',
  Energy = 'ENERGY',
  General = 'GENERAL',
  Math = 'MATH',
  Physics = 'PHYSICS',
  Space = 'SPACE'
}

export default Subject;

And for good measure (maybe it's my tsconfig.json??) Here's my tsconfig.json for the frontend:

{
  "compilerOptions": {
    "lib": [
      "dom",
      "dom.iterable",
      "es6"
    ],
    "target": "es6",
    "baseUrl": "src",
    "jsx": "react",
    "module": "esnext",
    "moduleResolution": "node",
    "sourceMap": true,
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "strict": true,
  },
  "include": [
    "src",
    "src/**/*.ts",
    "src/**/*.test.ts",
    "src/**/*.test.tsx"
  ]
}

I do have a tsconfig.json for the common folder, but not sure that woud be causing the issue. here's the structure of the common folder and tsconfig.json file for it.

Screen Shot 2020-03-02 at 8 34 31 PM
ali-aka-ahmed commented 4 years ago

Update^: what's weird is that I can create those enums in separate files under src/, import them, and there is no problem. It's only exporting enums from within the symlinked folder that throws an error. Why would that be?

vicary commented 4 years ago

Using icloud-nosync or nosync-icloud creates a symlink to the node_modules directory itself, but that totally breaks the directory restriction.

Failed to compile.

./src/index.css
Module not found: You attempted to import ../node_modules.nosync/css-loader/lib/css-base.js which falls outside of the project src/ directory. Relative imports outside of src/ are not supported. You can either move it inside src/, or add a symlink to it from project's node_modules/.
anton-g commented 4 years ago

I have a similar issue as @ali-wetrill except I haven't set up a symlink but rather uses the project reference feature in typescript. So in my clients tsconfig I've added this:

"references": [{ "path": "../common" }]

Interfaces and types work fine, but enums break with this error:

Module not found: You attempted to import ../../../../common/src/models/message-type which falls outside of the project src/ directory. Relative imports outside of src/ are not supported.

mako-taco commented 4 years ago

It's not a problem with enums. Everything else you've tried to export is valid javascript AND valid typescript. Try writing a function with a type signature; I expect you'll see the same issues.

Forcing webpackConfig.resolve.symlink = false solves the issue.

rmarfil3 commented 4 years ago

@mako-taco I have a similar problem as @ali-wetrill. I added webpackConfig.resolve.symlink = false like you said using customize-cra and it no longer resolves to the actual path. However, it's still showing the same error about "keyword enum is reserved, may need an appropriate loader".

vicary commented 4 years ago

It seems customize-cra doesn't help symlink paths by postcss-loader et al.

PeledYuval commented 4 years ago

Here's the Solution we've arrived to at Vim (no, not the text editor :) ) for working with CRA + symlinked components in a monorepo:

  1. Install craco (allows for config overrides in create-react-app. This might seem like exactly the opposite of what CRA wants to be - and it is - but sometimes CRA is too-opinionated. Between having CRA and minimal craco config and having to manage a complete webpack+typescript+jest+babel stack, I'd choose the former)
  2. Create a craco.config.js file in your project root. Use the code below as a starting point for it
  3. In your package.json, change your start, build, test scripts to craco start, craco build, craco test

Portions of our craco.config.js:

const cracoLessPlugin = require('craco-less');
const path = require('path');
const { whenProd } = require('@craco/craco');

/* Allows importing code from other packages in a monorepo. Explanation:
When you use lerna / yarn workspaces to import a package, you create a symlink in node_modules to
that package's location. By default Webpack resolves those symlinks to the package's actual path,
which makes some create-react-app plugins and compilers fail (in prod builds) because you're only
allowed to import things from ./src or from node_modules
 */
const disableSymlinkResolution = {
  plugin: {
    overrideWebpackConfig: ({ webpackConfig }) => {
      webpackConfig.resolve.symlinks = false;
      return webpackConfig;
    },
  },
};

const webpackSingleModulesResolution = {
  alias: {
    react$: path.resolve(__dirname, 'node_modules/react'),
    'react-dom$': path.resolve(__dirname, 'node_modules/react-dom'),
    'react-router-dom$': path.resolve(__dirname, 'node_modules/react-router-dom'),
  },
};

const jestSingleModuleResolution = {
  moduleNameMapper: {
    '^react$': '<rootDir>/node_modules/react',
    '^react-dom$': '<rootDir>/node_modules/react-dom',
    '^react-router-dom$': '<rootDir>/node_modules/react-router-dom',
  },
};

module.exports = {
  plugins: [...whenProd(() => [disableSymlinkResolution], [])],
  webpack: webpackSingleModulesResolution,
  jest: {
    configure: {
      jestSingleModuleResolution,
    },
  },
};

Sadly this does require manual management of each broken import. Over a few months we haven't seen too many of these, so this is fine for now.

Please excuse syntax errors if any, this is edited from our real configuration for brevity.

harryjubb commented 3 years ago

Had this issue with symlinking a TS file, where non-TS syntax works, but on adding any TS syntax, get the "Unexpected token" error.

In my case so far, adding CRACO with their instructions, with the following simple craco.config.js, has worked:

module.exports = {
  webpack: {
    configure: (webpackConfig, { env, paths }) => ({
      ...webpackConfig,
      resolve: {
        ...webpackConfig.resolve,
        symlinks: false
      }
    })
  }
}

Presumably when https://github.com/facebook/create-react-app/pull/7993 is merged this fix will no longer be needed.

nitinkatyal1314 commented 3 years ago

With {symlinks: false} the hot reloading does not work. Changing the src code for which link was created is not hot reloaded in the browser. Is there a solution to this?

dudulasry commented 3 years ago

@nitinkatyal1314 The hot reload functionality doesn't work for me either, when disabling the symlinks. Does anyone know how to solve it?

romgrk-comparative commented 2 years ago

For googlers ending up here, there is a (better) alternative to { symlinks: false }. This is especially useful if you're using pnpm (which you should) because pnpm symlinks all modules. The alternative is to modify the webpack config to add the other directories to the include list of the loader that should process the file, which is the issue that symlinks: false work-arounds without really solving. Below is an example using CRACO, but a similar modification will work for anything else that let's you modifiy the webpack config.

/* craco.config.js */
const path = require('path')

const updateWebpackConfig = {
  overrideWebpackConfig: ({ webpackConfig }) => {

    // This is a bit brittle, but this retrieves the `babel-loader` for me.
    const loader = webpackConfig.module.rules[1].oneOf[2]
    loader.include = [
      path.join(__dirname, 'src'),
      path.join(__dirname, '../backend/src'), // This is the directory containing the symlinked-to files
    ]

    return webpackConfig;
  }
}

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

/cc @romgrk

benvium commented 2 years ago

@romgrk-comparative This really helped me :-). Thanks. I improved the 'bit brittle' part by using built-in utilities from craco to grab babel-loader without 'magic numbers' in the code.

const path = require('path');
const {getLoader, loaderByName} = require('@craco/craco');

// Relative paths to shared folders.
// IMPORTANT: If you want to directly import these (no symlink) these folders must also be added to tsconfig.json {"compilerOptions": "rootDirs": [..]}
const SRC_LOCATIONS = [
 'src',
  '../../shared/src',
];

const updateWebpackConfig = {
  overrideWebpackConfig: ({webpackConfig}) => {

    // Get hold of the babel-loader, so we can add shared folders to it, ensuring that they get compiled too
    const {match:{loader}} = getLoader(webpackConfig, loaderByName("babel-loader"));

    loader.include = SRC_LOCATIONS.map(p => path.join(__dirname, p)),

    return webpackConfig;
  }
}

module.exports = {
  plugins: [
    { plugin: updateWebpackConfig, options: {} }
  ]
}
RandScullard commented 2 years ago

The solution from @romgrk-comparative and @benvium is working great for me now, but I had a lot of trouble getting started because of a quirk in the Windows mklink command. If you're on Windows, read on...

When you're creating a symlink, the mklink command remembers the case of the destination folder as you typed it. I had a folder named C:\RDS\ATWebsites\ATClientShared\src and I typed:

mklink /J shared \rds\ATWebsites\ATClientShared\src

Note that I didn't bother upper-casing the RDS part of the path. (Why would I, when Windows pathnames are case-insensitive... right?)

The resulting symlink remembered the lower-case rds and since the actual source file pathnames start with upper-case RDS, none of them matched the loader.include and I got the "unexpected token" error from webpack. 😭

I'll never get back the hours I wasted figuring this out, but maybe I can save someone else the trouble.

TeemuKoivisto commented 2 years ago

I don't really understand what's going on with this symlinking soup. All I know my CRA app crashes due to library being imported twice in my pnpm monorepo. The fixes shown above didn't really work but using aliases did:

const path = require('path')

const updateWebpackConfig = {
  overrideWebpackConfig: ({ webpackConfig }) => {
    // As described in https://github.com/facebook/create-react-app/issues/3547
    // there are some issues with how CRA treats symlinks which pnpm heavily uses.
    // This hack should fix this issue (loading prosemirror-model twice) for now
    webpackConfig.resolve.alias = {
      'prosemirror-model$': path.resolve(__dirname, '../editor/node_modules/prosemirror-model'),
    }
    return webpackConfig;
  }
}

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

Basically I'm aliasing the library to the main module that uses it. Not really scalable but I hope I'll be able to contain it to only a few broken modules.

RandScullard commented 2 years ago

I'm using another method to solve the duplicate package problem. Replace @TeemuKoivisto's webpackConfig.resolve.alias line with:

webpackConfig.resolve.modules = webpackConfig.resolve.modules.filter(mod => mod !== "node_modules")

Explanation: CRA configures this modules array to include two entries: node_modules and the full path of ClientApp/node_modules. The line of code above removes node_modules. Now webpack will only resolve modules from your main ClientApp/node_modules folder tree and not from any of your "sibling" shared code projects. This means no duplicated modules, and no need to add modules one by one to your craco config.

Note: You will need to npm install in your main ClientApp the union of all packages you've installed in your shared code projects. (In @TeemuKoivisto's example above, prosemirror-model needs to be installed in the main app even if it is only used in ../editor.) In my project this is just a few packages - YMMV. You can try the config line above and run webpack to immediately see what you need to install.

Note 2: It's important to prevent all module duplication even if it doesn't cause a runtime error, because it can significantly increase the size of your webpack bundle. (You can use source-map-explorer to do a before-and-after comparison.)