From 9e074bbf08a8760579e7345fe1391a76f9c41f96 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ma=C3=ABl=20Nison?= <nison.mael@gmail.com>
Date: Mon, 1 Oct 2018 15:53:07 +0100
Subject: [PATCH] Plug'n'Play support (#5136)

* Adds the PnP plugin for Webpack to find dependencies when working under PnP

* Adds configuration for jest

* Adds an e2e test for when using PnP

* Avoids cra from crashing at the engine check

* Avoids cra from crashing when initializing react-scripts

* Makes the ownPath portable

* Fixes linting

* Bumps to pnp-webpack-plugin@1.1.0, removes symlinks: false

* Adds a --use-pnp option

* Pin version
---
 .travis.yml                                   |  2 +-
 appveyor.yml                                  |  2 +-
 packages/create-react-app/createReactApp.js   | 89 +++++++++++++++----
 .../config/webpack.config.dev.js              | 11 +++
 .../config/webpack.config.prod.js             | 11 +++
 packages/react-scripts/package.json           |  3 +
 packages/react-scripts/scripts/init.js        |  6 +-
 .../scripts/utils/createJestConfig.js         |  3 +-
 tasks/e2e-installs.sh                         | 11 +++
 9 files changed, 116 insertions(+), 22 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index e104f22e1..d5fdc1564 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,7 +9,7 @@ cache:
   directories:
     - .npm
 before_install:
-  - curl -o- -L https://yarnpkg.com/install.sh | bash
+  - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --nightly
   - export PATH="$HOME/.yarn/bin:$PATH"
 install: true
 script:
diff --git a/appveyor.yml b/appveyor.yml
index ab266ab97..5f5143167 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -37,7 +37,7 @@ platform:
 install:
   - ps: Install-Product node $env:nodejs_version $env:platform
   - ps: |
-      (New-Object Net.WebClient).DownloadFile("https://yarnpkg.com/latest.msi", "$env:temp\yarn.msi")
+      (New-Object Net.WebClient).DownloadFile("https://nightly.yarnpkg.com/latest.msi", "$env:temp\yarn.msi")
       cmd /c start /wait msiexec.exe /i $env:temp\yarn.msi /quiet /qn /norestart
 
 build: off
diff --git a/packages/create-react-app/createReactApp.js b/packages/create-react-app/createReactApp.js
index 015417a2c..132d55a83 100755
--- a/packages/create-react-app/createReactApp.js
+++ b/packages/create-react-app/createReactApp.js
@@ -76,6 +76,7 @@ const program = new commander.Command(packageJson.name)
     'use a non-standard version of react-scripts'
   )
   .option('--use-npm')
