Skip to content

Commit cc9164c

Browse files
huntiemeta-codesync[bot]
authored andcommitted
Move JavaScript exports rewrites to publishConfig/prepack (#54857)
Summary: ### Motivation Updates the shared JavaScript build setup to use the modern `publishConfig` convention. This: - Simplifies the build script. - Makes the production values for `"exports"` more understandable in place (especially by separating from exports conditions). - Prevents us from creating a dirty file state when running `yarn build`. ### Changes - Add `publishConfig` to each `package.json` listing production `"exports"` targets. - Add `scripts/build/prepack.js` script to action `publishConfig` (now on `npm pack`, `npm publish` exclusively). - Remove `"exports"` rewriting (and un-rewriting safeguards) from build script. **Note on `"prepack"`** Slightly unfortunately, `publishConfig` doesn't work consistently between package managers currently, including npm — so this does not work implicitly (but may in future). We're instead following `publishConfig` as a convention, and explicitly implementing a full copy (theoretically forking us towards pnpm and Yarn v4's approach). However, I believe this is: - Worthwhile, for the motivations above — and in particular being able to understand the final shape of `"exports"` (independent from the dimension of conditional exports, which may come into play later). - Completely inspectable/maintainable as an explicit implementation (`scripts/build/prepack.js`). Changelog: [Internal] Pull Request resolved: #54857 Test Plan: ### CI ✅ GitHub Actions ### End-to-end release test script (Note: Rebased on `0.83-stable` when tested) ``` yarn test-release-local -t "RNTestProject" -p "iOS" -c $GITHUB_TOKEN ``` {F1984106139} ✅ Test script runs `npm publish` on packages to a local proxy. {F1984106146} ✅ Installed packages have `publishConfig` `"exports"` values applied NOTE: ⬆️ This is **exactly** the same output as before. {F1984106148} ✅ `/tmp/RNTestProject` runs using built + proxy-published + proxy-installed packages Reviewed By: cipolleschi Differential Revision: D88963450 Pulled By: huntie fbshipit-source-id: f328252cf93a1f1039b79d7f369d1e6e7e5b4b52
1 parent 7d2a7c9 commit cc9164c

9 files changed

Lines changed: 123 additions & 82 deletions

File tree

packages/community-cli-plugin/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,18 @@
1818
".": "./src/index.js",
1919
"./package.json": "./package.json"
2020
},
21+
"publishConfig": {
22+
"exports": {
23+
".": "./dist/index.js",
24+
"./package.json": "./package.json"
25+
}
26+
},
2127
"files": [
2228
"dist"
2329
],
30+
"scripts": {
31+
"prepack": "node ../../scripts/build/prepack.js"
32+
},
2433
"dependencies": {
2534
"@react-native/dev-middleware": "0.84.0-main",
2635
"debug": "^4.4.0",

packages/core-cli-utils/package.json

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,40 @@
33
"version": "0.84.0-main",
44
"description": "React Native CLI library for Frameworks to build on",
55
"license": "MIT",
6-
"main": "./src/index.flow.js",
6+
"keywords": [
7+
"cli-utils",
8+
"react-native"
9+
],
10+
"homepage": "https://github.com/facebook/react-native/tree/HEAD/packages/core-cli-utils#readme",
11+
"bugs": "https://github.com/facebook/react-native/issues",
712
"repository": {
813
"type": "git",
914
"url": "git+https://github.com/facebook/react-native.git",
1015
"directory": "packages/core-cli-utils"
1116
},
17+
"main": "./src/index.flow.js",
1218
"exports": {
1319
".": "./src/index.js",
1420
"./package.json": "./package.json",
1521
"./version.js": "./src/public/version.js"
1622
},
17-
"homepage": "https://github.com/facebook/react-native/tree/HEAD/packages/core-cli-utils#readme",
18-
"keywords": [
19-
"cli-utils",
20-
"react-native"
21-
],
22-
"bugs": "https://github.com/facebook/react-native/issues",
23-
"engines": {
24-
"node": ">= 20.19.4"
23+
"publishConfig": {
24+
"main": "./dist/index.js",
25+
"exports": {
26+
".": "./dist/index.js",
27+
"./package.json": "./package.json",
28+
"./version.js": "./dist/public/version.js"
29+
}
2530
},
2631
"files": [
2732
"dist"
2833
],
34+
"scripts": {
35+
"prepack": "node ../../scripts/build/prepack.js"
36+
},
2937
"dependencies": {},
30-
"devDependencies": {}
38+
"devDependencies": {},
39+
"engines": {
40+
"node": ">= 20.19.4"
41+
}
3142
}

packages/debugger-shell/package.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,19 @@
1717
},
1818
"./package.json": "./package.json"
1919
},
20+
"publishConfig": {
21+
"main": "./dist/index.js",
22+
"exports": {
23+
".": {
24+
"node": "./dist/node/index.js",
25+
"electron": "./dist/electron/index.js"
26+
},
27+
"./package.json": "./package.json"
28+
}
29+
},
2030
"scripts": {
21-
"dev": "electron src/electron"
31+
"dev": "electron src/electron",
32+
"prepack": "node ../../scripts/build/prepack.js"
2233
},
2334
"repository": {
2435
"type": "git",

packages/dev-middleware/package.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,18 @@
1818
".": "./src/index.js",
1919
"./package.json": "./package.json"
2020
},
21+
"publishConfig": {
22+
"exports": {
23+
".": "./dist/index.js",
24+
"./package.json": "./package.json"
25+
}
26+
},
2127
"files": [
2228
"dist"
2329
],
30+
"scripts": {
31+
"prepack": "node ../../scripts/build/prepack.js"
32+
},
2433
"dependencies": {
2534
"@isaacs/ttlcache": "^1.4.1",
2635
"@react-native/debugger-frontend": "0.84.0-main",
@@ -35,13 +44,13 @@
3544
"serve-static": "^1.16.2",
3645
"ws": "^7.5.10"
3746
},
38-
"engines": {
39-
"node": ">= 20.19.4"
40-
},
4147
"devDependencies": {
4248
"@react-native/debugger-shell": "0.84.0-main",
4349
"selfsigned": "^4.0.0",
4450
"undici": "^5.29.0",
4551
"wait-for-expect": "^3.0.2"
52+
},
53+
"engines": {
54+
"node": ">= 20.19.4"
4655
}
4756
}

packages/metro-config/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,18 @@
2222
".": "./src/index.js",
2323
"./package.json": "./package.json"
2424
},
25+
"publishConfig": {
26+
"exports": {
27+
".": "./dist/index.js",
28+
"./package.json": "./package.json"
29+
}
30+
},
2531
"files": [
2632
"dist"
2733
],
34+
"scripts": {
35+
"prepack": "node ../../scripts/build/prepack.js"
36+
},
2837
"dependencies": {
2938
"@react-native/js-polyfills": "0.84.0-main",
3039
"@react-native/metro-babel-transformer": "0.84.0-main",

packages/react-native-compatibility-check/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,18 @@
2525
".": "./src/index.js",
2626
"./package.json": "./package.json"
2727
},
28+
"publishConfig": {
29+
"exports": {
30+
".": "./dist/index.js",
31+
"./package.json": "./package.json"
32+
}
33+
},
2834
"files": [
2935
"dist"
3036
],
37+
"scripts": {
38+
"prepack": "node ../../scripts/build/prepack.js"
39+
},
3140
"dependencies": {
3241
"@react-native/codegen": "0.84.0-main"
3342
},

scripts/build/README.md

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,6 @@ These scripts form the modern build setup for JavaScript ([Flow](https://flow.or
1717
- **When does the build run?**
1818
- Packages are built in CI workflows — both for integration/E2E tests, and before publishing to npm.
1919

20-
#### Limitations/quirks
21-
22-
> [!Note]
23-
> **🚧 Work in progress!** This is not the final state for our monorepo build tooling. Unfortunately, our solution options are narrow due to integration requirements with Meta's codebase.
24-
25-
- Running `yarn build` will mutate `package.json` files in place, resulting in a dirty Git working copy.
26-
- We make use of "wrapper files" (`.js``.js.flow`) for each package entry point, to enable running from source with zero config. To validate these, package entry points must be explicitly defined via `"exports"`.
27-
2820
## Usage
2921

3022
**💡 Reminder**: 99% of the time, there is no need to use `yarn build`, as all packages will run from source during development.
@@ -44,9 +36,6 @@ yarn clean
4436

4537
Once built, developing in the monorepo should continue to work — now using the compiled version of each package.
4638

47-
> [!Warning]
48-
> **Build changes should not be committed**. Currently, `yarn build` will make changes to each `package.json` file, which should not be committed. This is validated in CI.
49-
5039
## Configuration
5140

5241
Monorepo packages must be opted in for build, configured in `config.js` (where build options are also documented).
@@ -77,6 +66,7 @@ packages/
7766
7867
Notes:
7968
69+
- We make use of "wrapper files" (`.js``.js.flow`) for each package entry point, to enable running from source with zero config. To validate these, package entry points must be explicitly defined via `"exports"`.
8070
- To minimize complexity, prefer only a single entry of `{".": "src/index.js"}` in `"exports"` for new packages.
8171
8272
## Build behavior
@@ -85,8 +75,7 @@ Running `yarn build` will compile each package following the below steps, depend
8575
8676
- Create a `dist/` directory, replicating each source file under `src/`:
8777
- For every `@flow` file, strip Flow annotations using [flow-api-extractor](https://www.npmjs.com/package/flow-api-translator).
88-
- For every entry point in `"exports"`, remove the `.js` wrapper file and compile from the `.flow.js` source.
89-
- Rewrite each package `"exports"` target to map to the `dist/` directory location.
78+
- For each entry point in `"exports"`, remove the `.js` wrapper file and compile from the `.flow.js` source.
9079
- If configured, emit a Flow (`.js.flow`) or TypeScript (`.d.ts`) type definition file per source file, using [flow-api-extractor](https://www.npmjs.com/package/flow-api-translator).
9180
9281
Together, this might look like the following:
@@ -99,7 +88,7 @@ packages/
9988
index.js.flow # Flow definition file
10089
index.d.ts # TypeScript definition file
10190
[other transformed files]
102-
package.json # "src/index.js" export rewritten to "dist/index.js"
91+
package.json # "publishConfig" will override exports to "dist/" on publish
10392
```
10493
10594
**Link**: [Example `dist/` output on npm](https://www.npmjs.com/package/@react-native/dev-middleware/v/0.76.5?activeTab=code).

scripts/build/build.js

Lines changed: 3 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,6 @@ async function buildPackage(packageName /*: string */) {
136136
validateTypeScriptDefs(packageName);
137137
}
138138

139-
// Rewrite package.json "exports" field (src -> dist)
140-
await rewritePackageExports(packageName);
141-
142139
process.stdout.write(
143140
styleText(['reset', 'inverse', 'bold', 'green'], ' DONE '),
144141
);
@@ -330,18 +327,15 @@ async function getEntryPoints(
330327
continue;
331328
}
332329

333-
// Normalize to original path if previously rewritten
334-
const original = normalizeExportsTarget(target);
335-
336-
if (original.endsWith('.flow.js')) {
330+
if (target.endsWith('.flow.js')) {
337331
throw new Error(
338-
`Package ${packageName} defines exports["${subpath}"] = "${original}". ` +
332+
`Package ${packageName} defines exports["${subpath}"] = "${target}". ` +
339333
'Expecting a .js wrapper file. See other monorepo packages for examples.',
340334
);
341335
}
342336

343337
// Our special case for wrapper files that need to be stripped
344-
const resolvedTarget = path.resolve(PACKAGES_DIR, packageName, original);
338+
const resolvedTarget = path.resolve(PACKAGES_DIR, packageName, target);
345339
const resolvedFlowTarget = resolvedTarget.replace(/\.js$/, '.flow.js');
346340

347341
try {
@@ -383,51 +377,6 @@ function getBuildPath(file /*: string */) /*: string */ {
383377
);
384378
}
385379

386-
async function rewritePackageExports(packageName /*: string */) {
387-
const packageJsonPath = path.join(PACKAGES_DIR, packageName, 'package.json');
388-
const pkg = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
389-
390-
pkg.exports = rewriteExportsField(pkg.exports);
391-
392-
if (pkg.main != null) {
393-
pkg.main = rewriteExportsTarget(pkg.main);
394-
}
395-
396-
await fs.writeFile(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n');
397-
}
398-
399-
/*::
400-
type ExportsField = {
401-
[subpath: string]: ExportsField | string,
402-
} | string;
403-
*/
404-
405-
function rewriteExportsField(
406-
exportsField /*: ExportsField */,
407-
) /*: ExportsField */ {
408-
if (typeof exportsField === 'string') {
409-
return rewriteExportsTarget(exportsField);
410-
}
411-
412-
for (const key in exportsField) {
413-
if (typeof exportsField[key] === 'string') {
414-
exportsField[key] = rewriteExportsTarget(exportsField[key]);
415-
} else if (typeof exportsField[key] === 'object') {
416-
exportsField[key] = rewriteExportsField(exportsField[key]);
417-
}
418-
}
419-
420-
return exportsField;
421-
}
422-
423-
function rewriteExportsTarget(target /*: string */) /*: string */ {
424-
return target.replace('./' + SRC_DIR + '/', './' + BUILD_DIR + '/');
425-
}
426-
427-
function normalizeExportsTarget(target /*: string */) /*: string */ {
428-
return target.replace('./' + BUILD_DIR + '/', './' + SRC_DIR + '/');
429-
}
430-
431380
function validateTypeScriptDefs(packageName /*: string */) {
432381
const files = globSync('**/*.d.ts', {
433382
cwd: path.resolve(PACKAGES_DIR, packageName, BUILD_DIR),

scripts/build/prepack.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
* @format
9+
*/
10+
11+
require('../shared/babelRegister').registerForScript();
12+
13+
const {promises: fs} = require('fs');
14+
const path = require('path');
15+
16+
// "prepack" script to prepare JavaScript packages for publishing.
17+
//
18+
// We use this to copy over fields from "publishConfig" to the root of each
19+
// package.json, which is not supported in Yarn v1.
20+
21+
async function prepack() {
22+
const pkgJsonPath = path.join(process.cwd(), './package.json');
23+
const contents = JSON.parse(await fs.readFile(pkgJsonPath, 'utf8'));
24+
25+
if (
26+
path.dirname(pkgJsonPath).split(path.sep).slice(-2, -1)[0] !== 'packages'
27+
) {
28+
console.error('Error: prepack.js must be run from a package directory');
29+
process.exitCode = 1;
30+
return;
31+
}
32+
33+
if (contents.publishConfig != null) {
34+
for (const key of Object.keys(contents.publishConfig)) {
35+
contents[key] = contents.publishConfig[key];
36+
}
37+
}
38+
delete contents.publishConfig;
39+
40+
await fs.writeFile(pkgJsonPath, JSON.stringify(contents, null, 2) + '\n');
41+
}
42+
43+
if (require.main === module) {
44+
void prepack();
45+
}

0 commit comments

Comments
 (0)