diff --git a/scripts/build/babel-register.js b/scripts/build/babel-register.js index 0b7f52b4c1b..8472e135814 100644 --- a/scripts/build/babel-register.js +++ b/scripts/build/babel-register.js @@ -9,7 +9,7 @@ * @oncall react_native */ -const {PACKAGES_DIR, RN_INTEGRATION_TESTS_RUNNER_DIR} = require('./build'); +const {PACKAGES_DIR, RN_INTEGRATION_TESTS_RUNNER_DIR} = require('../consts'); let isRegisteredForMonorepo = false; diff --git a/scripts/build/build.js b/scripts/build/build.js index e5107187e48..99bfd9d7524 100644 --- a/scripts/build/build.js +++ b/scripts/build/build.js @@ -9,7 +9,7 @@ * @oncall react_native */ -const {PACKAGES_DIR, RN_INTEGRATION_TESTS_RUNNER_DIR} = require('../consts'); +const {PACKAGES_DIR, REPO_ROOT} = require('../consts'); const { buildConfig, getBabelConfig, @@ -19,7 +19,7 @@ const { const babel = require('@babel/core'); const chalk = require('chalk'); const translate = require('flow-api-translator'); -const {accessSync, constants, promises: fs, readFileSync} = require('fs'); +const {promises: fs} = require('fs'); const glob = require('glob'); const micromatch = require('micromatch'); const path = require('path'); @@ -71,39 +71,39 @@ async function build() { process.exitCode = 0; } -function invert(map /*: Map*/) /*: Map */ { - const result /*: Map*/ = new Map(); - for (const [key, value] of map.entries()) { - result.set(value, key); - } - return result; -} - async function buildPackage(packageName /*: string */) { const {emitTypeScriptDefs} = getBuildOptions(packageName); - const entryPointRewrites = getEntryPoints(packageName); + const entryPoints = await getEntryPoints(packageName); const files = glob .sync(path.resolve(PACKAGES_DIR, packageName, SRC_DIR, '**/*'), { nodir: true, }) - .filter(file => !entryPointRewrites.has(file)); + .filter( + file => + !entryPoints.has(file) && + !entryPoints.has(file.replace(/\.js$/, '.flow.js')), + ); process.stdout.write( `${packageName} ${chalk.dim('.').repeat(72 - packageName.length)} `, ); - const invertedEntryPointRewrites = invert(entryPointRewrites); - - // Build all files matched for package + // Build regular files for (const file of files) { await buildFile(path.normalize(file), { silent: true, - destPath: invertedEntryPointRewrites.get(file), }); } - // Validate program for emitted .d.ts files + // Build entry point files + for (const entryPoint of entryPoints) { + await buildFile(path.normalize(entryPoint), { + silent: true, + }); + } + + // Validate program for emitted .d.ts files if (emitTypeScriptDefs) { validateTypeScriptDefs(packageName); } @@ -118,9 +118,9 @@ async function buildFile( file /*: string */, options /*: {silent?: boolean, destPath?: string}*/ = {}, ) { - const {silent, destPath} = {silent: false, ...options}; + const {silent = false} = options; const packageName = getPackageName(file); - const buildPath = getBuildPath(destPath ?? file); + const buildPath = getBuildPath(file); const {emitFlowDefs, emitTypeScriptDefs} = getBuildOptions(packageName); const logResult = ({copied, desc} /*: {copied: boolean, desc?: string} */) => @@ -179,76 +179,100 @@ async function buildFile( /*:: type PackageJson = { name: string, - exports?: {[lookup: string]: string}, -} + exports?: {[subpath: string]: string | mixed}, +}; */ -// As a convention, we use a .js/.js.flow file pair for each package -// entry point, with the .js file being a Babel wrapper that can be -// used directly in the monorepo. On build, we drop this wrapper and -// emit a single file from the .js.flow contents. -// can be used directly within the repo. When built, this needs to be rewritten -// and the wrapper dropped: -// -// index.js ──────►{remove wrapper} -// ┌─►index.js -// index.flow.js├─►index.d.ts -// └─►index.flow.js -function getEntryPoints(packageName /*: string*/) /*: Map */ { +/** + * Get the set of Flow entry points to build. + * + * As a convention, we use a .js/.flow.js file pair for each package entry + * point, with the .js file being a Babel wrapper that can be used directly in + * the monorepo. On build, we drop this wrapper and emit a single file from the + * .flow.js contents. + * + * index.js ──────►(removed) + * ┌─►index.js + * index.flow.js├─►index.d.ts + * └─►index.js.flow + */ +async function getEntryPoints( + packageName /*: string */, +) /*: Promise> */ { + const packagePath = path.resolve(PACKAGES_DIR, packageName, 'package.json'); const pkg /*: PackageJson */ = JSON.parse( - readFileSync( - path.resolve(PACKAGES_DIR, packageName, 'package.json'), - 'utf8', - ), + await fs.readFile(packagePath, 'utf8'), ); + const entryPoints /*: Set */ = new Set(); - // Flow files we want transpiled in place of the wrapper js files - const pathMap /*: Map*/ = new Map(); + if (pkg.exports == null) { + throw new Error( + packageName + + ' does not define an "exports" field in its package.json. As part ' + + 'of the build setup, this field must be used in order to rewrite ' + + 'paths to built files in production.', + ); + } - for (const packagePath in pkg.exports) { - const original = revertRewriteExportsTarget(pkg.exports[packagePath]); + const exportsEntries = Object.entries(pkg.exports); - // Exported json files shouldn't be considered - if (!original.endsWith('.js')) { + for (const [subpath, target] of exportsEntries) { + if (typeof target !== 'string') { + throw new Error( + `Invalid exports field in package.json for ${packageName}. ` + + `exports["${subpath}"] must be a string target.`, + ); + } + + // Skip non-JS files + if (!target.endsWith('.js')) { continue; } + if (target.includes('*')) { + console.warn( + `${chalk.yellow('Warning')}: Encountered subpath pattern ${subpath}` + + ` in package.json exports for ${packageName}. Matched entry points ` + + 'will not be validated.', + ); + continue; + } + + // Normalize to original path if previously rewritten + const original = normalizeExportsTarget(target); + if (original.endsWith('.flow.js')) { throw new Error( - `${chalk.bold(packageName)} has ${chalk.bold( - 'exports.' + packagePath + ' = "' + original + '"', - )}. Expecting a .js wrapper file. See other monorepo packages for examples.`, + `Package ${packageName} defines exports["${subpath}"] = "${original}". ` + + 'Expecting a .js wrapper file. See other monorepo packages for examples.', ); } // Our special case for wrapper files that need to be stripped - const entryPoint = path.resolve(PACKAGES_DIR, packageName, original); - - const {dir, name} = path.parse(entryPoint); - const entryPointFlow = path.join(dir, name + '.flow.js'); + const resolvedTarget = path.resolve(PACKAGES_DIR, packageName, original); + const resolvedFlowTarget = resolvedTarget.replace(/\.js$/, '.flow.js'); try { - accessSync(entryPointFlow, constants.F_OK); + await Promise.all([ + fs.access(resolvedTarget), + fs.access(resolvedFlowTarget), + ]); } catch { throw new Error( - `${chalk.bold( - entryPointFlow, - )} does not exist when building ${chalk.bold(packageName)}. + `${resolvedFlowTarget} does not exist when building ${packageName}. -The ${chalk.bold("package.json's")} ${chalk.bold( - 'exports["' + packagePath + '"]', - )}: - - found: ${chalk.bold.green(entryPoint)} - - missing: ${chalk.bold.red(entryPointFlow)} +From package.json exports["${subpath}"]: + - found: ${path.relative(REPO_ROOT, resolvedTarget)} + - missing: ${path.relative(REPO_ROOT, resolvedFlowTarget)} -This is needed so users can directly import the file from the monorepo using Node.`, +This is needed so users can directly import this entry point from the monorepo.`, ); } - pathMap.set(entryPoint, entryPointFlow); + entryPoints.add(resolvedFlowTarget); } - return pathMap; + return entryPoints; } function getPackageName(file /*: string */) /*: string */ { @@ -260,7 +284,9 @@ function getBuildPath(file /*: string */) /*: string */ { return path.join( packageDir, - file.replace(path.join(packageDir, SRC_DIR), BUILD_DIR), + file + .replace(path.join(packageDir, SRC_DIR), BUILD_DIR) + .replace('.flow.js', '.js'), ); } @@ -268,14 +294,6 @@ async function rewritePackageExports(packageName /*: string */) { const packageJsonPath = path.join(PACKAGES_DIR, packageName, 'package.json'); const pkg = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); - if (pkg.exports == null) { - throw new Error( - packageName + - ' does not define an "exports" field in its package.json. As part ' + - 'of the build setup, this field must be used in order to rewrite ' + - 'paths to built files in production.', - ); - } pkg.exports = rewriteExportsField(pkg.exports); if (pkg.main != null) { @@ -313,7 +331,7 @@ function rewriteExportsTarget(target /*: string */) /*: string */ { return target.replace('./' + SRC_DIR + '/', './' + BUILD_DIR + '/'); } -function revertRewriteExportsTarget(target /*: string */) /*: string */ { +function normalizeExportsTarget(target /*: string */) /*: string */ { return target.replace('./' + BUILD_DIR + '/', './' + SRC_DIR + '/'); } @@ -365,7 +383,6 @@ module.exports = { getBuildPath, BUILD_DIR, PACKAGES_DIR, - RN_INTEGRATION_TESTS_RUNNER_DIR, SRC_DIR, };