+  .option('--use-pnp')
   .allowUnknownOption()
   .on('--help', () => {
     console.log(`    Only ${chalk.green('<project-directory>')} is required.`);
@@ -178,10 +179,11 @@ createApp(
   program.verbose,
   program.scriptsVersion,
   program.useNpm,
+  program.usePnp,
   hiddenProgram.internalTestingTemplate
 );
 
-function createApp(name, verbose, version, useNpm, template) {
+function createApp(name, verbose, version, useNpm, usePnp, template) {
   const root = path.resolve(name);
   const appName = path.basename(root);
 
@@ -241,7 +243,16 @@ function createApp(name, verbose, version, useNpm, template) {
       version = 'react-scripts@0.9.x';
     }
   }
-  run(root, appName, version, verbose, originalDirectory, template, useYarn);
+  run(
+    root,
+    appName,
+    version,
+    verbose,
+    originalDirectory,
+    template,
+    useYarn,
+    usePnp
+  );
 }
 
 function shouldUseYarn() {
@@ -253,7 +264,7 @@ function shouldUseYarn() {
   }
 }
 
-function install(root, useYarn, dependencies, verbose, isOnline) {
+function install(root, useYarn, usePnp, dependencies, verbose, isOnline) {
   return new Promise((resolve, reject) => {
     let command;
     let args;
@@ -263,6 +274,9 @@ function install(root, useYarn, dependencies, verbose, isOnline) {
       if (!isOnline) {
         args.push('--offline');
       }
+      if (usePnp) {
+        args.push('--enable-pnp');
+      }
       [].push.apply(args, dependencies);
 
       // Explicitly set cwd() to work around issues like
@@ -287,6 +301,12 @@ function install(root, useYarn, dependencies, verbose, isOnline) {
         '--loglevel',
         'error',
       ].concat(dependencies);
+
+      if (usePnp) {
+        console.log(chalk.yellow("NPM doesn't support PnP."));
+        console.log(chalk.yellow('Falling back to the regular installs.'));
+        console.log();
+      }
     }
 
     if (verbose) {
@@ -313,7 +333,8 @@ function run(
   verbose,
   originalDirectory,
   template,
-  useYarn
+  useYarn,
+  usePnp
 ) {
   const packageToInstall = getInstallPackage(version, originalDirectory);
   const allDependencies = ['react', 'react-dom', packageToInstall];
@@ -336,23 +357,34 @@ function run(
       );
       console.log();
 
-      return install(root, useYarn, allDependencies, verbose, isOnline).then(
-        () => packageName
-      );
+      return install(
+        root,
+        useYarn,
+        usePnp,
+        allDependencies,
+        verbose,
+        isOnline
+      ).then(() => packageName);
     })
-    .then(packageName => {
+    .then(async packageName => {
       checkNodeVersion(packageName);
       setCaretRangeForRuntimeDeps(packageName);
 
-      const scriptsPath = path.resolve(
-        process.cwd(),
-        'node_modules',
-        packageName,
-        'scripts',
-        'init.js'
+      const pnpPath = path.resolve(process.cwd(), '.pnp.js');
+
+      const nodeArgs = fs.existsSync(pnpPath) ? ['--require', pnpPath] : [];
+
+      await executeNodeScript(
+        {
+          cwd: process.cwd(),
+          args: nodeArgs,
+        },
+        [root, appName, verbose, originalDirectory, template],
+        `
+        var init = require('${packageName}/scripts/init.js');
+        init.apply(null, JSON.parse(process.argv[1]));
+      `
       );
-      const init = require(scriptsPath);
-      init(root, appName, verbose, originalDirectory, template);
 
       if (version === 'react-scripts@0.9.x') {
         console.log(
@@ -540,6 +572,11 @@ function checkNodeVersion(packageName) {
     packageName,
     'package.json'
   );
+
+  if (!fs.existsSync(packageJsonPath)) {
+    return;
+  }
+
   const packageJson = require(packageJsonPath);
   if (!packageJson.engines || !packageJson.engines.node) {
     return;
@@ -794,3 +831,23 @@ function checkIfOnline(useYarn) {
     });
   });
 }
+
+function executeNodeScript({ cwd, args }, data, source) {
+  return new Promise((resolve, reject) => {
+    const child = spawn(
+      process.execPath,
+      [...args, '-e', source, '--', JSON.stringify(data)],
+      { cwd, stdio: 'inherit' }
+    );
+
+    child.on('close', code => {
+      if (code !== 0) {
+        reject({
+          command: `node ${args.join(' ')}`,
+        });
+        return;
+      }
+      resolve();
+    });
+  });
+}
diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js
index 819ca0e95..fe628e912 100644
--- a/packages/react-scripts/config/webpack.config.dev.js
+++ b/packages/react-scripts/config/webpack.config.dev.js
@@ -10,6 +10,7 @@
 
 const path = require('path');
 const webpack = require('webpack');
+const PnpWebpackPlugin = require('pnp-webpack-plugin');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
 const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
 const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
@@ -150,6 +151,9 @@ module.exports = {
       'react-native': 'react-native-web',
     },
     plugins: [
+      // Adds support for installing with Plug'n'Play, leading to faster installs and adding
+      // guards against forgotten dependencies and such.
+      PnpWebpackPlugin,
       // Prevents users from importing files from outside of src/ (or node_modules/).
       // This often causes confusion because we only process files within src/ with babel.
       // To fix this, we prevent you from importing files out of src/ -- if you'd like to,
@@ -158,6 +162,13 @@ module.exports = {
       new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
     ],
   },
+  resolveLoader: {
+    plugins: [
+      // Also related to Plug'n'Play, but this time it tells Webpack to load its loaders
+      // from the current package.
+      PnpWebpackPlugin.moduleLoader(module),
+    ],
+  },
   module: {
     strictExportPresence: true,
     rules: [
diff --git a/packages/react-scripts/config/webpack.config.prod.js b/packages/react-scripts/config/webpack.config.prod.js
index d71f2979c..b1db45d77 100644
--- a/packages/react-scripts/config/webpack.config.prod.js
+++ b/packages/react-scripts/config/webpack.config.prod.js
@@ -10,6 +10,7 @@
 
 const path = require('path');
 const webpack = require('webpack');
+const PnpWebpackPlugin = require('pnp-webpack-plugin');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
 const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
 const TerserPlugin = require('terser-webpack-plugin');
@@ -214,6 +215,9 @@ module.exports = {
       'react-native': 'react-native-web',
     },
     plugins: [
+      // Adds support for installing with Plug'n'Play, leading to faster installs and adding
+      // guards against forgotten dependencies and such.
+      PnpWebpackPlugin,
       // Prevents users from importing files from outside of src/ (or node_modules/).
       // This often causes confusion because we only process files within src/ with babel.
       // To fix this, we prevent you from importing files out of src/ -- if you'd like to,
@@ -222,6 +226,13 @@ module.exports = {
       new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
     ],
   },
+  resolveLoader: {
+    plugins: [
+      // Also related to Plug'n'Play, but this time it tells Webpack to load its loaders
+      // from the current package.
+      PnpWebpackPlugin.moduleLoader(module),
+    ],
+  },
   module: {
     strictExportPresence: true,
     rules: [
diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json
index 7170e4ff9..659eb02ec 100644
--- a/packages/react-scripts/package.json
+++ b/packages/react-scripts/package.json
@@ -47,8 +47,11 @@
     "html-webpack-plugin": "4.0.0-alpha.2",
     "identity-obj-proxy": "3.0.0",
     "jest": "23.6.0",
+    "jest-pnp-resolver": "1.0.1",
+    "jest-resolve": "23.6.0",
     "mini-css-extract-plugin": "0.4.3",
     "optimize-css-assets-webpack-plugin": "5.0.1",
+    "pnp-webpack-plugin": "1.1.0",
     "postcss-flexbugs-fixes": "4.1.0",
     "postcss-loader": "3.0.0",
     "postcss-preset-env": "6.0.6",
diff --git a/packages/react-scripts/scripts/init.js b/packages/react-scripts/scripts/init.js
index 2904ae70c..8ec249675 100644
--- a/packages/react-scripts/scripts/init.js
+++ b/packages/react-scripts/scripts/init.js
@@ -81,9 +81,9 @@ module.exports = function(
   originalDirectory,
   template
 ) {
-  const ownPackageName = require(path.join(__dirname, '..', 'package.json'))
-    .name;
-  const ownPath = path.join(appPath, 'node_modules', ownPackageName);
+  const ownPath = path.dirname(
+    require.resolve(path.join(__dirname, '..', 'package.json'))
+  );
   const appPackage = require(path.join(appPath, 'package.json'));
   const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock'));
 
diff --git a/packages/react-scripts/scripts/utils/createJestConfig.js b/packages/react-scripts/scripts/utils/createJestConfig.js
index 22e52dbdc..af085383f 100644
--- a/packages/react-scripts/scripts/utils/createJestConfig.js
+++ b/packages/react-scripts/scripts/utils/createJestConfig.js
@@ -22,7 +22,8 @@ module.exports = (resolve, rootDir, isEjecting) => {
   // in Jest configs. We need help from somebody with Windows to determine this.
   const config = {
     collectCoverageFrom: ['src/**/*.{js,jsx}'],
-    setupFiles: ['react-app-polyfill/jsdom'],
+    resolver: require.resolve('jest-pnp-resolver'),
+    setupFiles: [require.resolve('react-app-polyfill/jsdom')],
     setupTestFrameworkScriptFile: setupTestsFile,
     testMatch: [
       '<rootDir>/src/**/__tests__/**/*.{js,jsx}',
diff --git a/tasks/e2e-installs.sh b/tasks/e2e-installs.sh
index 642e4f671..e402ee5c8 100755
--- a/tasks/e2e-installs.sh
+++ b/tasks/e2e-installs.sh
@@ -229,5 +229,16 @@ npx create-react-app test-app-nested-paths-t3/aa/bb/cc/dd
 cd test-app-nested-paths-t3/aa/bb/cc/dd
 yarn start --smoke-test
 
+# ******************************************************************************
+# Test when PnP is enabled
+# ******************************************************************************
+cd "$temp_app_path"
+npx create-react-app test-app-pnp --use-pnp
+cd test-app-pnp
+! exists node_modules
+exists .pnp.js
+yarn start --smoke-test
+yarn build
+
 # Cleanup
 cleanup
-- 
GitLab