From 7609d4a08d7ef02539b05d9a3fdadc7a6aa8f784 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen <4123478+tido64@users.noreply.github.com> Date: Tue, 8 Aug 2023 16:36:03 +0200 Subject: [PATCH] feat(polyfills): introduce `@rnx-kit/polyfills` (#2580) --- .changeset/angry-impalas-sneeze.md | 2 + incubator/polyfills/README.md | 51 +++++++++++++++++++ incubator/polyfills/package.json | 71 +++++++++++++++++++++++++++ incubator/polyfills/src/dependency.ts | 53 ++++++++++++++++++++ incubator/polyfills/src/index.ts | 40 +++++++++++++++ incubator/polyfills/src/types.ts | 8 +++ incubator/polyfills/tsconfig.json | 4 ++ yarn.lock | 20 ++++++++ 8 files changed, 249 insertions(+) create mode 100644 .changeset/angry-impalas-sneeze.md create mode 100644 incubator/polyfills/README.md create mode 100644 incubator/polyfills/package.json create mode 100644 incubator/polyfills/src/dependency.ts create mode 100644 incubator/polyfills/src/index.ts create mode 100644 incubator/polyfills/src/types.ts create mode 100644 incubator/polyfills/tsconfig.json diff --git a/.changeset/angry-impalas-sneeze.md b/.changeset/angry-impalas-sneeze.md new file mode 100644 index 000000000..a845151cc --- /dev/null +++ b/.changeset/angry-impalas-sneeze.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/incubator/polyfills/README.md b/incubator/polyfills/README.md new file mode 100644 index 000000000..fa8a552a3 --- /dev/null +++ b/incubator/polyfills/README.md @@ -0,0 +1,51 @@ +# @rnx-kit/polyfills + +[![Build](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml/badge.svg)](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml) +[![npm version](https://img.shields.io/npm/v/@rnx-kit/polyfills)](https://www.npmjs.com/package/@rnx-kit/polyfills) + +🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧 + +### THIS TOOL IS EXPERIMENTAL β€” USE WITH CAUTION + +🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧 + +This is a polyfills "autolinker" for Metro. It works like native module +autolinking, but gathers polyfills from dependencies instead. + +> **Note** +> +> This package is temporary. Ideally, this should be upstreamed to +> `@react-native-community/cli`. + +## Motivation + +Please read the +[Modularity](https://github.com/microsoft/rnx-kit/blob/tido/react-native-standard-api/text/0002-react-native-webapis.md#modularity) +section of the +[React Native WebAPIs RFC](https://github.com/microsoft/rnx-kit/pull/2504) for +its raison d'Γͺtre. + +## Installation + +```sh +yarn add @rnx-kit/polyfills --dev +``` + +or if you're using npm + +```sh +npm add --save-dev @rnx-kit/polyfills +``` + +## Usage + +```diff + const { makeMetroConfig } = require("@rnx-kit/metro-config"); + const { getPreludeModules } = require("@rnx-kit/polyfills"); + + module.exports = makeMetroConfig({ ++ serializer: { ++ getModulesRunBeforeMainModule: getPreludeModules, ++ }, + }); +``` diff --git a/incubator/polyfills/package.json b/incubator/polyfills/package.json new file mode 100644 index 000000000..2fee7bf78 --- /dev/null +++ b/incubator/polyfills/package.json @@ -0,0 +1,71 @@ +{ + "private": true, + "name": "@rnx-kit/polyfills", + "version": "0.0.1", + "description": "EXPERIMENTAL - USE WITH CAUTION - New package called polyfills", + "homepage": "https://github.com/microsoft/rnx-kit/tree/main/incubator/polyfills#readme", + "license": "MIT", + "author": { + "name": "Microsoft Open Source", + "email": "microsoftopensource@users.noreply.github.com" + }, + "files": [ + "lib/*" + ], + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/rnx-kit", + "directory": "incubator/polyfills" + }, + "engines": { + "node": ">=14.15" + }, + "scripts": { + "build": "rnx-kit-scripts build", + "format": "rnx-kit-scripts format", + "lint": "rnx-kit-scripts lint" + }, + "dependencies": { + "@rnx-kit/console": "^1.0.0", + "@rnx-kit/tools-node": "^2.0.0" + }, + "peerDependencies": { + "@react-native/js-polyfills": "*" + }, + "peerDependenciesMeta": { + "@react-native/js-polyfills": { + "optional": true + } + }, + "devDependencies": { + "@rnx-kit/scripts": "*", + "eslint": "^8.0.0", + "jest": "^29.2.1", + "metro-config": "^0.73.7", + "prettier": "^3.0.0", + "typescript": "^5.0.0" + }, + "eslintConfig": { + "extends": "@rnx-kit/eslint-config" + }, + "jest": { + "preset": "@rnx-kit/scripts" + }, + "rnx-kit": { + "alignDeps": { + "presets": [ + "microsoft/react-native", + "@rnx-kit/scripts/align-deps-preset.js" + ], + "requirements": [ + "react-native@0.71" + ], + "capabilities": [ + "metro-config" + ] + } + }, + "experimental": true +} diff --git a/incubator/polyfills/src/dependency.ts b/incubator/polyfills/src/dependency.ts new file mode 100644 index 000000000..d99f110ec --- /dev/null +++ b/incubator/polyfills/src/dependency.ts @@ -0,0 +1,53 @@ +import { error } from "@rnx-kit/console"; +import { readPackage } from "@rnx-kit/tools-node"; +import * as path from "path"; +import type { Context } from "./types"; + +function getDependencies({ projectRoot }: Context): string[] { + const manifest = readPackage(projectRoot); + + const dependencies = new Set(); + for (const section of ["dependencies", "devDependencies"] as const) { + const names = manifest[section]; + if (names) { + Object.keys(names).forEach((name) => dependencies.add(name)); + } + } + + return Array.from(dependencies); +} + +function isValidPath(p: string): boolean { + return ( + Boolean(p) && + !p.startsWith("..") && + !p.startsWith("/") && + !/^[A-Za-z]:/.test(p) + ); +} + +export function getDependencyPolyfills(context: Context): string[] { + const polyfills: string[] = []; + + const options = { paths: [context.projectRoot] }; + const dependencies = getDependencies(context); + + for (const name of dependencies) { + try { + const config = require.resolve(`${name}/react-native.config.js`, options); + const polyfill = require(config).dependency?.api?.polyfill; + if (typeof polyfill === "string") { + if (!isValidPath(polyfill)) { + error(`${name}: invalid polyfill path: ${polyfill}`); + continue; + } + + polyfills.push(path.resolve(path.dirname(config), polyfill)); + } + } catch (_) { + // ignore + } + } + + return polyfills; +} diff --git a/incubator/polyfills/src/index.ts b/incubator/polyfills/src/index.ts new file mode 100644 index 000000000..4d106d1b8 --- /dev/null +++ b/incubator/polyfills/src/index.ts @@ -0,0 +1,40 @@ +import { getDependencyPolyfills } from "./dependency"; +import type { Context, GetPreludeModules } from "./types"; + +/** + * Ideally, we'd need something between `serializer.getPolyfills` and + * `serializer.getModulesRunBeforeMainModule`. The former does not have access + * to `require`, while the latter requires that the listed modules are + * explicitly used in the bundle itself (see + * https://github.com/facebook/metro/issues/850). For now, we will use this fact + * to simply list all prelude modules. + */ +function defaultModules({ projectRoot }: Context): string[] { + const platforms = [ + "react-native", + "react-native-macos", + "react-native-windows", + ]; + const options = { paths: [projectRoot] }; + + const modules = []; + for (const platform of platforms) { + const core = `${platform}/Libraries/Core/InitializeCore`; + try { + modules.push(require.resolve(core, options)); + } catch (_) { + // ignore + } + } + + return modules; +} + +export const getPreludeModules: GetPreludeModules = () => { + const context = { projectRoot: process.cwd() }; + const modules = defaultModules(context); + const dependencyPolyfills = getDependencyPolyfills(context); + return modules.concat(dependencyPolyfills); +}; + +export default getPreludeModules; diff --git a/incubator/polyfills/src/types.ts b/incubator/polyfills/src/types.ts new file mode 100644 index 000000000..2207087a0 --- /dev/null +++ b/incubator/polyfills/src/types.ts @@ -0,0 +1,8 @@ +import type { ConfigT } from "metro-config"; + +export type Context = { + projectRoot: string; +}; + +export type GetPreludeModules = + ConfigT["serializer"]["getModulesRunBeforeMainModule"]; diff --git a/incubator/polyfills/tsconfig.json b/incubator/polyfills/tsconfig.json new file mode 100644 index 000000000..5d6d07c16 --- /dev/null +++ b/incubator/polyfills/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@rnx-kit/scripts/tsconfig-shared.json", + "include": ["src"] +} diff --git a/yarn.lock b/yarn.lock index 7975c814b..805e1faad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3867,6 +3867,26 @@ __metadata: languageName: unknown linkType: soft +"@rnx-kit/polyfills@workspace:incubator/polyfills": + version: 0.0.0-use.local + resolution: "@rnx-kit/polyfills@workspace:incubator/polyfills" + dependencies: + "@rnx-kit/console": ^1.0.0 + "@rnx-kit/scripts": "*" + "@rnx-kit/tools-node": ^2.0.0 + eslint: ^8.0.0 + jest: ^29.2.1 + metro-config: ^0.73.7 + prettier: ^3.0.0 + typescript: ^5.0.0 + peerDependencies: + "@react-native/js-polyfills": "*" + peerDependenciesMeta: + "@react-native/js-polyfills": + optional: true + languageName: unknown + linkType: soft + "@rnx-kit/react-native-auth@*, @rnx-kit/react-native-auth@workspace:packages/react-native-auth": version: 0.0.0-use.local resolution: "@rnx-kit/react-native-auth@workspace:packages/react-native-auth"