Repository: facebook/react
Pull request: Fixed broken build script --unsafe-partial flag

Resolves #22323

The following example command sequence will fail:
```sh
# Grab the latest build artifacts from CI
scripts/release/download-experimental-build.js --commit=main

# Rebuild only the local NODE bundle from the source of react-dom
# Leave all other artifacts untouched
yarn build --unsafe-partial --type=NODE_DEV react-dom/index

# Throws
```
The above sequence _should_ work but instead fails with one of the two errors below:
> Error: ENOENT: no such file or directory

> Error: SyntaxError: Unexpected end of JSON input

This leaves the repo in a broken state (tests won't run, DevTools test shell will crash, etc).

These errors are thrown because this function prematurely resolves:
https://github.com/facebook/react/blob/67222f044c582a1a10f3b9d15d9daf67d5dc5c4f/scripts/rollup/utils.js#L10-L24

Either before the `package.json` file has been copied, or after it has been copied (but while its contents are still empty).

This causes the subsequent read of `package.json` to throw:
https://github.com/facebook/react/blob/67222f044c582a1a10f3b9d15d9daf67d5dc5c4f/scripts/rollup/packaging.js#L145-L148

The error above can be verified by adding logging to the `asyncTopyTo` method:
```js
function asyncCopyTo(from, to) {
  return asyncMkDirP(path.dirname(to)).then(
    () =>
      new Promise((resolve, reject) => {
        ncp(from, to, error => {
          if (error) {
            // Wrap to have a useful stack trace.
            reject(new Error(error));
            return;
          }
          if (to.includes('package.json')){
            console.log(`asyncCopyTo() -> ncp("${from}", "${to}") -> resolve`);

            // This line will either throw (no file) or print an empty string in many cases:
            console.log(require('fs').readFileSync(to).toString());
          }
          resolve();
        });
      })
  );
}
```

I'm able to "fix" this issue by introducing a small amount of delay, e.g.
```diff
diff --git a/scripts/rollup/packaging.js b/scripts/rollup/packaging.js
index eaf78959a6..5844d9116f 100644
--- a/scripts/rollup/packaging.js
+++ b/scripts/rollup/packaging.js
@@ -196,6 +196,10 @@ async function prepareNpmPackage(name) {
     ),
     asyncCopyTo(`packages/${name}/npm`, `build/node_modules/${name}`),
   ]);
+
+  // Wait for copied files to exist; asyncCopyTo() completes prematurely.
+  await new Promise(resolve => setTimeout(resolve, 100));
+
   filterOutEntrypoints(name);
   const tgzName = (
     await asyncExecuteCommand(`npm pack build/node_modules/${name}`)
```
But that's pretty hacky and fragile.

I evaluated replacing `ncp` with some other alternatives:
* `copyfiles` - Bad documentation.
* `cpx` – Similar interface, seems to have the exact same race case problem as `ncp`
* `recursively-copy` - Worked locally but broke CI (see #22323).

Seems like this is perhaps related to https://github.com/AvianFlu/ncp/issues/127

With this change, I am able to successfully run the command shown above:

![Screen Shot 2021-09-15 at 11 13 48 AM](https://user-images.githubusercontent.com/29597/133467273-26ada426-afec-4ec6-85d0-64104eebd604.png)


Changed files:
filename: scripts/rollup/utils.js
patch: @@ -15,9 +15,12 @@ function asyncCopyTo(from, to) {
           if (error) {
             // Wrap to have a useful stack trace.
             reject(new Error(error));
-            return;
+          } else {
+            // Wait for copied files to exist; ncp() sometimes completes prematurely.
+            // For more detail, see github.com/facebook/react/issues/22323
+            // Also github.com/AvianFlu/ncp/issues/127
+            setTimeout(resolve, 10);
           }
-          resolve();
         });
       })
   );