diff --git a/.eslintrc.js b/.eslintrc.js index 54373f38e6..4c3ed2b85b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -60,14 +60,5 @@ module.exports = { 'import/no-commonjs': 'off', }, }, - { - files: [ - // flow-api-translator doesn't support translating `empty` - 'packages/metro-file-map/types/flow-types.d.ts', - ], - rules: { - '@typescript-eslint/no-explicit-any': 'off', - }, - }, ], }; diff --git a/package.json b/package.json index f101c64894..38e8ebcf80 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,10 @@ "eslint-plugin-lint": "^1.0.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-relay": "^1.8.3", - "flow-api-translator": "0.331.0", + "flow-api-translator": "0.332.0", "flow-bin": "^0.332.0", - "flow-eslint": "0.331.0", - "flow-parser": "0.331.0", + "flow-eslint": "0.332.0", + "flow-parser": "0.332.0", "invariant": "^2.2.4", "istanbul-api": "3.0.0", "istanbul-lib-coverage": "3.0.0", diff --git a/packages/metro-babel-register/package.json b/packages/metro-babel-register/package.json index 1ea3351762..76c5ecaa33 100644 --- a/packages/metro-babel-register/package.json +++ b/packages/metro-babel-register/package.json @@ -21,7 +21,7 @@ "@babel/preset-typescript": "^7.24.7", "@babel/register": "^7.24.6", "babel-plugin-replace-ts-export-assignment": "^0.0.2", - "flow-parser": "0.331.0", + "flow-parser": "0.332.0", "babel-plugin-transform-flow-enums": "^0.0.2", "escape-string-regexp": "^1.0.5", "flow-enums-runtime": "^0.0.6" diff --git a/packages/metro-babel-transformer/package.json b/packages/metro-babel-transformer/package.json index b6f330f444..250d291432 100644 --- a/packages/metro-babel-transformer/package.json +++ b/packages/metro-babel-transformer/package.json @@ -25,7 +25,7 @@ "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", - "flow-parser": "0.331.0", + "flow-parser": "0.332.0", "metro-cache-key": "0.87.1", "nullthrows": "^1.1.1" }, diff --git a/packages/metro-file-map/src/lib/dependencyExtractor.d.ts b/packages/metro-file-map/src/lib/dependencyExtractor.d.ts deleted file mode 100644 index 934a76244e..0000000000 --- a/packages/metro-file-map/src/lib/dependencyExtractor.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * 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. - * - * @format - */ - -declare const dependencyExtractor: { - extract: (code: string) => Set; -}; - -export = dependencyExtractor; diff --git a/packages/metro-file-map/src/plugins/dependencies/dependencyExtractor.d.ts b/packages/metro-file-map/src/plugins/dependencies/dependencyExtractor.d.ts deleted file mode 100644 index b8cf6d95c9..0000000000 --- a/packages/metro-file-map/src/plugins/dependencies/dependencyExtractor.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * 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. - * - * @generated by js1 build metro-ts-defs / yarn run build-ts-defs - */ - -declare const dependencyExtractor: { - extract(code: string): Set; -}; - -export = dependencyExtractor; diff --git a/packages/metro-file-map/src/plugins/dependencies/worker.d.ts b/packages/metro-file-map/src/plugins/dependencies/worker.d.ts deleted file mode 100644 index c7b79538f0..0000000000 --- a/packages/metro-file-map/src/plugins/dependencies/worker.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * 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. - * - * @format - * @oncall react_native - */ - -import type { - MetadataWorker, - V8Serializable, - WorkerMessage, -} from '../../flow-types'; - -declare class DependencyExtractorWorker implements MetadataWorker { - constructor(opts: Readonly<{dependencyExtractor: null | undefined | string}>); - processFile( - data: WorkerMessage, - utils: Readonly<{getContent: () => Buffer}>, - ): V8Serializable; -} -export = DependencyExtractorWorker; diff --git a/packages/metro-file-map/src/plugins/dependencies/worker.js b/packages/metro-file-map/src/plugins/dependencies/worker.js index 2828e2e02c..ce5eb3e7f4 100644 --- a/packages/metro-file-map/src/plugins/dependencies/worker.js +++ b/packages/metro-file-map/src/plugins/dependencies/worker.js @@ -19,7 +19,7 @@ const defaultDependencyExtractor = require('./dependencyExtractor'); import type {MetadataWorker, WorkerMessage, V8Serializable, DependencyExtractor} from '../../flow-types'; */ -module.exports = class DependencyExtractorWorker /*:: implements MetadataWorker */ { +class DependencyExtractorWorker /*:: implements MetadataWorker */ { // prettier-ignore /*:: readonly */ #dependencyExtractor /*: ?DependencyExtractor['extract'] */; @@ -56,4 +56,6 @@ module.exports = class DependencyExtractorWorker /*:: implements MetadataWorker // Return as array (PerFileData type) return Array.from(dependencies); } -}; +} + +module.exports = DependencyExtractorWorker; diff --git a/packages/metro-file-map/src/plugins/haste/worker.d.ts b/packages/metro-file-map/src/plugins/haste/worker.d.ts deleted file mode 100644 index eec44e52da..0000000000 --- a/packages/metro-file-map/src/plugins/haste/worker.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * 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. - * - * @format - */ - -import type { - MetadataWorker, - V8Serializable, - WorkerMessage, -} from '../../flow-types'; - -declare class Worker implements MetadataWorker { - constructor(opts: Readonly<{hasteImplModulePath: null | undefined | string}>); - processFile( - data: WorkerMessage, - utils: Readonly<{getContent: () => Buffer}>, - ): V8Serializable; -} - -export = Worker; diff --git a/packages/metro-file-map/src/plugins/haste/worker.js b/packages/metro-file-map/src/plugins/haste/worker.js index 63254a9ae2..6eb0a2f3e5 100644 --- a/packages/metro-file-map/src/plugins/haste/worker.js +++ b/packages/metro-file-map/src/plugins/haste/worker.js @@ -21,7 +21,7 @@ import type {MetadataWorker, WorkerMessage, V8Serializable} from '../../flow-typ const PACKAGE_JSON = path.sep + 'package.json'; -module.exports = class Worker /*:: implements MetadataWorker */ { +class Worker /*:: implements MetadataWorker */ { /*:: readonly */ #hasteImpl /*: ?Readonly<{getHasteName: string => ?string}> */ = null; @@ -63,4 +63,6 @@ module.exports = class Worker /*:: implements MetadataWorker */ { } return hasteName; } -}; +} + +module.exports = Worker; diff --git a/packages/metro-file-map/src/worker.d.ts b/packages/metro-file-map/src/worker.d.ts deleted file mode 100644 index 1c1e3d94c2..0000000000 --- a/packages/metro-file-map/src/worker.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * 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. - * - * @format - */ - -import type { - WorkerMessage, - WorkerMetadata, - WorkerSetupArgs, -} from './flow-types'; - -declare const worker: { - /** - * Called automatically by jest-worker before the first call to `worker` when - * this module is used as worker thread or child process. - */ - setup: (args: WorkerSetupArgs) => void; - /** - * Called by jest-worker with each workload - */ - processFile: (data: WorkerMessage) => WorkerMetadata; - /** - * Exposed for use outside a jest-worker context, ie when processing in-band. - */ - Worker: { - new (setupArgs: WorkerSetupArgs): { - processFile(data: WorkerMessage): WorkerMetadata; - }; - }; -}; - -export = worker; diff --git a/packages/metro-file-map/src/workerExclusionList.d.ts b/packages/metro-file-map/src/workerExclusionList.d.ts deleted file mode 100644 index ac3fba8b76..0000000000 --- a/packages/metro-file-map/src/workerExclusionList.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * 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. - * - * @format - */ - -declare const exclusionList: ReadonlySet; - -export = exclusionList; diff --git a/packages/metro-minify-terser/API.md b/packages/metro-minify-terser/API.md index 3bedd4d497..45e1507f12 100644 --- a/packages/metro-minify-terser/API.md +++ b/packages/metro-minify-terser/API.md @@ -4,4 +4,9 @@ ```ts +const $$MODULE_EXPORTS$$: typeof minifier; + +type $$MODULE_EXPORTS$$ = typeof $$MODULE_EXPORTS$$; +export default $$MODULE_EXPORTS$$; + ``` diff --git a/packages/metro-minify-terser/src/index.d.ts b/packages/metro-minify-terser/src/index.d.ts deleted file mode 100644 index 39ef5fc46f..0000000000 --- a/packages/metro-minify-terser/src/index.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * 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. - * - * @format - * @oncall react_native - */ - -import minifier from './minifier'; - -declare const minifierFn: typeof minifier; -export = minifierFn; diff --git a/packages/metro-runtime/API-modules-HMRClient.md b/packages/metro-runtime/API-modules-HMRClient.md new file mode 100644 index 0000000000..1849e12ca1 --- /dev/null +++ b/packages/metro-runtime/API-modules-HMRClient.md @@ -0,0 +1,20 @@ +## API Report File for "metro-runtime" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import EventEmitter = require('./vendor/eventemitter3'); + +class HMRClient extends EventEmitter { + constructor(url: string); + close(): void; + disable(): void; + enable(): void; + hasPendingUpdates(): boolean; + isEnabled(): boolean; + send(message: string): void; +} +export default HMRClient; + +``` diff --git a/packages/metro-runtime/API-modules-asyncRequire.md b/packages/metro-runtime/API-modules-asyncRequire.md new file mode 100644 index 0000000000..49ae822ea4 --- /dev/null +++ b/packages/metro-runtime/API-modules-asyncRequire.md @@ -0,0 +1,22 @@ +## API Report File for "metro-runtime" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +const $$MODULE_EXPORTS$$: AsyncRequire; + +type $$MODULE_EXPORTS$$ = typeof $$MODULE_EXPORTS$$; +export default $$MODULE_EXPORTS$$; + +export type AsyncRequire = { + (moduleID: number, paths: DependencyMapPaths, moduleName?: string): Promise; + unstable_importMaybeSync(moduleID: number, paths: DependencyMapPaths): Promise | T; + prefetch(moduleID: number, paths: DependencyMapPaths, moduleName?: string): void; +}; + +export type DependencyMapPaths = null | undefined | Readonly<{[moduleID: number | string]: ReadonlyJsonData}>; + +export type ReadonlyJsonData = null | boolean | number | string | ReadonlyArray | Readonly<{[$$Key$$: string]: ReadonlyJsonData}>; + +``` diff --git a/packages/metro-runtime/API-modules-null-module.md b/packages/metro-runtime/API-modules-null-module.md new file mode 100644 index 0000000000..a7ade4eeda --- /dev/null +++ b/packages/metro-runtime/API-modules-null-module.md @@ -0,0 +1,12 @@ +## API Report File for "metro-runtime" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +const $$MODULE_EXPORTS$$: null; + +type $$MODULE_EXPORTS$$ = typeof $$MODULE_EXPORTS$$; +export default $$MODULE_EXPORTS$$; + +``` diff --git a/packages/metro-runtime/API-polyfills-require.md b/packages/metro-runtime/API-polyfills-require.md new file mode 100644 index 0000000000..d0a5fdf23b --- /dev/null +++ b/packages/metro-runtime/API-polyfills-require.md @@ -0,0 +1,41 @@ +## API Report File for "metro-runtime" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +export type ArrayIndexable = Readonly<{[indexer: number]: T}>; + +export type DefineFn = (factory: FactoryFn, moduleId: number, dependencyMap?: DependencyMap, verboseName?: string, inverseDependencies?: InverseDependencyMap) => void; + +export type DependencyMap = Readonly< + ArrayIndexable & { + paths?: {[id in ModuleID]: string} | undefined; + } +>; + +export type Exports = any; + +export type FactoryFn = ( + global: object, + require: RequireFn, + metroImportDefault: RequireFn, + metroImportAll: RequireFn, + moduleObject: {exports: {}}, + exports: {}, + dependencyMap: null | undefined | DependencyMap, +) => void; + +export type InverseDependencyMap = {[key in ModuleID]: Array}; + +export function metroImportAll(moduleId: ModuleID | VerboseModuleNameForDev | number): any | Exports | {[$$Key$$: string]: any}; + +export function metroImportDefault(moduleId: ModuleID | VerboseModuleNameForDev): any | Exports; + +export type ModuleID = number; + +export type RequireFn = (id: ModuleID | VerboseModuleNameForDev) => Exports; + +export type VerboseModuleNameForDev = string; + +``` diff --git a/packages/metro-runtime/src/modules/HMRClient.js b/packages/metro-runtime/src/modules/HMRClient.js index 6a1d1916d7..b919c70f05 100644 --- a/packages/metro-runtime/src/modules/HMRClient.js +++ b/packages/metro-runtime/src/modules/HMRClient.js @@ -41,7 +41,7 @@ class HMRClient extends EventEmitter { _queue: Array = []; _state: SocketState = 'opening'; _ws: WebSocket; - _heartbeatTimer: ?IntervalID = null; + _heartbeatTimer: ?ReturnType = null; constructor(url: string) { super(); diff --git a/packages/metro-runtime/src/modules/asyncRequire.js b/packages/metro-runtime/src/modules/asyncRequire.js index c2fab9e1f9..400416fdf7 100644 --- a/packages/metro-runtime/src/modules/asyncRequire.js +++ b/packages/metro-runtime/src/modules/asyncRequire.js @@ -32,6 +32,24 @@ type DependencyMapPaths = ?Readonly<{ [moduleID: number | string]: ReadonlyJsonData, }>; +type AsyncRequire = { + ( + moduleID: number, + paths: DependencyMapPaths, + moduleName?: string, + ): Promise, + unstable_importMaybeSync( + moduleID: number, + paths: DependencyMapPaths, + ): Promise | T, + prefetch( + moduleID: number, + paths: DependencyMapPaths, + moduleName?: string, + ): void, + ... +}; + declare var __METRO_GLOBAL_PREFIX__: string; function maybeLoadBundle( @@ -97,4 +115,5 @@ asyncRequire.prefetch = function ( ); }; -module.exports = asyncRequire; +// The explicit type carries the statics through to generated type definitions. +module.exports = asyncRequire as AsyncRequire; diff --git a/packages/metro-symbolicate/src/index.d.ts b/packages/metro-symbolicate/src/index.d.ts deleted file mode 100644 index 84510675f4..0000000000 --- a/packages/metro-symbolicate/src/index.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 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. - * - * @format - * @oncall react_native - */ - -// No exports diff --git a/packages/metro-transform-plugins/API.md b/packages/metro-transform-plugins/API.md index e323191ed6..9b03f280ab 100644 --- a/packages/metro-transform-plugins/API.md +++ b/packages/metro-transform-plugins/API.md @@ -4,4 +4,25 @@ ```ts +import type $$IMPORT_TYPEOF_1$$ from '@babel/traverse'; +import type * as $$IMPORT_TYPEOF_1$$_2 from '@babel/core'; +import type * as $$IMPORT_TYPEOF_2$$ from '@babel/types'; +import type {Node as Node_2} from '@babel/types'; +import type {PluginObj} from '@babel/core'; +import type {SourceLocation} from '@babel/types'; +import type {Statement} from '@babel/types'; + +export interface TransformPlugins { + addParamsToDefineCall(code: string, ...params: unknown[]): string; + constantFoldingPlugin: typeof constantFoldingPlugin; + getTransformPluginCacheKeyFiles(): ReadonlyArray; + importExportPlugin: typeof importExportPlugin; + inlinePlugin: typeof inlinePlugin; + inlineRequiresPlugin: typeof $$EXPORT_DEFAULT_DECLARATION$$; + normalizePseudoGlobals: typeof normalizePseudoglobals; +} + +const transformPlugins: TransformPlugins; +export default transformPlugins; + ``` diff --git a/packages/metro/package.json b/packages/metro/package.json index 59531091f5..7c0e2d3728 100644 --- a/packages/metro/package.json +++ b/packages/metro/package.json @@ -32,7 +32,7 @@ "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", - "flow-parser": "0.331.0", + "flow-parser": "0.332.0", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", diff --git a/scripts/eslint/typescript.js b/scripts/eslint/typescript.js index 42c393b58a..c79351dbba 100644 --- a/scripts/eslint/typescript.js +++ b/scripts/eslint/typescript.js @@ -27,4 +27,18 @@ module.exports = { ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', + rules: { + // These files are generated from Flow sources, which Flow itself checks - + // an `any` here reflects one in the Flow source, or a construct the + // translator can't express. + '@typescript-eslint/no-explicit-any': 'off', + // Noise in generated definitions: a declaration a CommonJS module exports + // via `typeof` is reported as unused. The generator runs this rule itself, + // with `--fix`, to drop declarations that really are unused. + '@typescript-eslint/no-unused-vars': 'off', + // Flow's inexact `{...}` translates to `{}`, which this rule flags. The + // Flow sources that produce one are worth fixing rather than silencing, so + // this is off only until they are. + '@typescript-eslint/no-empty-object-type': 'off', + }, }; diff --git a/scripts/generateApiSnapshots.js b/scripts/generateApiSnapshots.js index a113016abb..4b04983c7a 100644 --- a/scripts/generateApiSnapshots.js +++ b/scripts/generateApiSnapshots.js @@ -94,6 +94,61 @@ function hasExports(dtsAbsolutePath: string): boolean { return /^\s*export[\s{*=]/m.test(source); } +const EXPORT_EQUALS = /^export = ([^;\n]+);$/m; + +// API Extractor ignores `export =` (it skips `ts.InternalSymbolName.ExportEquals` +// when collecting a module's exports), so a CommonJS entry point's report would +// be empty. For extraction only, point the entry point at a sibling copy of its +// `.d.ts` that uses `export default` instead - the published `.d.ts` keeps +// `export =`. The copy sits beside the original so that relative imports still +// resolve, and the caller deletes it once extraction is done. +// +// A report only includes exported declarations, and in an `export =` module +// every type is local - so the copy also exports its top-level declarations, +// which (as flow-api-translator emits only what the exports reference) are the +// types that make up the exported value's API. +// +// Once the entry point's declarations are analysed, API Extractor also throws +// ("Unable to analyze the export \"default\"") on any default import of +// another `export =` module, so the copy imports those with `import x = +// require()`, which it reports as an import rather than following. +function withExtractableExports( + entryPoint: EntryPoint, + createdFiles: Array, +): EntryPoint { + const source = fs.readFileSync(entryPoint.dtsPath, 'utf-8'); + const exportEquals = EXPORT_EQUALS.exec(source); + if (exportEquals == null) { + return entryPoint; + } + const exportedName = exportEquals[1]; + const dtsPath = entryPoint.dtsPath.replace( + /\.d\.ts$/, + `.api-extractor-${process.pid}.d.ts`, + ); + const extractable = source + .replace(EXPORT_EQUALS, 'export default $1;') + .replace( + /^(?:declare )?(?:type|interface|class|function|const|enum|namespace) (\w+)/gm, + (declaration, name: string) => + name === exportedName ? declaration : 'export ' + declaration, + ) + .replace( + /^import (type )?(\w+) from '(\.[^']+)';$/gm, + (statement, typeOnly: ?string, name: string, specifier: string) => { + const importedDts = + path.resolve(path.dirname(entryPoint.dtsPath), specifier) + '.d.ts'; + return fs.existsSync(importedDts) && + EXPORT_EQUALS.test(fs.readFileSync(importedDts, 'utf-8')) + ? `import ${typeOnly ?? ''}${name} = require('${specifier}');` + : statement; + }, + ); + fs.writeFileSync(dtsPath, extractable); + createdFiles.push(dtsPath); + return {...entryPoint, dtsPath}; +} + // Map a `./src/.js` exports target to its generated `types/.d.ts`. function sourceTargetToDts(packageDir: string, target: string): ?string { const match = /^\.\/src\/(.+)\.js$/.exec(target); @@ -269,7 +324,9 @@ function cleanReport(report: string): string { // the source `.d.ts` (parameter lists, type arguments) otherwise loses its // indentation. The options match the `.d.ts` generator's, so declarations wrap // as they do in the published definitions. -async function formatReport(report: string): Promise { +// +// Returns null for a report with no declarations. +async function formatReport(report: string): Promise { const match = report.match(/^([\s\S]*?```ts\n)([\s\S]*)(```\n?)$/); if (match == null) { throw new Error('Could not find the TypeScript code block in the report'); @@ -282,8 +339,11 @@ async function formatReport(report: string): Promise { printWidth: 200, requirePragma: false, }); + if (formatted.trim() === '') { + return null; + } // Keep the blank lines API Extractor leaves inside the fences. - return header + '\n' + (formatted === '' ? '' : formatted + '\n') + footer; + return header + '\n' + formatted + '\n' + footer; } // Build the API Extractor config for one entry point. The report is written to @@ -395,6 +455,7 @@ export async function generateApiSnapshots( const tempFolder = fs.mkdtempSync( path.join(os.tmpdir(), 'metro-api-snapshots-'), ); + const extractorInputFiles: Array = []; try { const packageDirs = fs @@ -420,7 +481,12 @@ export async function generateApiSnapshots( ); allSkipped.push(...skipped); if (entryPoints.length > 0) { - packages.push({packageDir, entryPoints}); + packages.push({ + packageDir, + entryPoints: entryPoints.map(entryPoint => + withExtractableExports(entryPoint, extractorInputFiles), + ), + }); } } @@ -478,6 +544,30 @@ export async function generateApiSnapshots( entryPoint.outputFileName, ); + if (snapshot == null) { + // Nothing to snapshot. Make sure no stale snapshot is left behind. + allSkipped.push( + `${entryPoint.packageName} "${entryPoint.exportKey}" (no exported API surface)`, + ); + if (!fs.existsSync(outputPath)) { + continue; + } + if (verifyOnly) { + errors.push({ + context, + error: new Error( + `Public API snapshot ${entryPoint.outputFileName} has no ` + + 'exported API surface and should be deleted. Run ' + + '`js1 build metro-ts-defs` (internal) or ' + + '`yarn run build-api-snapshots` (OSS) to update it.', + ), + }); + } else { + fs.rmSync(outputPath); + } + continue; + } + if (verifyOnly) { let existing = null; try { @@ -519,6 +609,9 @@ export async function generateApiSnapshots( } } finally { fs.rmSync(tempFolder, {recursive: true, force: true}); + for (const filePath of extractorInputFiles) { + fs.rmSync(filePath, {force: true}); + } } if (logger && allSkipped.length > 0) { diff --git a/scripts/generateTypeScriptDefinitions.js b/scripts/generateTypeScriptDefinitions.js index 837a6b39f5..ec42aac19b 100644 --- a/scripts/generateTypeScriptDefinitions.js +++ b/scripts/generateTypeScriptDefinitions.js @@ -56,7 +56,6 @@ const IGNORED_PATTERNS = [ 'packages/metro/src/cli.js', 'packages/**/third-party', 'packages/metro/src/integration_tests', - 'packages/metro-runtime/**/!(types*).js', ]; function isSourceTSDeclaration(filePath: string): boolean { @@ -82,6 +81,13 @@ export async function generateTsDefsForJsGlobs( fix: true, cwd: WORKSPACE_ROOT, overrideConfig: { + rules: { + // Off when linting the repo, because a declaration exported via + // `typeof` is reported as unused - but the generator relies on it to + // drop declarations left unused by translation, and acts only on + // "is defined but never used". + '@typescript-eslint/no-unused-vars': 'error', + }, parserOptions: { // typescript-eslint writes its "version of TypeScript which is not // officially supported" warning straight to `console.log`, bypassing @@ -138,6 +144,10 @@ export async function generateTsDefsForJsGlobs( // corresponding `.js` file, which is enforced to be a transparent // entry file that only registers Babel and re-exports the module. toProcess.set(filePath.replace(/\.flow\.js$/, '.js'), filePath); + } else if (filePath.endsWith('.js.flow')) { + // A .js.flow declaration file is the Flow source of truth for the + // adjacent .js file, which may be untyped (e.g. vendored code). + toProcess.set(filePath.replace(/\.flow$/, ''), filePath); } else if (filePath.endsWith('.js') && !toProcess.has(filePath)) { toProcess.set(filePath, filePath); } else if (isSourceTSDeclaration(filePath)) { @@ -157,23 +167,28 @@ export async function generateTsDefsForJsGlobs( absoluteTsFile: string, sourceFile: string, ) { - // Lint and fix the generated output + // Lint and fix the generated output. `lintText` returns no result at all + // for a path .eslintignore covers - `**/vendor/**`, for one - in which case + // the output stands unlinted. let [lintResult] = await linter.lintText(sourceContent, { filePath: absoluteTsFile, }); - let lintedOutput = lintResult.output ?? sourceContent; + let lintedOutput = lintResult?.output ?? sourceContent; const withoutUnusedGeneratedDeclarations = - removeUnusedGeneratedDeclarations(lintedOutput, lintResult.messages); + removeUnusedGeneratedDeclarations( + lintedOutput, + lintResult?.messages ?? [], + ); if (withoutUnusedGeneratedDeclarations !== lintedOutput) { [lintResult] = await linter.lintText(withoutUnusedGeneratedDeclarations, { filePath: absoluteTsFile, }); - lintedOutput = lintResult.output ?? withoutUnusedGeneratedDeclarations; + lintedOutput = lintResult?.output ?? withoutUnusedGeneratedDeclarations; } - if (logger && lintResult.messages.length > 0) { + if (logger && lintResult != null && lintResult.messages.length > 0) { logger.warn(sourceFile, lintResult.messages); } @@ -246,28 +261,19 @@ export async function generateTsDefsForJsGlobs( } try { const flowDef = await translateFlowToFlowDef( - source, + expandFlowCommentTypes(source), {}, {mungeUnderscores}, ); - if (flowDef.includes('declare module.exports')) { - errors.push({ - sourceFile, - error: new Error( - 'module.exports is not supported by TypeScript auto-generation', - ), - }); - } else { - const tsDef = await translateFlowDefToTSDef(flowDef); + const tsDef = await translateFlowDefToTSDef(flowDef); - const beforeLint = tsDef - // Fix up gap left in license header by removal of atflow - .replace('\n *\n *\n', '\n *\n') - // TypeScript has no analogue for __proto__: null - .replace(/__proto__: null[,;]?/g, ''); + const beforeLint = tsDef + // Fix up gap left in license header by removal of atflow + .replace('\n *\n *\n', '\n *\n') + // TypeScript has no analogue for __proto__: null + .replace(/__proto__: null[,;]?/g, ''); - await writeOutputFile(beforeLint, absoluteTsFile, sourceFile); - } + await writeOutputFile(beforeLint, absoluteTsFile, sourceFile); } catch (error) { errors.push({sourceFile, error}); } @@ -299,6 +305,20 @@ export async function generateTsDefsForJsGlobs( } } +// Files that run in Node without a Babel transform (e.g. worker entry points) +// carry their types in Flow comment syntax, which the translator doesn't see. +// Rewrite them as regular Flow syntax, per +// https://flow.org/en/docs/types/comments/: +// `/*: T */` -> `: T` +// `/*:: code */` and `/*flow-include code */` -> `code` +function expandFlowCommentTypes(source: string): string { + return source.replace( + /\/\*(::|flow-include|:)([\s\S]*?)\*\//g, + (_match, marker: string, content: string) => + marker === ':' ? ':' + content : content, + ); +} + function removeUnusedGeneratedDeclarations( sourceContent: string, messages: ReadonlyArray, diff --git a/yarn.lock b/yarn.lock index b04b498a55..5021fbbe3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3161,19 +3161,19 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flow-api-translator@0.331.0: - version "0.331.0" - resolved "https://registry.yarnpkg.com/flow-api-translator/-/flow-api-translator-0.331.0.tgz#4d8b2ca87d5bdf9bb2eb38b5572c9b49444fc246" - integrity sha512-ancC/o6L4I+2788TYZwJRCMXra5YkmP93YPktuSjaukK1UKggelK090omFgqVc1q5GoxOlmfzGaH9ZuosT9aQw== +flow-api-translator@0.332.0: + version "0.332.0" + resolved "https://registry.yarnpkg.com/flow-api-translator/-/flow-api-translator-0.332.0.tgz#863fb062b1a398e362e62a8b68ca54ed89221676" + integrity sha512-iyOfE5emx2xSbO+o9a9oU5CeZIKfFAPtbkhOGFtVcAcwlcznGYuSingFNiESl/lVvhgsOXoSNr5sDRKuUtRn2A== dependencies: "@babel/code-frame" "^7.16.0" "@typescript-eslint/parser" "8.38.0" "@typescript-eslint/visitor-keys" "8.38.0" flow-enums-runtime "^0.0.6" - flow-eslint "0.331.0" - flow-estree "0.331.0" - flow-parser "0.331.0" - flow-transform "0.331.0" + flow-eslint "0.332.0" + flow-estree "0.332.0" + flow-parser "0.332.0" + flow-transform "0.332.0" typescript "5.3.2" flow-bin@^0.332.0: @@ -3186,43 +3186,43 @@ flow-enums-runtime@^0.0.6: resolved "https://registry.yarnpkg.com/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz#5bb0cd1b0a3e471330f4d109039b7eba5cb3e787" integrity sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw== -flow-eslint@0.331.0: - version "0.331.0" - resolved "https://registry.yarnpkg.com/flow-eslint/-/flow-eslint-0.331.0.tgz#4fee8ed51eb924ff8cff45537fe7d3af53f6ddcd" - integrity sha512-6l1WAnvBMmrGljLLpnb+yuqbKbOcaHWQ6iz9M2QlUWXLgRUkxl1dQJfJ7dU/U3kvTjJyHCDt9vJiM9kf4BtA8Q== +flow-eslint@0.332.0: + version "0.332.0" + resolved "https://registry.yarnpkg.com/flow-eslint/-/flow-eslint-0.332.0.tgz#0a0d6124d8c9cb0c24cb3598fd9324a61fbdc8ff" + integrity sha512-b4XF0EyMAdV01hXYnHWErxwEbzLlApg5FOdTiPXeyC44U1tVfr6qESunBIP7tjIt3OYRnqMq8rxe/rgdKUiz3w== dependencies: esrecurse "^4.3.0" - flow-estree "0.331.0" - flow-parser "0.331.0" + flow-estree "0.332.0" + flow-parser "0.332.0" -flow-estree@0.331.0: - version "0.331.0" - resolved "https://registry.yarnpkg.com/flow-estree/-/flow-estree-0.331.0.tgz#4069f6b5e86337805413ee511d785753a015ceaa" - integrity sha512-FVLYkSL/ITb/QXBEQvNWPjPooRGswuUtGOwrH+puSlMDneNkxy640+FZsk/TfKL+b7Wbw/Fg9FTUv2bMQDpj2w== +flow-estree@0.332.0: + version "0.332.0" + resolved "https://registry.yarnpkg.com/flow-estree/-/flow-estree-0.332.0.tgz#88ea49c50732a8bf2cee179877a5dfaa05966861" + integrity sha512-nsh1Ty3l/a8xJLGWnrZ0VFfQXjIpA6Gm1LqovVyj8RR6gE9Hz/cDl951qNNUnZtJ08u+6V0SFrY66XAoct76Fg== flow-parser@0.*: version "0.163.0" resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.163.0.tgz#c2d5c77c57c53e8357016a837f3e3af04a0c87a6" integrity sha512-txOsBcZ1nKsKtoAsBFnArFdq0cnPCmEHI5S2jQIgU1ZO1xtWpg64TsZSRktsaOyoWLpAvVxPap4Qbdd/TCJ3FQ== -flow-parser@0.331.0: - version "0.331.0" - resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.331.0.tgz#d4b1ff0026930fb82788b02173449efd5e7b25c0" - integrity sha512-vEZcHIlnKeN8JSVtYFczj8sWJfHIJOkz+YG00O/oul/Bete+t1ZhTi+O6JRRLFYvGW+yJkZ0YgL16iStnbkUqQ== +flow-parser@0.332.0: + version "0.332.0" + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.332.0.tgz#c217c8cf68a957ef0600fb095a9a6cc0c6fbd450" + integrity sha512-Ly9ylvWknL6YGW2lq0stmDg8PfEh9qHxPoqlZLDjjH/9yd9Ik37m335a0WngsxQvDfrW8lwBPm++RmZm3e4lFQ== dependencies: - flow-estree "0.331.0" + flow-estree "0.332.0" -flow-transform@0.331.0: - version "0.331.0" - resolved "https://registry.yarnpkg.com/flow-transform/-/flow-transform-0.331.0.tgz#4ac9fd230192562141ab8d9bccd93690e2f349c4" - integrity sha512-R/YarAJMyFakA2P/Fn8H0tkhiJwGZ1+b9AT3h5u/iG7XD5wLUhMYcuu96gi4ZLyR5emX06QMhVf6CZVT1Ln+ZQ== +flow-transform@0.332.0: + version "0.332.0" + resolved "https://registry.yarnpkg.com/flow-transform/-/flow-transform-0.332.0.tgz#acd99be3e210226407eb9a72a04cf19e1a05061a" + integrity sha512-1GxvaV+XJao64nCgGwo3hCDYaz3kR5EqEALQICkzs0flg1jGOU5T60M8YRmlQSgbRDVofOc8V+jvksLuU5LCXw== dependencies: "@babel/code-frame" "^7.16.0" esquery "^1.4.0" flow-enums-runtime "^0.0.6" - flow-eslint "0.331.0" - flow-estree "0.331.0" - flow-parser "0.331.0" + flow-eslint "0.332.0" + flow-estree "0.332.0" + flow-parser "0.332.0" string-width "4.2.3" for-each@^0.3.3, for-each@^0.3.5: