From cb01ec09c6a8df51f39d6b1419cbf173fa5abf4a Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Thu, 19 Jan 2023 14:14:10 -0800 Subject: [PATCH 01/25] Filter options passed to ResolutionContext, convert to exact type Summary: Update the context object passed to metro-resolver and custom resolvers to match the keys typed for metro-resolver and in our docs. The keys which are no longer exposed are: - `dirExists` - `emptyModulePath` - `mainFields` (will be re-added) - `moduleCache` - `platform` - `projectRoot` This is a breaking change that is summarised in the next commit. Reviewed By: motiz88 Differential Revision: D42546071 fbshipit-source-id: e50355da2edff864848a15c5aad3a98985e93bb6 --- docs/Configuration.md | 2 +- .../src/__tests__/index-test.js | 6 ++-- packages/metro-resolver/src/resolve.js | 12 +++---- packages/metro-resolver/src/types.js | 28 +++++++--------- .../DependencyGraph/ModuleResolution.js | 33 ++++++++++++++++--- 5 files changed, 49 insertions(+), 32 deletions(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index cba9cbb3a7..14bd5dcadf 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -271,7 +271,7 @@ Type: `Array` Additional platforms to resolve. Defaults to `['ios', 'android', 'windows', 'web']`. -For more information, see [Module Resolution](https://facebook.github.io/metro/docs/resolution) and [React Native's documentation for platform-specific extensions](https://reactnative.dev/docs/platform-specific-code#platform-specific-extensions). +For more information, see [Module Resolution](./Resolution.md) and [React Native's documentation for platform-specific extensions](https://reactnative.dev/docs/platform-specific-code#platform-specific-extensions). #### `requireCycleIgnorePatterns` diff --git a/packages/metro-resolver/src/__tests__/index-test.js b/packages/metro-resolver/src/__tests__/index-test.js index 4e484bb571..1df292703a 100644 --- a/packages/metro-resolver/src/__tests__/index-test.js +++ b/packages/metro-resolver/src/__tests__/index-test.js @@ -724,10 +724,10 @@ describe('resolveRequest', () => { ); }); - it('receives customTransformOptions', () => { + it('receives customResolverOptions', () => { expect( Resolver.resolve( - {...context, customTransformOptions: {key: 'value'}}, + {...context, customResolverOptions: {key: 'value'}}, '/root/project/foo.js', 'android', ), @@ -741,7 +741,7 @@ describe('resolveRequest', () => { { ...context, resolveRequest: Resolver.resolve, - customTransformOptions: {key: 'value'}, + customResolverOptions: {key: 'value'}, }, '/root/project/foo.js', 'android', diff --git a/packages/metro-resolver/src/resolve.js b/packages/metro-resolver/src/resolve.js index 6de2011cc2..496d2d13c6 100644 --- a/packages/metro-resolver/src/resolve.js +++ b/packages/metro-resolver/src/resolve.js @@ -143,7 +143,7 @@ function resolve( * `/smth/lib/foobar/index.ios.js`. */ function resolveModulePath( - context: ModulePathContext, + context: $ReadOnly<{...ModulePathContext, ...}>, toModuleName: string, platform: string | null, ): Resolution { @@ -167,7 +167,7 @@ function resolveModulePath( * a Haste package, it could be `/smth/Foo/index.js`. */ function resolveHasteName( - context: HasteContext, + context: $ReadOnly<{...HasteContext, ...}>, moduleName: string, platform: string | null, ): Result { @@ -227,7 +227,7 @@ class MissingFileInHastePackageError extends Error { * even a package directory. */ function resolveFileOrDir( - context: FileOrDirContext, + context: $ReadOnly<{...FileOrDirContext, ...}>, potentialModulePath: string, platform: string | null, ): Result { @@ -255,7 +255,7 @@ function resolveFileOrDir( * `bar` contains a package which entry point is `./lib/index` (or `./lib`). */ function resolveDir( - context: FileOrDirContext, + context: $ReadOnly<{...FileOrDirContext, ...}>, potentialDirPath: string, platform: string | null, ): Result { @@ -275,7 +275,7 @@ function resolveDir( * resolution process altogether. */ function resolvePackage( - context: FileOrDirContext, + context: $ReadOnly<{...FileOrDirContext, ...}>, packageJsonPath: string, platform: string | null, ): Resolution { @@ -308,7 +308,7 @@ function resolvePackage( * `/js/boop/index.js` (see `_loadAsDir` for that). */ function resolveFile( - context: FileContext, + context: $ReadOnly<{...FileContext, ...}>, dirPath: string, fileName: string, platform: string | null, diff --git a/packages/metro-resolver/src/types.js b/packages/metro-resolver/src/types.js index f2196a4862..0478a6e90e 100644 --- a/packages/metro-resolver/src/types.js +++ b/packages/metro-resolver/src/types.js @@ -63,14 +63,13 @@ export type ResolveAsset = ( ) => ?$ReadOnlyArray; export type FileContext = $ReadOnly<{ - +doesFileExist: DoesFileExist, - +isAssetFile: IsAssetFile, - +nodeModulesPaths: $ReadOnlyArray, - +preferNativePlatform: boolean, - +redirectModulePath: (modulePath: string) => string | false, - +resolveAsset: ResolveAsset, - +sourceExts: $ReadOnlyArray, - ... + doesFileExist: DoesFileExist, + isAssetFile: IsAssetFile, + nodeModulesPaths: $ReadOnlyArray, + preferNativePlatform: boolean, + redirectModulePath: (modulePath: string) => string | false, + resolveAsset: ResolveAsset, + sourceExts: $ReadOnlyArray, }>; export type FileOrDirContext = $ReadOnly<{ @@ -84,8 +83,7 @@ export type FileOrDirContext = $ReadOnly<{ * located in `node-haste/Package.js`, and fully duplicated in * `ModuleGraph/node-haste/Package.js` (!) */ - +getPackageMainPath: (packageJsonPath: string) => string, - ... + getPackageMainPath: (packageJsonPath: string) => string, }>; export type HasteContext = $ReadOnly<{ @@ -94,14 +92,13 @@ export type HasteContext = $ReadOnly<{ * Given a name, this should return the full path to the file that provides * a Haste module of that name. Ex. for `Foo` it may return `/smth/Foo.js`. */ - +resolveHasteModule: (name: string) => ?string, + resolveHasteModule: (name: string) => ?string, /** * Given a name, this should return the full path to the package manifest that * provides a Haste package of that name. Ex. for `Foo` it may return * `/smth/Foo/package.json`. */ - +resolveHastePackage: (name: string) => ?string, - ... + resolveHastePackage: (name: string) => ?string, }>; export type ModulePathContext = $ReadOnly<{ @@ -110,8 +107,7 @@ export type ModulePathContext = $ReadOnly<{ * Full path of the module that is requiring or importing the module to be * resolved. */ - +originModulePath: string, - ... + originModulePath: string, }>; export type ResolutionContext = $ReadOnly<{ @@ -127,13 +123,11 @@ export type ResolutionContext = $ReadOnly<{ unstable_enablePackageExports: boolean, resolveRequest?: ?CustomResolver, customResolverOptions: CustomResolverOptions, - ... }>; export type CustomResolutionContext = $ReadOnly<{ ...ResolutionContext, resolveRequest: CustomResolver, - ... }>; export type CustomResolver = ( diff --git a/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js b/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js index 24641a4321..ca00c72977 100644 --- a/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js +++ b/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js @@ -177,23 +177,46 @@ class ModuleResolver { platform: string | null, resolverOptions: ResolverInputOptions, ): BundlerResolution { + const { + disableHierarchicalLookup, + doesFileExist, + extraNodeModules, + isAssetFile, + nodeModulesPaths, + preferNativePlatform, + resolveAsset, + resolveRequest, + sourceExts, + unstable_conditionNames, + unstable_conditionsByPlatform, + unstable_enablePackageExports, + } = this._options; + try { const result = Resolver.resolve( { - ...this._options, + allowHaste, + disableHierarchicalLookup, + doesFileExist, + extraNodeModules, + isAssetFile, + nodeModulesPaths, + preferNativePlatform, + resolveAsset, + resolveRequest, + sourceExts, + unstable_conditionNames, + unstable_conditionsByPlatform, + unstable_enablePackageExports, customResolverOptions: resolverOptions.customResolverOptions ?? {}, originModulePath: fromModule.path, redirectModulePath: (modulePath: string) => this._redirectRequire(fromModule, modulePath), - allowHaste, - platform, resolveHasteModule: (name: string) => this._options.getHasteModulePath(name, platform), resolveHastePackage: (name: string) => this._options.getHastePackagePath(name, platform), getPackageMainPath: this._getPackageMainPath, - unstable_enablePackageExports: - this._options.unstable_enablePackageExports, }, moduleName, platform, From f018ea2592c6e7fe9937679a0235b9ef450689d6 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Thu, 19 Jan 2023 14:14:10 -0800 Subject: [PATCH 02/25] Drop getPackageMainPath from resolution context, move logic to metro-resolver Summary: - Expand `CustomResolutionContext` to expose an accessor for cached `package.json` contents and to include overridable `mainFields` key. This will be used by [package exports](https://github.com/react-native-community/discussions-and-proposals/blob/master/proposals/0534-metro-package-exports-support.md) resolution. - Remove `getPackageMainPath` from the `CustomResolutionContext` API (replaced by `getPackage`). This is made along with a refactor to move resolution helpers against `package.json` into metro-resolver. - Add dedicated tests for browser spec to metro-resolver. Changelog: **[Breaking]** Filter untyped context properties passed to custom resolvers, expose `mainFields` Reviewed By: robhogan Differential Revision: D42501435 fbshipit-source-id: 6918df8dfc0ab2a1e1c3e07969f76df7377c4c6f --- docs/Resolution.md | 8 +- packages/metro-resolver/src/PackageResolve.js | 84 +++++++++ .../__snapshots__/index-test.js.snap | 4 +- .../src/__tests__/browser-spec-test.js | 94 ++++++++++ .../src/__tests__/index-test.js | 166 ++++++------------ .../metro-resolver/src/__tests__/utils.js | 49 ++++++ packages/metro-resolver/src/resolve.js | 10 +- packages/metro-resolver/src/types.js | 23 ++- .../DependencyGraph/ModuleResolution.js | 14 +- packages/metro/src/node-haste/Package.js | 89 +--------- 10 files changed, 325 insertions(+), 216 deletions(-) create mode 100644 packages/metro-resolver/src/PackageResolve.js create mode 100644 packages/metro-resolver/src/__tests__/browser-spec-test.js create mode 100644 packages/metro-resolver/src/__tests__/utils.js diff --git a/docs/Resolution.md b/docs/Resolution.md index 27db40ce87..8cb6341c9b 100644 --- a/docs/Resolution.md +++ b/docs/Resolution.md @@ -160,11 +160,13 @@ See also [Static Image Resources](https://reactnative.dev/docs/images#static-ima The list of file extensions to try, in order, when resolving a module path that does not exist on disk. Defaults to [`resolver.sourceExts`](./Configuration.md#sourceexts). -#### `getPackageMainPath: string => string` +#### `mainFields: $ReadOnlyArray` -Given the path to a `package.json` file, returns the contents of the `main` field, or the appropriate alternative field describing the entry point (e.g. `browser`). +The ordered list of fields in `package.json` that should be read to resolve a package's main entry point (and any subpath file replacements) per the ["browser" field spec](https://github.com/defunctzombie/package-browser-field-spec). Defaults to [`resolver.resolverMainFields`](./Configuration.md#resolvermainfields). -The default implementation of this function respects [`resolver.resolverMainFields`](./Configuration.md#resolvermainfields). +#### `getPackage: string => PackageJson` + +Given the path to a `package.json` file, returns the parsed file contents. #### `resolveHasteModule: string => ?string` diff --git a/packages/metro-resolver/src/PackageResolve.js b/packages/metro-resolver/src/PackageResolve.js new file mode 100644 index 0000000000..02873ed816 --- /dev/null +++ b/packages/metro-resolver/src/PackageResolve.js @@ -0,0 +1,84 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + * @format + * @oncall react_native + */ + +import type {PackageJson} from './types'; + +/** + * Resolve the main entry point for a package. + * + * Implements legacy (non-exports) package resolution behaviour based on the + * "browser" field spec (https://github.com/defunctzombie/package-browser-field-spec). + */ +export function getPackageEntryPoint( + pkg: PackageJson, + mainFields: $ReadOnlyArray, +): string { + let main = 'index'; + + for (const name of mainFields) { + if (typeof pkg[name] === 'string' && pkg[name].length) { + main = pkg[name]; + break; + } + } + + const replacements = getSubpathReplacements(pkg, mainFields); + if (replacements) { + const variants = [main]; + if (main.slice(0, 2) === './') { + variants.push(main.slice(2)); + } else { + variants.push('./' + main); + } + + for (const variant of variants) { + const winner = + replacements[variant] || + replacements[variant + '.js'] || + replacements[variant + '.json'] || + replacements[variant.replace(/(\.js|\.json)$/, '')]; + + if (winner) { + main = winner; + break; + } + } + } + + return main; +} + +/** + * Get the subpath replacements defined by any non-string `mainFields` values + * (https://github.com/defunctzombie/package-browser-field-spec#replace-specific-files---advanced). + */ +export function getSubpathReplacements( + pkg: PackageJson, + mainFields: $ReadOnlyArray, +): {[subpath: string]: string | false} | null { + const replacements = mainFields + .map((name: string) => { + // If the field is a string, that doesn't mean we want to redirect the + // `main` file itself to anything else. See the spec. + if (!pkg[name] || typeof pkg[name] === 'string') { + return null; + } + + return pkg[name]; + }) + .filter(Boolean); + + if (!replacements.length) { + return null; + } + + return Object.assign({}, ...replacements.reverse()); +} diff --git a/packages/metro-resolver/src/__tests__/__snapshots__/index-test.js.snap b/packages/metro-resolver/src/__tests__/__snapshots__/index-test.js.snap index c14652658e..f7a01c85d9 100644 --- a/packages/metro-resolver/src/__tests__/__snapshots__/index-test.js.snap +++ b/packages/metro-resolver/src/__tests__/__snapshots__/index-test.js.snap @@ -3,6 +3,6 @@ exports[`throws on invalid package name 1`] = ` "The package \`/root/node_modules/invalid/package.json\` is invalid because it specifies a \`main\` module field that could not be resolved (\`/root/node_modules/invalid/main\`. None of these files exist: - * /root/node_modules/invalid/main(.js) - * /root/node_modules/invalid/main/index(.js)" + * /root/node_modules/invalid/main(.js|.jsx|.json|.ts|.tsx) + * /root/node_modules/invalid/main/index(.js|.jsx|.json|.ts|.tsx)" `; diff --git a/packages/metro-resolver/src/__tests__/browser-spec-test.js b/packages/metro-resolver/src/__tests__/browser-spec-test.js new file mode 100644 index 0000000000..52b732784c --- /dev/null +++ b/packages/metro-resolver/src/__tests__/browser-spec-test.js @@ -0,0 +1,94 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +import type {ResolutionContext} from '../index'; + +import Resolver from '../index'; +import {createResolutionContext} from './utils'; + +const files = { + '/root/src/main.js': '', + '/root/node_modules/test-pkg/package.json': '', + '/root/node_modules/test-pkg/index.js': '', + '/root/node_modules/test-pkg/index-browser.js': '', + '/root/node_modules/test-pkg/index-react-native.js': '', +}; + +describe('browser field spec', () => { + describe('alternate main fields', () => { + const resolveTestPkg = (context: $Partial) => + Resolver.resolve( + { + ...createResolutionContext(files), + originModulePath: '/root/src/main.js', + ...context, + }, + 'test-pkg', + null, + ); + const packageJson = { + name: 'test-pkg', + main: 'index.js', + browser: 'index-browser.js', + 'react-native': 'index-react-native.js', + }; + + test('should resolve package entry point using passed `mainFields` in order', () => { + expect( + resolveTestPkg({ + getPackage: () => packageJson, + mainFields: ['browser', 'main'], + }), + ).toEqual({ + type: 'sourceFile', + filePath: '/root/node_modules/test-pkg/index-browser.js', + }); + + expect( + resolveTestPkg({ + getPackage: () => packageJson, + mainFields: ['react-native', 'browser', 'main'], + }), + ).toEqual({ + type: 'sourceFile', + filePath: '/root/node_modules/test-pkg/index-react-native.js', + }); + + expect( + resolveTestPkg({ + getPackage: () => ({ + name: 'test-pkg', + main: 'index.js', + }), + mainFields: ['browser', 'main'], + }), + ).toEqual({ + type: 'sourceFile', + filePath: '/root/node_modules/test-pkg/index.js', + }); + }); + + test('should resolve .js and .json file extensions implicitly', () => { + expect( + resolveTestPkg({ + getPackage: () => ({ + ...packageJson, + browser: 'index-browser', + }), + mainFields: ['browser', 'main'], + }), + ).toEqual({ + type: 'sourceFile', + filePath: '/root/node_modules/test-pkg/index-browser.js', + }); + }); + }); +}); diff --git a/packages/metro-resolver/src/__tests__/index-test.js b/packages/metro-resolver/src/__tests__/index-test.js index 1df292703a..db07cf942b 100644 --- a/packages/metro-resolver/src/__tests__/index-test.js +++ b/packages/metro-resolver/src/__tests__/index-test.js @@ -16,115 +16,53 @@ import type {ResolutionContext} from '../index'; const FailedToResolvePathError = require('../FailedToResolvePathError'); const Resolver = require('../index'); const path = require('path'); - -type FileTreeNode = $ReadOnly<{ - [name: string]: true | FileTreeNode, -}>; - -const CONTEXT: ResolutionContext = (() => { - const fileSet = new Set(); - (function fillFileSet(fileTree: FileTreeNode, prefix: string) { - for (const entName in fileTree) { - const entPath = path.join(prefix, entName); - if (fileTree[entName] === true) { - fileSet.add(entPath); - continue; - } - fillFileSet(fileTree[entName], entPath); +import {createResolutionContext} from './utils'; + +const fileMap = { + '/root/project/foo.js': '', + '/root/project/bar.js': '', + '/root/smth/beep.js': '', + '/root/node_modules/apple/package.json': '', + '/root/node_modules/apple/main.js': '', + '/root/node_modules/invalid/package.json': '', + '/node_modules/root-module/main.js': '', + '/node_modules/root-module/package.json': '', + '/other-root/node_modules/banana-module/main.js': '', + '/other-root/node_modules/banana-module/package.json': '', + '/other-root/node_modules/banana/main.js': '', + '/other-root/node_modules/banana/package.json': '', + '/other-root/node_modules/banana/node_modules/banana-module/main.js': '', + '/other-root/node_modules/banana/node_modules/banana-module/package.json': '', + '/haste/Foo.js': '', + '/haste/Bar.js': '', + '/haste/Override.js': '', + '/haste/some-package/package.json': '', + '/haste/some-package/subdir/other-file.js': '', + '/haste/some-package/main.js': '', +}; + +const CONTEXT: ResolutionContext = { + ...createResolutionContext(fileMap), + originModulePath: '/root/project/foo.js', + getPackage: (packageJsonPath: string) => ({ + name: path.basename(path.dirname(packageJsonPath)), + main: 'main', + }), + resolveHasteModule: (name: string) => { + const candidate = '/haste/' + name + '.js'; + if (candidate in fileMap) { + return candidate; } - })( - { - root: { - project: { - 'foo.js': true, - 'bar.js': true, - }, - smth: { - 'beep.js': true, - }, - node_modules: { - apple: { - 'package.json': true, - 'main.js': true, - }, - invalid: { - 'package.json': true, - }, - }, - }, - node_modules: { - 'root-module': { - 'package.json': true, - 'main.js': true, - }, - }, - 'other-root': { - node_modules: { - 'banana-module': { - 'package.json': true, - 'main.js': true, - }, - banana: { - 'package.json': true, - 'main.js': true, - node_modules: { - 'banana-module': { - 'package.json': true, - 'main.js': true, - }, - }, - }, - }, - }, - haste: { - 'Foo.js': true, - 'Bar.js': true, - 'Override.js': true, - 'some-package': { - 'package.json': true, - subdir: { - 'other-file.js': true, - }, - 'main.js': true, - }, - }, - }, - '/', - ); - return { - allowHaste: true, - customResolverOptions: {}, - disableHierarchicalLookup: false, - doesFileExist: (filePath: string) => fileSet.has(filePath), - extraNodeModules: null, - getPackageMainPath: (dirPath: string) => - path.join(path.dirname(dirPath), 'main'), - isAssetFile: () => false, - nodeModulesPaths: [], - originModulePath: '/root/project/foo.js', - preferNativePlatform: false, - redirectModulePath: (filePath: string) => filePath, - resolveAsset: (filePath: string) => null, - resolveHasteModule: (name: string) => { - const candidate = '/haste/' + name + '.js'; - if (fileSet.has(candidate)) { - return candidate; - } - return null; - }, - resolveHastePackage: (name: string) => { - const candidate = '/haste/' + name + '/package.json'; - if (fileSet.has(candidate)) { - return candidate; - } - return null; - }, - sourceExts: ['js'], - unstable_conditionNames: [], - unstable_conditionsByPlatform: {}, - unstable_enablePackageExports: false, - }; -})(); + return null; + }, + resolveHastePackage: (name: string) => { + const candidate = '/haste/' + name + '/package.json'; + if (candidate in fileMap) { + return candidate; + } + return null; + }, +}; it('resolves a relative path', () => { expect(Resolver.resolve(CONTEXT, './bar', null)).toEqual({ @@ -157,12 +95,12 @@ it('fails to resolve a relative path', () => { } expect(error.candidates).toEqual({ dir: { - candidateExts: ['', '.js'], + candidateExts: ['', '.js', '.jsx', '.json', '.ts', '.tsx'], filePathPrefix: '/root/project/apple/index', type: 'sourceFile', }, file: { - candidateExts: ['', '.js'], + candidateExts: ['', '.js', '.jsx', '.json', '.ts', '.tsx'], filePathPrefix: '/root/project/apple', type: 'sourceFile', }, @@ -180,12 +118,12 @@ it('throws on invalid package name', () => { } expect(error.message).toMatchSnapshot(); expect(error.fileCandidates).toEqual({ - candidateExts: ['', '.js'], + candidateExts: ['', '.js', '.jsx', '.json', '.ts', '.tsx'], filePathPrefix: '/root/node_modules/invalid/main', type: 'sourceFile', }); expect(error.indexCandidates).toEqual({ - candidateExts: ['', '.js'], + candidateExts: ['', '.js', '.jsx', '.json', '.ts', '.tsx'], filePathPrefix: '/root/node_modules/invalid/main/index', type: 'sourceFile', }); @@ -348,8 +286,8 @@ it('throws a descriptive error when a file inside a Haste package cannot be reso }).toThrowErrorMatchingInlineSnapshot(` "While resolving module \`some-package/subdir/does-not-exist\`, the Haste package \`some-package\` was found. However the module \`subdir/does-not-exist\` could not be found within the package. Indeed, none of these files exist: - * \`/haste/some-package/subdir/does-not-exist(.js)\` - * \`/haste/some-package/subdir/does-not-exist/index(.js)\`" + * \`/haste/some-package/subdir/does-not-exist(.js|.jsx|.json|.ts|.tsx)\` + * \`/haste/some-package/subdir/does-not-exist/index(.js|.jsx|.json|.ts|.tsx)\`" `); }); diff --git a/packages/metro-resolver/src/__tests__/utils.js b/packages/metro-resolver/src/__tests__/utils.js new file mode 100644 index 0000000000..7baad385b4 --- /dev/null +++ b/packages/metro-resolver/src/__tests__/utils.js @@ -0,0 +1,49 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +import type {ResolutionContext} from '../index'; + +/** + * Data structure approximating a file tree. Should be populated with complete + * paths mapping to file contents. + */ +type MockFileMap = {[path: string]: string}; + +/** + * Create a new partial `ResolutionContext` object given a mock file structure. + * Includes defaults closely matching metro-config, which can be overridden by + * consuming tests. + */ +export function createResolutionContext( + fileMap: MockFileMap, +): $Diff { + return { + allowHaste: true, + customResolverOptions: {}, + disableHierarchicalLookup: false, + doesFileExist: (filePath: string) => filePath in fileMap, + extraNodeModules: null, + getPackage: (packageJsonPath: string) => + JSON.parse(fileMap[packageJsonPath]), + isAssetFile: () => false, + mainFields: ['browser', 'main'], + nodeModulesPaths: [], + preferNativePlatform: false, + redirectModulePath: (filePath: string) => filePath, + resolveAsset: (filePath: string) => null, + resolveHasteModule: (name: string) => null, + resolveHastePackage: (name: string) => null, + sourceExts: ['js', 'jsx', 'json', 'ts', 'tsx'], + unstable_conditionNames: [], + unstable_conditionsByPlatform: {}, + unstable_enablePackageExports: false, + }; +} diff --git a/packages/metro-resolver/src/resolve.js b/packages/metro-resolver/src/resolve.js index 496d2d13c6..b43567079a 100644 --- a/packages/metro-resolver/src/resolve.js +++ b/packages/metro-resolver/src/resolve.js @@ -28,6 +28,7 @@ const FailedToResolveNameError = require('./FailedToResolveNameError'); const FailedToResolvePathError = require('./FailedToResolvePathError'); const formatFileCandidates = require('./formatFileCandidates'); const InvalidPackageError = require('./InvalidPackageError'); +const {getPackageEntryPoint} = require('./PackageResolve'); const isAbsolutePath = require('absolute-path'); const path = require('path'); @@ -279,7 +280,14 @@ function resolvePackage( packageJsonPath: string, platform: string | null, ): Resolution { - const mainPrefixPath = context.getPackageMainPath(packageJsonPath); + const packagePath = path.dirname(path.resolve(packageJsonPath)); + const mainPrefixPath = path.join( + packagePath, + getPackageEntryPoint( + context.getPackage(packageJsonPath), + context.mainFields, + ), + ); const dirPath = path.dirname(mainPrefixPath); const prefixName = path.basename(mainPrefixPath); const fileResult = resolveFile(context, dirPath, prefixName, platform); diff --git a/packages/metro-resolver/src/types.js b/packages/metro-resolver/src/types.js index 0478a6e90e..6d9dd00e61 100644 --- a/packages/metro-resolver/src/types.js +++ b/packages/metro-resolver/src/types.js @@ -44,6 +44,12 @@ export type FileCandidates = +candidateExts: $ReadOnlyArray, }; +export type PackageJson = $ReadOnly<{ + name?: string, + main?: string, + ... +}>; + /** * Check existence of a single file. */ @@ -74,16 +80,17 @@ export type FileContext = $ReadOnly<{ export type FileOrDirContext = $ReadOnly<{ ...FileContext, + + /** + * The ordered list of fields to read in `package.json` to resolve a main + * entry point based on the "browser" field spec. + */ + mainFields: $ReadOnlyArray, + /** - * This should return the path of the "main" module of the specified - * `package.json` file, after post-processing: for example, applying the - * 'browser' field if necessary. - * - * FIXME: move the post-processing here. Right now it is - * located in `node-haste/Package.js`, and fully duplicated in - * `ModuleGraph/node-haste/Package.js` (!) + * Get the parsed contents of the specified `package.json` file. */ - getPackageMainPath: (packageJsonPath: string) => string, + getPackage: (packageJsonPath: string) => PackageJson, }>; export type HasteContext = $ReadOnly<{ diff --git a/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js b/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js index ca00c72977..5ed1ed4fa4 100644 --- a/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js +++ b/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js @@ -19,6 +19,8 @@ import type { Resolution, ResolveAsset, } from 'metro-resolver'; +import type {ResolverInputOptions} from '../../shared/types.flow'; +import type {PackageJson} from 'metro-resolver/src/types'; const {codeFrameColumns} = require('@babel/code-frame'); const fs = require('fs'); @@ -26,18 +28,17 @@ const invariant = require('invariant'); const Resolver = require('metro-resolver'); const path = require('path'); const util = require('util'); -import type {ResolverInputOptions} from '../../shared/types.flow'; import type {BundlerResolution} from '../../DeltaBundler/types.flow'; export type DirExistsFn = (filePath: string) => boolean; export type Packageish = interface { path: string, + read(): PackageJson, redirectRequire( toModuleName: string, mainFields: $ReadOnlyArray, ): string | false, - getMain(mainFields: $ReadOnlyArray): string, }; export type Moduleish = interface { @@ -182,6 +183,7 @@ class ModuleResolver { doesFileExist, extraNodeModules, isAssetFile, + mainFields, nodeModulesPaths, preferNativePlatform, resolveAsset, @@ -200,6 +202,7 @@ class ModuleResolver { doesFileExist, extraNodeModules, isAssetFile, + mainFields, nodeModulesPaths, preferNativePlatform, resolveAsset, @@ -216,7 +219,7 @@ class ModuleResolver { this._options.getHasteModulePath(name, platform), resolveHastePackage: (name: string) => this._options.getHastePackagePath(name, platform), - getPackageMainPath: this._getPackageMainPath, + getPackage: this._getPackage, }, moduleName, platform, @@ -268,9 +271,8 @@ class ModuleResolver { } } - _getPackageMainPath = (packageJsonPath: string): string => { - const package_ = this._options.moduleCache.getPackage(packageJsonPath); - return package_.getMain(this._options.mainFields); + _getPackage = (packageJsonPath: string): PackageJson => { + return this._options.moduleCache.getPackage(packageJsonPath).read(); }; /** diff --git a/packages/metro/src/node-haste/Package.js b/packages/metro/src/node-haste/Package.js index 5a2d772db5..d3a4538a42 100644 --- a/packages/metro/src/node-haste/Package.js +++ b/packages/metro/src/node-haste/Package.js @@ -11,22 +11,18 @@ 'use strict'; +import type {PackageJson} from 'metro-resolver/src/types'; + +import {getSubpathReplacements} from 'metro-resolver/src/PackageResolve'; + const fs = require('fs'); const path = require('path'); -type PackageContent = { - name: string, - 'react-native': mixed, - browser: mixed, - main: ?string, - ... -}; - class Package { path: string; _root: string; - _content: ?PackageContent; + _content: ?PackageJson; constructor({file}: {file: string, ...}) { this.path = path.resolve(file); @@ -34,53 +30,6 @@ class Package { this._content = null; } - /** - * The `browser` field and replacement behavior is specified in - * https://github.com/defunctzombie/package-browser-field-spec. - */ - getMain(mainFields: $ReadOnlyArray): string { - const json = this.read(); - - let main; - - for (const name of mainFields) { - if (typeof json[name] === 'string') { - main = json[name]; - break; - } - } - - // flowlint-next-line sketchy-null-string:off - if (!main) { - main = 'index'; - } - - const replacements = getReplacements(json, mainFields); - if (replacements) { - const variants = [main]; - if (main.slice(0, 2) === './') { - variants.push(main.slice(2)); - } else { - variants.push('./' + main); - } - - for (const variant of variants) { - const winner = - replacements[variant] || - replacements[variant + '.js'] || - replacements[variant + '.json'] || - replacements[variant.replace(/(\.js|\.json)$/, '')]; - - if (winner) { - main = winner; - break; - } - } - } - - return path.join(this._root, main); - } - invalidate() { this._content = null; } @@ -89,8 +38,7 @@ class Package { name: string, mainFields: $ReadOnlyArray, ): string | false { - const json = this.read(); - const replacements = getReplacements(json, mainFields); + const replacements = getSubpathReplacements(this.read(), mainFields); if (!replacements) { return name; @@ -131,7 +79,7 @@ class Package { return name; } - read(): PackageContent { + read(): PackageJson { if (this._content == null) { this._content = JSON.parse(fs.readFileSync(this.path, 'utf8')); } @@ -139,27 +87,4 @@ class Package { } } -function getReplacements( - pkg: PackageContent, - mainFields: $ReadOnlyArray, -): ?{[string]: string | false, ...} { - const replacements = mainFields - .map((name: string) => { - // If the field is a string, that doesn't mean we want to redirect the - // `main` file itself to anything else. See the spec. - if (!pkg[name] || typeof pkg[name] === 'string') { - return null; - } - - return pkg[name]; - }) - .filter(Boolean); - - if (!replacements.length) { - return null; - } - - return Object.assign({}, ...replacements.reverse()); -} - module.exports = Package; From 4bcb4864c4619bc5b27cb0664d7a53023008368d Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Fri, 20 Jan 2023 02:34:02 -0800 Subject: [PATCH 03/25] Add integration test for require() + Fast Refresh Summary: Adds a test for the integration between Metro's `require()` and React, complementing the existing unit test. This lets us cover scenarios where the precise order of calling React's private APIs can impact the correctness of the result - e.g. if we call `performReactRefresh` before registering the latest versions of all components. NOTE: The tests aren't comprehensive and the test harness may undergo changes. This is meant mostly as a blueprint for future tests accompanying further work on the `require()` implementation. Reviewed By: GijsWeterings Differential Revision: D42551229 fbshipit-source-id: e62f7c49061aab46ea56ee667a33c1f620f6e90b --- flow-typed/npm/react-test-renderer_v16.x.x.js | 79 +++++++++ packages/metro-runtime/package.json | 4 +- .../__tests__/MetroFastRefreshMockRuntime.js | 152 ++++++++++++++++++ .../fast-refresh-integration-test.js | 124 ++++++++++++++ .../metro-runtime/src/polyfills/require.js | 17 +- yarn.lock | 43 ++++- 6 files changed, 408 insertions(+), 11 deletions(-) create mode 100644 flow-typed/npm/react-test-renderer_v16.x.x.js create mode 100644 packages/metro-runtime/src/polyfills/__tests__/MetroFastRefreshMockRuntime.js create mode 100644 packages/metro-runtime/src/polyfills/__tests__/fast-refresh-integration-test.js diff --git a/flow-typed/npm/react-test-renderer_v16.x.x.js b/flow-typed/npm/react-test-renderer_v16.x.x.js new file mode 100644 index 0000000000..44dc3e2c71 --- /dev/null +++ b/flow-typed/npm/react-test-renderer_v16.x.x.js @@ -0,0 +1,79 @@ +// flow-typed signature: 7bac6c05f7415881918d3d510109e739 +// flow-typed version: fce74493f0/react-test-renderer_v16.x.x/flow_>=v0.104.x + +// Type definitions for react-test-renderer 16.x.x +// Ported from: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-test-renderer + +type ReactComponentInstance = React$Component; + +type ReactTestRendererJSON = { + type: string, + props: { [propName: string]: any, ... }, + children: null | ReactTestRendererJSON[], + ... +}; + +type ReactTestRendererTree = ReactTestRendererJSON & { + nodeType: "component" | "host", + instance: ?ReactComponentInstance, + rendered: null | ReactTestRendererTree, + ... +}; + +type ReactTestInstance = { + instance: ?ReactComponentInstance, + type: string, + props: { [propName: string]: any, ... }, + parent: null | ReactTestInstance, + children: Array, + find(predicate: (node: ReactTestInstance) => boolean): ReactTestInstance, + findByType(type: React$ElementType): ReactTestInstance, + findByProps(props: { [propName: string]: any, ... }): ReactTestInstance, + findAll( + predicate: (node: ReactTestInstance) => boolean, + options?: { deep: boolean, ... } + ): ReactTestInstance[], + findAllByType( + type: React$ElementType, + options?: { deep: boolean, ... } + ): ReactTestInstance[], + findAllByProps( + props: { [propName: string]: any, ... }, + options?: { deep: boolean, ... } + ): ReactTestInstance[], + ... +}; + +type TestRendererOptions = { createNodeMock(element: React$Element): any, ... }; + +declare module "react-test-renderer" { + declare export type ReactTestRenderer = { + toJSON(): null | ReactTestRendererJSON, + toTree(): null | ReactTestRendererTree, + unmount(nextElement?: React$Element): void, + update(nextElement: React$Element): void, + getInstance(): ?ReactComponentInstance, + root: ReactTestInstance, + ... + }; + + declare type Thenable = { then(resolve: () => mixed, reject?: () => mixed): mixed, ... }; + + declare function create( + nextElement: React$Element, + options?: TestRendererOptions + ): ReactTestRenderer; + + declare function act(callback: () => void | Promise): Thenable; +} + +declare module "react-test-renderer/shallow" { + declare export default class ShallowRenderer { + static createRenderer(): ShallowRenderer; + getMountedInstance(): ReactTestInstance; + getRenderOutput>(): E; + getRenderOutput(): React$Element; + render(element: React$Element, context?: any): void; + unmount(): void; + } +} diff --git a/packages/metro-runtime/package.json b/packages/metro-runtime/package.json index 5744c64f85..f906b84a81 100644 --- a/packages/metro-runtime/package.json +++ b/packages/metro-runtime/package.json @@ -17,6 +17,8 @@ "react-refresh": "^0.4.0" }, "devDependencies": { - "@babel/core": "^7.20.0" + "@babel/core": "^7.20.0", + "react": "^18.2.0", + "react-test-renderer": "^18.2.0" } } diff --git a/packages/metro-runtime/src/polyfills/__tests__/MetroFastRefreshMockRuntime.js b/packages/metro-runtime/src/polyfills/__tests__/MetroFastRefreshMockRuntime.js new file mode 100644 index 0000000000..0744f2f2d0 --- /dev/null +++ b/packages/metro-runtime/src/polyfills/__tests__/MetroFastRefreshMockRuntime.js @@ -0,0 +1,152 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + * @oncall react_native + */ + +import {transformSync} from '@babel/core'; +import fs from 'fs'; + +import typeof React from 'react'; +import typeof ReactRefreshRuntime from 'react-refresh/runtime'; +import typeof ReactTestRenderer from 'react-test-renderer'; + +import type {DefineFn, RequireFn} from '../require'; + +type RuntimeGlobal = Object; + +/** + * A runtime that combines Metro's module system, a React renderer + * (react-test-renderer) and Fast Refresh. + * + * The runtime has its own global object and dedicated instances of the relevant + * Metro/React modules, but otherwise runs in the enclosing JS context without + * any true isolation. + */ +export class Runtime { + // Metro APIs (see require.js) + + /** + * Adds a module implementation to the module registry. + */ + define: DefineFn; + + /** + * Evaluates a given module (if not already evaluated) and returns its exports + * object. + */ + metroRequire: RequireFn; + + // Special modules + + /** + * The instance of React running in this runtime. Conceptually equivalent to + * require('react'). + */ + React: React; + + /** + * The React renderer running in this runtime. Conceptually equivalent to + * require('react-test-renderer'). + */ + renderer: ReactTestRenderer; + + /** + * Jest mock functions used as event handlers. + */ + events: { + onFullReload: JestMockFn<[string], void>, + onFastRefresh: JestMockFn<[], void>, + } = { + /** + * Called when there is a full reload, with a reason argument. + */ + onFullReload: jest.fn(), + + /** + * Called when Fast Refresh has occured. + */ + onFastRefresh: jest.fn(), + }; + + // $FlowFixMe[value-as-type]: react-refresh/runtime is untyped + #reactRefreshRuntime: ReactRefreshRuntime; + #global: RuntimeGlobal = {}; + #globalPrefix: string = ''; + + constructor() { + // Set up the module system and expose relevant APIs. + createModuleSystem(this.#global, /* __DEV__ */ true, this.#globalPrefix); + this.define = this.#global[this.#globalPrefix + '__d']; + this.metroRequire = this.#global[this.#globalPrefix + '__r']; + + // Set up Fast Refresh. Adapted from `setUpReactRefresh.js` in React Native. + jest.isolateModules(() => { + // $FlowFixMe[incompatible-type] Not sure why Flow doesn't approve + this.React = require('react'); + + this.#reactRefreshRuntime = require('react-refresh/runtime'); + this.#reactRefreshRuntime.injectIntoGlobalHook(this.#global); + + // Associate the renderer instance with this runtime's global object. + // NOTE: Strictly speaking, this is an implementation detail of React. + global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = + this.#global.__REACT_DEVTOOLS_GLOBAL_HOOK__; + this.renderer = require('react-test-renderer'); + delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__; + }); + + // Inject Fast Refresh APIs called by Metro. + this.#global[this.#globalPrefix + '__ReactRefresh'] = { + performFullRefresh: (reason: string) => { + this.events.onFullReload(reason); + }, + + createSignatureFunctionForTransform: + this.#reactRefreshRuntime.createSignatureFunctionForTransform, + + isLikelyComponentType: this.#reactRefreshRuntime.isLikelyComponentType, + + getFamilyByType: this.#reactRefreshRuntime.getFamilyByType, + + register: this.#reactRefreshRuntime.register, + + performReactRefresh: () => { + if (this.#reactRefreshRuntime.hasUnrecoverableErrors()) { + this.events.onFullReload('Fast Refresh - Unrecoverable'); + return; + } + this.#reactRefreshRuntime.performReactRefresh(); + this.events.onFastRefresh(); + }, + }; + } +} + +const moduleSystemCode = (() => { + const rawCode = fs.readFileSync(require.resolve('../require'), 'utf8'); + return transformSync(rawCode, { + ast: false, + babelrc: false, + cwd: '/', + filename: 'test.js', + presets: [require.resolve('metro-react-native-babel-preset')], + retainLines: true, + sourceMaps: 'inline', + sourceType: 'module', + }).code; +})(); + +const createModuleSystem: (RuntimeGlobal, boolean, string) => mixed = + // eslint-disable-next-line no-new-func + new Function( + 'global', + '__DEV__', + '__METRO_GLOBAL_PREFIX__', + moduleSystemCode, + ); diff --git a/packages/metro-runtime/src/polyfills/__tests__/fast-refresh-integration-test.js b/packages/metro-runtime/src/polyfills/__tests__/fast-refresh-integration-test.js new file mode 100644 index 0000000000..c05f4547fb --- /dev/null +++ b/packages/metro-runtime/src/polyfills/__tests__/fast-refresh-integration-test.js @@ -0,0 +1,124 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + * @oncall react_native + */ + +import {Runtime} from './MetroFastRefreshMockRuntime'; + +describe('Fast Refresh integration with require()', () => { + test('preserves state in a single-module bundle', async () => { + const {renderer, define, metroRequire, React, events} = new Runtime(); + + const ids = { + 'Component.js': 0, + }; + + // Define the initial version of the component + define( + (global, _1, _2, _3, module, _5, _6) => { + module.exports = function Component() { + const [state] = React.useState('initialState1'); + return 'version1: ' + state; + }; + // Register the component like `react-refresh/babel` would. + global.$RefreshReg$(module.exports, 'Component'); + }, + ids['Component.js'], + undefined, + 'Component.js', + ); + + // Initial render + const Component = metroRequire(ids['Component.js']); + const rendered = renderer.create(React.createElement(Component)); + expect(rendered.toJSON()).toBe('version1: initialState1'); + + // Edit the component + define( + (global, _1, _2, _3, module, _5, _6) => { + module.exports = function Component() { + const [state] = React.useState('initialState2'); + return 'version2: ' + state; + }; + // Register the component like `react-refresh/babel` would. + global.$RefreshReg$(module.exports, 'Component'); + }, + ids['Component.js'], + undefined, + 'Component.js', + // Inverse dependency map + { + [ids['Component.js']]: [], + }, + ); + jest.runAllTimers(); + + // Fast Refresh: Render the new version of the component with the old state. + expect(rendered.toJSON()).toBe('version2: initialState1'); + expect(events.onFastRefresh).toHaveBeenCalled(); + expect(events.onFullReload).not.toHaveBeenCalled(); + }); + + test('reloads a single-module bundle when invalidated by component signatures', async () => { + const {renderer, define, metroRequire, React, events} = new Runtime(); + + const ids = { + 'Component.js': 0, + }; + + // Define the initial version of the component + define( + (global, _1, _2, _3, module, _5, _6) => { + module.exports = function Component1() { + const [state] = React.useState('initialState1'); + return 'version1: ' + state; + }; + // Register the component like `react-refresh/babel` would. + global.$RefreshReg$(module.exports, 'Component1'); + }, + ids['Component.js'], + undefined, + 'Component.js', + ); + + // Initial render + const Component = metroRequire(ids['Component.js']); + const rendered = renderer.create(React.createElement(Component)); + expect(rendered.toJSON()).toBe('version1: initialState1'); + + // Edit the component + define( + (global, _1, _2, _3, module, _5, _6) => { + module.exports = function Component2() { + const [state] = React.useState('initialState2'); + return 'version2: ' + state; + }; + // Register the component like `react-refresh/babel` would. + global.$RefreshReg$(module.exports, 'Component2'); + }, + ids['Component.js'], + undefined, + 'Component.js', + // Inverse dependency map + { + [ids['Component.js']]: [], + }, + ); + jest.runAllTimers(); + + // Full refresh: The component does not rerender. Instead, we signal a + // reload. + expect(rendered.toJSON()).toBe('version1: initialState1'); + expect(events.onFastRefresh).not.toHaveBeenCalled(); + expect(events.onFullReload).toHaveBeenCalled(); + expect(events.onFullReload.mock.calls).toEqual([ + ['Fast Refresh - Invalidated boundary '], + ]); + }); +}); diff --git a/packages/metro-runtime/src/polyfills/require.js b/packages/metro-runtime/src/polyfills/require.js index 088f0e68a8..43dc352cc7 100644 --- a/packages/metro-runtime/src/polyfills/require.js +++ b/packages/metro-runtime/src/polyfills/require.js @@ -28,6 +28,7 @@ type DependencyMap = $ReadOnly< paths?: {[id: ModuleID]: string}, }, >; +type InverseDependencyMap = {[key: ModuleID]: Array, ...}; type Exports = any; type FactoryFn = ( global: Object, @@ -71,12 +72,20 @@ type ModuleList = { __proto__: null, ... }; -type RequireFn = (id: ModuleID | VerboseModuleNameForDev) => Exports; +export type RequireFn = (id: ModuleID | VerboseModuleNameForDev) => Exports; +export type DefineFn = ( + factory: FactoryFn, + moduleId: number, + dependencyMap?: DependencyMap, + verboseName?: string, + inverseDependencies?: InverseDependencyMap, +) => void; + type VerboseModuleNameForDev = string; type ModuleDefiner = (moduleId: ModuleID) => void; -global.__r = metroRequire; -global[`${__METRO_GLOBAL_PREFIX__}__d`] = define; +global.__r = (metroRequire: RequireFn); +global[`${__METRO_GLOBAL_PREFIX__}__d`] = (define: DefineFn); global.__c = clear; global.__registerSegment = registerSegment; @@ -542,7 +551,7 @@ if (__DEV__) { id: ModuleID, factory: FactoryFn, dependencyMap: DependencyMap, - inverseDependencies: {[key: ModuleID]: Array, ...}, + inverseDependencies: InverseDependencyMap, ) { const mod = modules[id]; if (!mod) { diff --git a/yarn.lock b/yarn.lock index b10c45697e..010a8a7a0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5340,7 +5340,7 @@ lodash@^4.17.14, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.2.1, lodash@^4.3.0: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -loose-envify@^1.0.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -6268,6 +6268,11 @@ quick-lru@^1.0.0: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= +"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0, react-is@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + react-is@^16.13.1, react-is@^16.8.4: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -6278,16 +6283,35 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-is@^18.0.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" - integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== - react-refresh@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.4.2.tgz#54a277a6caaac2803d88f1d6f13c1dcfbd81e334" integrity sha512-kv5QlFFSZWo7OlJFNYbxRtY66JImuP2LcrFgyJfQaf85gSP+byzG21UbDQEYjU7f//ny8rwiEkO6py2Y+fEgAQ== +react-shallow-renderer@^16.15.0: + version "16.15.0" + resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz#48fb2cf9b23d23cde96708fe5273a7d3446f4457" + integrity sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA== + dependencies: + object-assign "^4.1.1" + react-is "^16.12.0 || ^17.0.0 || ^18.0.0" + +react-test-renderer@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-18.2.0.tgz#1dd912bd908ff26da5b9fca4fd1c489b9523d37e" + integrity sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA== + dependencies: + react-is "^18.2.0" + react-shallow-renderer "^16.15.0" + scheduler "^0.23.0" + +react@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + read-cmd-shim@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz#87e43eba50098ba5a32d0ceb583ab8e43b961c16" @@ -6656,6 +6680,13 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + "semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" From 365fe6c0ef84e2c9548686097625b04e362cea51 Mon Sep 17 00:00:00 2001 From: Marshall Roch Date: Mon, 23 Jan 2023 07:38:48 -0800 Subject: [PATCH 04/25] Upgrade to Flow 0.198.1 Summary: Changelog: [Internal] Reviewed By: SamChou19815 Differential Revision: D42666047 fbshipit-source-id: d0cc4d048151f3aa1d1033f6096125080dbf2cbd --- .flowconfig | 2 +- package.json | 2 +- packages/metro/src/lib/formatBundlingError.js | 3 +++ yarn.lock | 8 ++++---- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.flowconfig b/.flowconfig index f71a7302e3..c79303b306 100644 --- a/.flowconfig +++ b/.flowconfig @@ -43,4 +43,4 @@ untyped-import untyped-type-import [version] -^0.196.3 +^0.198.1 diff --git a/package.json b/package.json index a5df044035..eb82f2d082 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.30.1", "eslint-plugin-relay": "^1.8.3", - "flow-bin": "^0.196.3", + "flow-bin": "^0.198.1", "glob": "^7.1.1", "hermes-eslint": "0.8.0", "invariant": "^2.2.4", diff --git a/packages/metro/src/lib/formatBundlingError.js b/packages/metro/src/lib/formatBundlingError.js index 5dcc338738..f9eee7e46a 100644 --- a/packages/metro/src/lib/formatBundlingError.js +++ b/packages/metro/src/lib/formatBundlingError.js @@ -75,6 +75,7 @@ function formatBundlingError(error: CustomError): FormattedError { return { type: 'ResourceNotFoundError', // $FlowFixMe[missing-empty-array-annot] + // $FlowFixMe[incompatible-return] errors: [], message: error.message, }; @@ -82,6 +83,7 @@ function formatBundlingError(error: CustomError): FormattedError { return { type: 'GraphNotFoundError', // $FlowFixMe[missing-empty-array-annot] + // $FlowFixMe[incompatible-return] errors: [], message: error.message, }; @@ -89,6 +91,7 @@ function formatBundlingError(error: CustomError): FormattedError { return { type: 'RevisionNotFoundError', // $FlowFixMe[missing-empty-array-annot] + // $FlowFixMe[incompatible-return] errors: [], message: error.message, }; diff --git a/yarn.lock b/yarn.lock index 010a8a7a0d..be499cd717 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3397,10 +3397,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561" integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA== -flow-bin@^0.196.3: - version "0.196.3" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.196.3.tgz#b6df48986a2629f2c6a26fb79d73fc07c8056af0" - integrity sha512-pmvjlksi1CvkSnDHpcfhDFj/KC3hwSgE2OpzvugW57dfgqfHzqX1UfZIcScGWM5AmP/IeOsQCW383k3zIbEnrA== +flow-bin@^0.198.1: + version "0.198.1" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.198.1.tgz#5fdad4d572bdc76f9ab24890c335e1f7addcecd4" + integrity sha512-9jWC1GJgV5QyeBxvT0GtTQtaw55imDRIh//C5WaS/dijl7IP34CrNY2NgBSwzif516SktkG8KylQWJaslZI2QA== for-in@^1.0.2: version "1.0.2" From 3ef8f13d8ce633ef598cefe9c1c4b29b1e08a421 Mon Sep 17 00:00:00 2001 From: Deepak Jacob Date: Mon, 23 Jan 2023 08:07:48 -0800 Subject: [PATCH 05/25] Use consistent naming: unstable_perfLogger to unstable_perfLoggerFactory Summary: Changelog: [Internal] Reviewed By: robhogan Differential Revision: D42675934 fbshipit-source-id: 7f80a0a08c0ee649b86256d3fa84f818f205aa72 --- packages/metro-config/src/configTypes.flow.js | 2 +- packages/metro/src/Server.js | 2 +- packages/metro/src/node-haste/DependencyGraph/createHasteMap.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/metro-config/src/configTypes.flow.js b/packages/metro-config/src/configTypes.flow.js index a6cfbda6f1..f0c7dec5e2 100644 --- a/packages/metro-config/src/configTypes.flow.js +++ b/packages/metro-config/src/configTypes.flow.js @@ -154,7 +154,7 @@ type MetalConfigT = { hasteMapCacheDirectory?: string, // Deprecated, alias of fileMapCacheDirectory unstable_fileMapCacheManagerFactory?: CacheManagerFactory, maxWorkers: number, - unstable_perfLogger?: ?PerfLoggerFactory, + unstable_perfLoggerFactory?: ?PerfLoggerFactory, projectRoot: string, stickyWorkers: boolean, transformerPath: string, diff --git a/packages/metro/src/Server.js b/packages/metro/src/Server.js index f6c654c89c..5037f6457e 100644 --- a/packages/metro/src/Server.js +++ b/packages/metro/src/Server.js @@ -519,7 +519,7 @@ class Server { await this._processBundleRequest(req, res, options, { buildNumber, bundlePerfLogger: - this._config.unstable_perfLogger?.('BUNDLING_REQUEST', { + this._config.unstable_perfLoggerFactory?.('BUNDLING_REQUEST', { key: buildNumber, }) ?? noopLogger, }); diff --git a/packages/metro/src/node-haste/DependencyGraph/createHasteMap.js b/packages/metro/src/node-haste/DependencyGraph/createHasteMap.js index 45cf96fb4f..5ecf812f86 100644 --- a/packages/metro/src/node-haste/DependencyGraph/createHasteMap.js +++ b/packages/metro/src/node-haste/DependencyGraph/createHasteMap.js @@ -66,7 +66,7 @@ function createHasteMap( config.fileMapCacheDirectory ?? config.hasteMapCacheDirectory, cacheFilePrefix: options?.cacheFilePrefix, })), - perfLoggerFactory: config.unstable_perfLogger, + perfLoggerFactory: config.unstable_perfLoggerFactory, computeDependencies, computeSha1: true, dependencyExtractor: config.resolver.dependencyExtractor, From c6ee5b307247d76f6822dbd6d092e4470f8e8120 Mon Sep 17 00:00:00 2001 From: Deepak Jacob Date: Mon, 23 Jan 2023 11:37:56 -0800 Subject: [PATCH 06/25] make perfLoggerFactory type not optional. Summary: Small refactoring to make `perfLoggerFactory` API cleaner. Right now, we have two types of logger passed to PerfLoggerFactory - `BUNDLING_REQUEST` - `HMR` When there is no `type` argument passed, we implicitly pass `rootPerfLogger`. I am making this explicit by adding new type `START_UP` - Add new PerfLoggerFactory type `START_UP`. - Change type to non-null for PerfLoggerFactory. Changelog: [Internal] Reviewed By: huntie Differential Revision: D42678113 fbshipit-source-id: 5a7936c98f7f1b30cf7572e69c6933a4673d117b --- packages/metro-config/src/configTypes.flow.js | 2 +- packages/metro-file-map/src/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/metro-config/src/configTypes.flow.js b/packages/metro-config/src/configTypes.flow.js index f0c7dec5e2..5a41915bf2 100644 --- a/packages/metro-config/src/configTypes.flow.js +++ b/packages/metro-config/src/configTypes.flow.js @@ -90,7 +90,7 @@ export type PerfLoggerFactoryOptions = $ReadOnly<{ }>; export type PerfLoggerFactory = ( - type?: 'BUNDLING_REQUEST' | 'HMR', + type: 'START_UP' | 'BUNDLING_REQUEST' | 'HMR', opts?: PerfLoggerFactoryOptions, ) => RootPerfLogger; diff --git a/packages/metro-file-map/src/index.js b/packages/metro-file-map/src/index.js index 82af415190..2d7b81d040 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -259,7 +259,7 @@ export default class HasteMap extends EventEmitter { if (options.perfLoggerFactory) { this._startupPerfLogger = - options.perfLoggerFactory?.().subSpan('hasteMap') ?? null; + options.perfLoggerFactory?.('START_UP').subSpan('hasteMap') ?? null; this._startupPerfLogger?.point('constructor_start'); } From 533baabca6b2fc9c9dda6d3919efa8a15901b037 Mon Sep 17 00:00:00 2001 From: Pieter Vanderwerff Date: Mon, 23 Jan 2023 11:50:48 -0800 Subject: [PATCH 07/25] Add types for prettier and jest-worker package Summary: Add Flow types for the `prettier` and `jest-worker` packages as well as improve the `fb-watchman` types. Reviewed By: evanyeung Differential Revision: D42614477 fbshipit-source-id: 714955b7f8017000baa693072faf364b907e5ae0 --- flow-typed/fb-watchman.js | 37 +- flow-typed/jest-worker.js | 143 +++ flow-typed/prettier.js | 835 ++++++++++++++++++ packages/metro-file-map/src/index.js | 7 +- packages/metro/src/DeltaBundler/WorkerFarm.js | 1 + scripts/updateBabelFlowTypes.js | 5 +- 6 files changed, 1018 insertions(+), 10 deletions(-) create mode 100644 flow-typed/jest-worker.js create mode 100644 flow-typed/prettier.js diff --git a/flow-typed/fb-watchman.js b/flow-typed/fb-watchman.js index 15ce4e57c8..a1c4e016b4 100644 --- a/flow-typed/fb-watchman.js +++ b/flow-typed/fb-watchman.js @@ -10,25 +10,34 @@ */ declare module 'fb-watchman' { - declare type WatchmanClockResponse = $ReadOnly<{ + declare type WatchmanBaseResponse = $ReadOnly<{ + version: string, clock: string, + }>; + + declare type WatchmanClockResponse = $ReadOnly<{ + ...WatchmanBaseResponse, warning?: string, - ... }>; declare type WatchmanSubscribeResponse = $ReadOnly<{ + ...WatchmanBaseResponse, subscribe: string, warning?: string, 'asserted-states': $ReadOnlyArray, - ... }>; declare type WatchmanWatchResponse = $ReadOnly<{ + ...WatchmanBaseResponse, watch: string, watcher: string, relative_path: string, warning?: string, - ... + }>; + + declare type WatchmanWatchListResponse = $ReadOnly<{ + ...WatchmanBaseResponse, + roots: $ReadOnlyArray, }>; declare type WatchmanSubscriptionEvent = { @@ -103,6 +112,13 @@ declare module 'fb-watchman' { 'suffix', string | $ReadOnlyArray, ]; + declare type WatchmanNameExpression = + | ['name' | 'iname', string | $ReadOnlyArray] + | [ + 'name' | 'iname', + string | $ReadOnlyArray, + 'basename' | 'wholename', + ]; declare type WatchmanTypeExpression = ['type', WatchmanFileType]; @@ -116,6 +132,7 @@ declare module 'fb-watchman' { | WatchmanDirnameExpression | WatchmanMatchExpression | WatchmanNotExpression + | WatchmanNameExpression | WatchmanSuffixExpression | WatchmanTypeExpression | WatchmanVariadicExpression; @@ -165,6 +182,10 @@ declare module 'fb-watchman' { config: ['watch-project', string], callback: (error: ?Error, response: WatchmanWatchResponse) => void, ): void; + command( + config: ['watch-list'], + callback: (error: ?Error, response: WatchmanWatchListResponse) => void, + ): void; command( config: ['query', string, WatchmanQuery], callback: (error: ?Error, response: WatchmanQueryResponse) => void, @@ -181,6 +202,14 @@ declare module 'fb-watchman' { config: ['subscribe', string, string, WatchmanQuery], callback: (error: ?Error, response: WatchmanSubscribeResponse) => void, ): void; + command( + config: ['state-enter', string, string], + callback: (error: ?Error, response: WatchmanBaseResponse) => void, + ): void; + command( + config: ['state-leave', string, string], + callback: (error: ?Error, response: WatchmanBaseResponse) => void, + ): void; end(): void; on('connect', () => void): void; diff --git a/flow-typed/jest-worker.js b/flow-typed/jest-worker.js new file mode 100644 index 0000000000..c6903e47d2 --- /dev/null +++ b/flow-typed/jest-worker.js @@ -0,0 +1,143 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + * @format + */ + +declare module 'jest-worker' { + declare export var CHILD_MESSAGE_INITIALIZE: 0; + declare export var CHILD_MESSAGE_CALL: 1; + declare export var CHILD_MESSAGE_END: 2; + + declare export var PARENT_MESSAGE_OK: 0; + declare export var PARENT_MESSAGE_CLIENT_ERROR: 1; + declare export var PARENT_MESSAGE_SETUP_ERROR: 2; + + declare export type PARENT_MESSAGE_ERROR = + | typeof PARENT_MESSAGE_CLIENT_ERROR + | typeof PARENT_MESSAGE_SETUP_ERROR; + + declare export type WorkerPoolOptions = $ReadOnly<{ + setupArgs: $ReadOnlyArray, + forkOptions: child_process$forkOpts, + maxRetries: number, + numWorkers: number, + enableWorkerThreads: boolean, + }>; + + declare export type ChildMessageInitialize = [ + typeof CHILD_MESSAGE_INITIALIZE, // type + boolean, // processed + string, // file + Array | void, // setupArgs + MessagePort | void, // MessagePort + ]; + + declare export type ChildMessageCall = [ + typeof CHILD_MESSAGE_CALL, // type + boolean, // processed + string, // method + Array, // args + ]; + + declare export type ChildMessageEnd = [ + typeof CHILD_MESSAGE_END, // type + boolean, // processed + ]; + + declare export type ChildMessage = + | ChildMessageInitialize + | ChildMessageCall + | ChildMessageEnd; + + declare export type ParentMessageOk = [ + typeof PARENT_MESSAGE_OK, // type + mixed, // result + ]; + + declare export type ParentMessageError = [ + PARENT_MESSAGE_ERROR, // type + string, // constructor + string, // message + string, // stack + mixed, // extra + ]; + + declare export type ParentMessage = ParentMessageOk | ParentMessageError; + + declare export interface WorkerInterface { + send( + request: ChildMessage, + onProcessStart: OnStart, + onProcessEnd: OnEnd, + ): void; + getWorkerId(): number; + getStderr(): stream$Readable | null; + getStdout(): stream$Readable | null; + onExit(exitCode: number): void; + onMessage(message: ParentMessage): void; + } + + declare export type OnStart = (worker: WorkerInterface) => void; + declare export type OnEnd = (err: Error | null, result: mixed) => void; + declare export interface WorkerPoolInterface { + getStderr(): stream$Readable; + getStdout(): stream$Readable; + getWorkers(): Array; + createWorker(options: WorkerOptions): WorkerInterface; + send( + workerId: number, + request: ChildMessage, + onStart: OnStart, + onEnd: OnEnd, + ): void; + end(): void; + } + + declare export type FarmOptions> = + $ReadOnly<{ + computeWorkerKey?: ( + method: string, + ...args: $ReadOnlyArray + ) => string | null, + exposedMethods?: $ReadOnlyArray, + forkOptions?: child_process$forkOpts, + setupArgs?: TSetupArgs, + maxRetries?: number, + numWorkers?: number, + WorkerPool?: ( + workerPath: string, + options?: WorkerPoolOptions, + ) => WorkerPoolInterface, + enableWorkerThreads?: boolean, + }>; + + declare export type IJestWorker = $ReadOnly<{ + // dynamically exposed methods from the worker + ...TExposed, + + getStderr: () => stream$Readable, + getStdout: () => stream$Readable, + end: () => Promise, + }>; + + declare export class Worker< + TExposed: $ReadOnly<{ + [string]: (...Array<$FlowFixMe>) => Promise<$FlowFixMe>, + }> = {}, + TSetupArgs: $ReadOnlyArray = $ReadOnlyArray, + > { + constructor( + workerPath: string, + options?: FarmOptions, + ): IJestWorker; + + getStderr(): stream$Readable; + getStdout(): stream$Readable; + end(): Promise; + } +} diff --git a/flow-typed/prettier.js b/flow-typed/prettier.js new file mode 100644 index 0000000000..7dfa9da6e6 --- /dev/null +++ b/flow-typed/prettier.js @@ -0,0 +1,835 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + * @format + */ + +// Adapted from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/3503bd8d80d4c5ebc9673eb1a16335896e10f47b/types/prettier/index.d.ts +declare module 'prettier' { + // declare export type LiteralUnion = T | (Pick & { _?: never | void }); + + declare export type AST = any; + + // https://github.com/prettier/prettier/blob/main/src/common/ast-path.js + + declare export class AstPath { + constructor(value: T): AstPath; + stack: Array; + getName(): string | number | null; + getValue(): T; + getNode(count?: number): T | null; + getParentNode(count?: number): T | null; + call(callback: (path: this) => U, ...names: Array): U; + callParent(callback: (path: this) => U, count?: number): U; + each( + callback: (path: this, index: number, value: any) => void, + ...names: Array + ): void; + map( + callback: (path: this, index: number, value: any) => U, + ...names: Array + ): Array; + match( + ...predicates: Array< + (node: any, name: string | null, number: number | null) => boolean, + > + ): boolean; + } + + declare export type BuiltInParser = (text: string, options?: any) => AST; + declare export type BuiltInParserName = + | 'angular' + | 'babel-flow' + | 'babel-ts' + | 'babel' + | 'css' + | 'espree' + | 'flow' + | 'glimmer' + | 'graphql' + | 'html' + | 'json-stringify' + | 'json' + | 'json5' + | 'less' + | 'lwc' + | 'markdown' + | 'mdx' + | 'meriyah' + | 'scss' + | 'typescript' + | 'vue' + | 'yaml'; + declare export type BuiltInParsers = {[BuiltInParserName]: BuiltInParser}; + + declare export type CustomParser = ( + text: string, + parsers: BuiltInParsers, + options: Options, + ) => AST; + + declare export type Options = $Partial; + declare export type RequiredOptions = { + ...DocPrinterOptions, + /** + * Print semicolons at the ends of statements. + * @default true + */ + semi: boolean, + /** + * Use single quotes instead of double quotes. + * @default false + */ + singleQuote: boolean, + /** + * Use single quotes in JSX. + * @default false + */ + jsxSingleQuote: boolean, + /** + * Print trailing commas wherever possible. + * @default 'es5' + */ + trailingComma: 'none' | 'es5' | 'all', + /** + * Print spaces between brackets in object literals. + * @default true + */ + bracketSpacing: boolean, + /** + * Put the `>` of a multi-line HTML (HTML, JSX, Vue, Angular) element at the end of the last line instead of being + * alone on the next line (does not apply to self closing elements). + * @default false + */ + bracketSameLine: boolean, + /** + * Put the `>` of a multi-line JSX element at the end of the last line instead of being alone on the next line. + * @default false + * @deprecated use bracketSameLine instead + */ + jsxBracketSameLine: boolean, + /** + * Format only a segment of a file. + * @default 0 + */ + rangeStart: number, + /** + * Format only a segment of a file. + * @default Infinity + */ + rangeEnd: number, + /** + * Specify which parser to use. + */ + parser: BuiltInParserName | CustomParser, + /** + * Specify the input filepath. This will be used to do parser inference. + */ + filepath: string, + /** + * Prettier can restrict itself to only format files that contain a special comment, called a pragma, at the top of the file. + * This is very useful when gradually transitioning large, unformatted codebases to prettier. + * @default false + */ + requirePragma: boolean, + /** + * Prettier can insert a special @format marker at the top of files specifying that + * the file has been formatted with prettier. This works well when used in tandem with + * the --require-pragma option. If there is already a docblock at the top of + * the file then this option will add a newline to it with the @format marker. + * @default false + */ + insertPragma: boolean, + /** + * By default, Prettier will wrap markdown text as-is since some services use a linebreak-sensitive renderer. + * In some cases you may want to rely on editor/viewer soft wrapping instead, so this option allows you to opt out. + * @default 'preserve' + */ + proseWrap: 'always' | 'never' | 'preserve', + /** + * Include parentheses around a sole arrow function parameter. + * @default 'always' + */ + arrowParens: 'avoid' | 'always', + /** + * Provide ability to support new languages to prettier. + */ + plugins: Array>, + /** + * Specify plugin directory paths to search for plugins if not installed in the same `node_modules` where prettier is located. + */ + pluginSearchDirs: Array, + /** + * How to handle whitespaces in HTML. + * @default 'css' + */ + htmlWhitespaceSensitivity: 'css' | 'strict' | 'ignore', + /** + * Which end of line characters to apply. + * @default 'lf' + */ + endOfLine: 'auto' | 'lf' | 'crlf' | 'cr', + /** + * Change when properties in objects are quoted. + * @default 'as-needed' + */ + quoteProps: 'as-needed' | 'consistent' | 'preserve', + /** + * Whether or not to indent the code inside