diff --git a/incubator/polyfills/README.md b/incubator/polyfills/README.md new file mode 100644 index 0000000000..5d592762df --- /dev/null +++ b/incubator/polyfills/README.md @@ -0,0 +1,47 @@ +# @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 + +This package is part of +[React Native WebAPIs](https://github.com/microsoft/rnx-kit/pull/2504). + +## 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 { getPolyfills } = require("@rnx-kit/polyfills"); + module.exports = makeMetroConfig({ ++ serializer: { ++ getPolyfills, ++ }, + }); +``` diff --git a/incubator/polyfills/package.json b/incubator/polyfills/package.json new file mode 100644 index 0000000000..0e822a52dc --- /dev/null +++ b/incubator/polyfills/package.json @@ -0,0 +1,74 @@ +{ + "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", + "test": "rnx-kit-scripts test" + }, + "dependencies": { + "@rnx-kit/console": "^1.0.0", + "@rnx-kit/tools-node": "^2.0.0", + "@rnx-kit/tools-react-native": "^1.3.1" + }, + "peerDependencies": { + "@react-native/js-polyfills": "*", + "react-native": ">=0.71.0" + }, + "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/defaultPolyfills.ts b/incubator/polyfills/src/defaultPolyfills.ts new file mode 100644 index 0000000000..f19298826a --- /dev/null +++ b/incubator/polyfills/src/defaultPolyfills.ts @@ -0,0 +1,34 @@ +import { error } from "@rnx-kit/console"; +import { getAvailablePlatforms } from "@rnx-kit/tools-react-native"; +import type { Context } from "./types"; + +function getDefaultPolyfillsPath({ + platform, + projectRoot, +}: Context): string | null { + const options = { paths: [projectRoot] }; + + try { + return require.resolve("@react-native/js-polyfills", options); + } catch (_) { + // `@react-native/js-polyfills` is available from 0.72+ + } + + const platforms = getAvailablePlatforms(projectRoot); + const reactNativePath = platforms[platform || "ios"] || "react-native"; + + try { + return require.resolve(`${reactNativePath}/rn-get-polyfills`, options); + } catch (_) { + error( + `Could not find polyfills for '${platform}' — if this is expected, you can ignore this error message` + ); + } + + return null; +} + +export function getDefaultPolyfills(context: Context): string[] { + const defaultPolyfillsPath = getDefaultPolyfillsPath(context); + return defaultPolyfillsPath ? require(defaultPolyfillsPath)() : []; +} diff --git a/incubator/polyfills/src/dependency.ts b/incubator/polyfills/src/dependency.ts new file mode 100644 index 0000000000..d99f110ecf --- /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 0000000000..b3f5820cfa --- /dev/null +++ b/incubator/polyfills/src/index.ts @@ -0,0 +1,12 @@ +import { getDefaultPolyfills } from "./defaultPolyfills"; +import { getDependencyPolyfills } from "./dependency"; +import type { GetPolyfills } from "./types"; + +export const getPolyfills: GetPolyfills = ({ platform }) => { + const context = { platform, projectRoot: process.cwd() }; + const polyfills = getDefaultPolyfills(context); + const dependencyPolyfills = getDependencyPolyfills(context); + return polyfills.concat(dependencyPolyfills); +}; + +export default getPolyfills; diff --git a/incubator/polyfills/src/types.ts b/incubator/polyfills/src/types.ts new file mode 100644 index 0000000000..f7dc04d02d --- /dev/null +++ b/incubator/polyfills/src/types.ts @@ -0,0 +1,8 @@ +import type { ConfigT } from "metro-config"; + +export type Context = { + platform: string | null; + projectRoot: string; +}; + +export type GetPolyfills = ConfigT["serializer"]["getPolyfills"]; diff --git a/incubator/polyfills/test/defaultPolyfills.test.ts b/incubator/polyfills/test/defaultPolyfills.test.ts new file mode 100644 index 0000000000..0d646811bb --- /dev/null +++ b/incubator/polyfills/test/defaultPolyfills.test.ts @@ -0,0 +1,17 @@ +import { getDefaultPolyfills } from "../src/defaultPolyfills"; + +describe("getDefaultPolyfills", () => { + const context = { + platform: "ios", + projectRoot: process.cwd(), + }; + + it("should return default polyfills", () => { + const defaultPolyfills = getDefaultPolyfills(context).sort(); + expect(defaultPolyfills).toEqual([ + expect.stringMatching(/[/\\]@react-native[/\\]polyfills[/\\]Object.es8.js$/), + expect.stringMatching(/[/\\]@react-native[/\\]polyfills[/\\]console.js$/), + expect.stringMatching(/[/\\]@react-native[/\\]polyfills[/\\]error-guard.js$/), + ]); + }); +}); diff --git a/incubator/polyfills/tsconfig.json b/incubator/polyfills/tsconfig.json new file mode 100644 index 0000000000..5d6d07c16e --- /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 a18db836b7..a5b68723cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3866,6 +3866,28 @@ __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 + "@rnx-kit/tools-react-native": ^1.3.1 + 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": "*" + react-native: ">=0.71.0" + 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"