diff --git a/docs/Caching.md b/docs/Caching.md index 9275efe5eb..bfda4f658e 100644 --- a/docs/Caching.md +++ b/docs/Caching.md @@ -20,7 +20,7 @@ The main option for configuring the Metro cache is [`cacheStores`](./Configurati Metro provides a number of built-in cache store implementations for use with the [`cacheStores`](./Configuration.md#cachestores) config option: * **`FileStore({root: string})`** will store cache entries as files under the directory specified by `root`. -* **`AutoCleanFileStore()`** is a `FileStore` that periodically cleans up old entries. It accepts the same options as `FileStore` plus the following: +* **`AutoCleanFileStore()`**
Deprecated
is a `FileStore` that periodically cleans up old entries. It accepts the same options as `FileStore` plus the following: * **`options.intervalMs: number`** is the time in milliseconds between cleanup attempts. Defaults to 10 minutes. * **`options.cleanupThresholdMs: number`** is the minimum time in milliseconds since the last modification of an entry before it can be deleted. Defaults to 3 days. * **`HttpStore(options)`** is a bare-bones remote cache client that reads (`GET`) and writes (`PUT`) compressed cache artifacts over HTTP or HTTPS. diff --git a/packages/metro-cache/package.json b/packages/metro-cache/package.json index de252800a7..1f1101dd47 100644 --- a/packages/metro-cache/package.json +++ b/packages/metro-cache/package.json @@ -23,7 +23,7 @@ "metro-core": "0.83.1" }, "devDependencies": { - "metro-memory-fs": "*" + "memfs": "^4.38.2" }, "license": "MIT", "engines": { diff --git a/packages/metro-cache/src/Cache.js b/packages/metro-cache/src/Cache.js index bb15396116..2c70a311e2 100644 --- a/packages/metro-cache/src/Cache.js +++ b/packages/metro-cache/src/Cache.js @@ -9,7 +9,7 @@ * @oncall react_native */ -import type {CacheStore} from 'metro-cache'; +import type {CacheStore} from './types'; import {Logger} from 'metro-core'; @@ -21,17 +21,15 @@ import {Logger} from 'metro-core'; * All get/set operations are logged via Metro's logger. */ export default class Cache { - _stores: $ReadOnlyArray>; - - _hits: WeakMap>; + +#stores: $ReadOnlyArray>; + +#hits: WeakMap> = new WeakMap(); constructor(stores: $ReadOnlyArray>) { - this._hits = new WeakMap(); - this._stores = stores; + this.#stores = stores; } async get(key: Buffer): Promise { - const stores = this._stores; + const stores = this.#stores; const length = stores.length; for (let i = 0; i < length; i++) { @@ -74,7 +72,7 @@ export default class Cache { ); if (value != null) { - this._hits.set(key, store); + this.#hits.set(key, store); return value; } @@ -85,8 +83,8 @@ export default class Cache { } async set(key: Buffer, value: T): Promise { - const stores = this._stores; - const stop = this._hits.get(key); + const stores = this.#stores; + const stop = this.#hits.get(key); const length = stores.length; const promises = []; const writeErrors = []; @@ -133,6 +131,6 @@ export default class Cache { // writing to the cache is a no-op and reading from the cache will always // return null. get isDisabled(): boolean { - return this._stores.length === 0; + return this.#stores.length === 0; } } diff --git a/packages/metro-cache/src/index.js b/packages/metro-cache/src/index.js index 46a756f8f4..df7c8f143e 100644 --- a/packages/metro-cache/src/index.js +++ b/packages/metro-cache/src/index.js @@ -29,6 +29,15 @@ export { stableHash, }; +export interface MetroCache { + +AutoCleanFileStore: typeof AutoCleanFileStore; + +Cache: typeof Cache; + +FileStore: typeof FileStore; + +HttpGetStore: typeof HttpGetStore; + +HttpStore: typeof HttpStore; + +stableHash: typeof stableHash; +} + /** * Backwards-compatibility with CommonJS consumers using interopRequireDefault. * Do not add to this list. @@ -42,4 +51,4 @@ export default { HttpGetStore, HttpStore, stableHash, -}; +} as MetroCache; diff --git a/packages/metro-cache/src/stores/AutoCleanFileStore.js b/packages/metro-cache/src/stores/AutoCleanFileStore.js index effde7df9b..8ebc34bcbc 100644 --- a/packages/metro-cache/src/stores/AutoCleanFileStore.js +++ b/packages/metro-cache/src/stores/AutoCleanFileStore.js @@ -4,8 +4,9 @@ * 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 - * @flow + * @oncall react_native */ import type {Options} from './FileStore'; @@ -14,90 +15,72 @@ import FileStore from './FileStore'; import fs from 'fs'; import path from 'path'; -type CleanOptions = { +type CleanOptions = $ReadOnly<{ ...Options, intervalMs?: number, cleanupThresholdMs?: number, - ... -}; - -type FileList = { - path: string, - stats: fs.Stats, - ... -}; - -// List all files in a directory in Node.js recursively in a synchronous fashion -const walkSync = function ( - dir: string, - filelist: Array, -): Array { - const files = fs.readdirSync(dir); - filelist = filelist || []; - files.forEach(function (file) { - const fullPath = path.join(dir, file); - const stats = fs.statSync(fullPath); - if (stats.isDirectory()) { - filelist = walkSync(fullPath + path.sep, filelist); - } else { - filelist.push({path: fullPath, stats}); - } - }); - return filelist; -}; - -function get(property: ?T, defaultValue: T): T { - if (property == null) { - return defaultValue; - } - - return property; -} +}>; /** - * A FileStore that cleans itself up in a given interval + * A FileStore that, at a given interval, stats the content of the cache root + * and deletes any file last modified a set threshold in the past. + * + * @deprecated This is not efficiently implemented and may cause significant + * redundant I/O when caches are large. Prefer your own cleanup scripts, or a + * custom Metro cache that uses watches, hooks get/set, and/or implements LRU. */ export default class AutoCleanFileStore extends FileStore { - _intervalMs: number; - _cleanupThresholdMs: number; - _root: string; + +#intervalMs: number; + +#cleanupThresholdMs: number; + +#root: string; constructor(opts: CleanOptions) { super({root: opts.root}); - this._intervalMs = get(opts.intervalMs, 10 * 60 * 1000); // 10 minutes - this._cleanupThresholdMs = get( - opts.cleanupThresholdMs, - 3 * 24 * 60 * 60 * 1000, // 3 days - ); + this.#root = opts.root; + this.#intervalMs = opts.intervalMs ?? 10 * 60 * 1000; // 10 minutes + this.#cleanupThresholdMs = + opts.cleanupThresholdMs ?? 3 * 24 * 60 * 60 * 1000; // 3 days - this._scheduleCleanup(); + this.#scheduleCleanup(); } - _scheduleCleanup() { - // $FlowFixMe[method-unbinding] added when improving typing for this parameters - setTimeout(this._doCleanup.bind(this), this._intervalMs); + #scheduleCleanup() { + setTimeout(() => this.#doCleanup(), this.#intervalMs); } - _doCleanup() { - const files = walkSync(this._root, []); + #doCleanup() { + const dirents = fs.readdirSync(this.#root, { + recursive: true, + withFileTypes: true, + }); let warned = false; - files.forEach(file => { - if (file.stats.mtimeMs < Date.now() - this._cleanupThresholdMs) { + const minModifiedTime = Date.now() - this.#cleanupThresholdMs; + dirents + .filter(dirent => dirent.isFile()) + .forEach(dirent => { + const absolutePath = path.join( + // $FlowFixMe[prop-missing] - dirent.parentPath added in Node 20.12 + dirent.parentPath, + dirent.name.toString(), + ); try { - fs.unlinkSync(file.path); + if (fs.statSync(absolutePath).mtimeMs < minModifiedTime) { + fs.unlinkSync(absolutePath); + } } catch (e) { if (!warned) { console.warn( - 'Problem cleaning up cache for ' + file.path + ': ' + e.message, + 'Problem cleaning up cache for ' + + absolutePath + + ': ' + + e.message, ); warned = true; } } - } - }); - - this._scheduleCleanup(); + }); + this.#scheduleCleanup(); } } diff --git a/packages/metro-cache/src/stores/FileStore.js b/packages/metro-cache/src/stores/FileStore.js index fc37b558da..3394eee0fe 100644 --- a/packages/metro-cache/src/stores/FileStore.js +++ b/packages/metro-cache/src/stores/FileStore.js @@ -4,8 +4,9 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @format * @flow + * @format + * @oncall react_native */ import fs from 'fs'; @@ -14,20 +15,20 @@ import path from 'path'; const NULL_BYTE = 0x00; const NULL_BYTE_BUFFER = Buffer.from([NULL_BYTE]); -export type Options = { +export type Options = $ReadOnly<{ root: string, -}; +}>; export default class FileStore { - _root: string; + +#root: string; constructor(options: Options) { - this._root = options.root; + this.#root = options.root; } async get(key: Buffer): Promise { try { - const data = await fs.promises.readFile(this._getFilePath(key)); + const data = await fs.promises.readFile(this.#getFilePath(key)); if (data[0] === NULL_BYTE) { return (data.slice(1): any); @@ -44,20 +45,20 @@ export default class FileStore { } async set(key: Buffer, value: T): Promise { - const filePath = this._getFilePath(key); + const filePath = this.#getFilePath(key); try { - await this._set(filePath, value); + await this.#set(filePath, value); } catch (err) { if (err.code === 'ENOENT') { fs.mkdirSync(path.dirname(filePath), {recursive: true}); - await this._set(filePath, value); + await this.#set(filePath, value); } else { throw err; } } } - async _set(filePath: string, value: T): Promise { + async #set(filePath: string, value: T): Promise { let content; if (value instanceof Buffer) { content = Buffer.concat([NULL_BYTE_BUFFER, value]); @@ -68,20 +69,20 @@ export default class FileStore { } clear() { - this._removeDirs(); + this.#removeDirs(); } - _getFilePath(key: Buffer): string { + #getFilePath(key: Buffer): string { return path.join( - this._root, + this.#root, key.slice(0, 1).toString('hex'), key.slice(1).toString('hex'), ); } - _removeDirs() { + #removeDirs() { for (let i = 0; i < 256; i++) { - fs.rmSync(path.join(this._root, ('0' + i.toString(16)).slice(-2)), { + fs.rmSync(path.join(this.#root, ('0' + i.toString(16)).slice(-2)), { force: true, recursive: true, }); diff --git a/packages/metro-cache/src/stores/HttpError.js b/packages/metro-cache/src/stores/HttpError.js index 4baf14a5d0..aaf9a974ef 100644 --- a/packages/metro-cache/src/stores/HttpError.js +++ b/packages/metro-cache/src/stores/HttpError.js @@ -4,8 +4,8 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @format * @flow strict + * @format */ export default class HttpError extends Error { diff --git a/packages/metro-cache/src/stores/HttpGetStore.js b/packages/metro-cache/src/stores/HttpGetStore.js index 7608b7801a..560e0071ab 100644 --- a/packages/metro-cache/src/stores/HttpGetStore.js +++ b/packages/metro-cache/src/stores/HttpGetStore.js @@ -4,24 +4,25 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @format * @flow strict-local + * @format + * @oncall react_native */ import type HttpError from './HttpError'; +import type {Options as HttpOptions} from './HttpStore'; import type NetworkError from './NetworkError'; -import type {HttpOptions} from 'metro-cache'; import HttpStore from './HttpStore'; import {Logger} from 'metro-core'; export default class HttpGetStore extends HttpStore { - _warned: boolean; + #warned: boolean; constructor(options: HttpOptions) { super(options); - this._warned = false; + this.#warned = false; } async get(key: Buffer): Promise { @@ -35,18 +36,16 @@ export default class HttpGetStore extends HttpStore { throw err; } - this._warn(err); + this.#warn(err); return null; } } - set(): Promise { - return Promise.resolve(undefined); - } + async set(_key: Buffer, _value: T): Promise {} - _warn(err: HttpError | NetworkError) { - if (!this._warned) { + #warn(err: HttpError | NetworkError) { + if (!this.#warned) { process.emitWarning( [ 'Could not connect to the HTTP cache.', @@ -60,7 +59,7 @@ export default class HttpGetStore extends HttpStore { log_entry_label: `${err.message} (${err.code})`, }), ); - this._warned = true; + this.#warned = true; } } } diff --git a/packages/metro-cache/src/stores/HttpStore.js b/packages/metro-cache/src/stores/HttpStore.js index 8481b2bf49..590a3eaa86 100644 --- a/packages/metro-cache/src/stores/HttpStore.js +++ b/packages/metro-cache/src/stores/HttpStore.js @@ -4,8 +4,9 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @format * @flow + * @format + * @oncall react_native */ import type {HttpsProxyAgentOptions} from 'https-proxy-agent'; @@ -78,19 +79,19 @@ export default class HttpStore { static HttpError: typeof HttpError = HttpError; static NetworkError: typeof NetworkError = NetworkError; - _getEndpoint: Endpoint; - _setEndpoint: Endpoint; + #getEndpoint: Endpoint; + #setEndpoint: Endpoint; constructor(options: Options) { - this._getEndpoint = this.createEndpointConfig( + this.#getEndpoint = this.#createEndpointConfig( options.getOptions != null ? options.getOptions : options, ); - this._setEndpoint = this.createEndpointConfig( + this.#setEndpoint = this.#createEndpointConfig( options.setOptions != null ? options.setOptions : options, ); } - createEndpointConfig(options: EndpointOptions): Endpoint { + #createEndpointConfig(options: EndpointOptions): Endpoint { const agentConfig: http$agentOptions & HttpsProxyAgentOptions = { family: options.family, keepAlive: true, @@ -151,32 +152,32 @@ export default class HttpStore { } get(key: Buffer): Promise { - return this.#withRetries(() => this.#getOnce(key), this._getEndpoint); + return this.#withRetries(() => this.#getOnce(key), this.#getEndpoint); } #getOnce(key: Buffer): Promise { return new Promise((resolve, reject) => { - let searchParamsString = this._getEndpoint.params.toString(); + let searchParamsString = this.#getEndpoint.params.toString(); if (searchParamsString != '') { searchParamsString = '?' + searchParamsString; } const options = { - agent: this._getEndpoint.agent, - headers: this._getEndpoint.headers, - host: this._getEndpoint.host, + agent: this.#getEndpoint.agent, + headers: this.#getEndpoint.headers, + host: this.#getEndpoint.host, method: 'GET', - path: `${this._getEndpoint.path}/${key.toString( + path: `${this.#getEndpoint.path}/${key.toString( 'hex', )}${searchParamsString}`, - port: this._getEndpoint.port, - timeout: this._getEndpoint.timeout, + port: this.#getEndpoint.port, + timeout: this.#getEndpoint.timeout, }; // $FlowFixMe[incompatible-type] /* $FlowFixMe[missing-local-annot](>=0.101.0 site=react_native_fb) This comment suppresses an * error found when Flow v0.101 was deployed. To see the error, delete * this comment and run Flow. */ - const req = this._getEndpoint.module.request(options, res => { + const req = this.#getEndpoint.module.request(options, res => { const code = res.statusCode; const data = []; @@ -187,9 +188,9 @@ export default class HttpStore { return; } else if ( code !== 200 && - !this._getEndpoint.additionalSuccessStatuses.has(code) + !this.#getEndpoint.additionalSuccessStatuses.has(code) ) { - if (this._getEndpoint.debug) { + if (this.#getEndpoint.debug) { res.on('data', chunk => { data.push(chunk); }); @@ -275,7 +276,7 @@ export default class HttpStore { set(key: Buffer, value: T): Promise { return this.#withRetries( () => this.#setOnce(key, value), - this._setEndpoint, + this.#setEndpoint, ); } @@ -283,35 +284,35 @@ export default class HttpStore { return new Promise((resolve, reject) => { const gzip = zlib.createGzip(ZLIB_OPTIONS); - let searchParamsString = this._setEndpoint.params.toString(); + let searchParamsString = this.#setEndpoint.params.toString(); if (searchParamsString != '') { searchParamsString = '?' + searchParamsString; } const options = { - agent: this._setEndpoint.agent, - headers: this._setEndpoint.headers, - host: this._setEndpoint.host, + agent: this.#setEndpoint.agent, + headers: this.#setEndpoint.headers, + host: this.#setEndpoint.host, method: 'PUT', - path: `${this._setEndpoint.path}/${key.toString( + path: `${this.#setEndpoint.path}/${key.toString( 'hex', )}${searchParamsString}`, - port: this._setEndpoint.port, - timeout: this._setEndpoint.timeout, + port: this.#setEndpoint.port, + timeout: this.#setEndpoint.timeout, }; // $FlowFixMe[incompatible-type] /* $FlowFixMe[missing-local-annot](>=0.101.0 site=react_native_fb) This comment suppresses an * error found when Flow v0.101 was deployed. To see the error, delete * this comment and run Flow. */ - const req = this._setEndpoint.module.request(options, res => { + const req = this.#setEndpoint.module.request(options, res => { const code = res.statusCode; if ( (code < 200 || code > 299) && - !this._setEndpoint.additionalSuccessStatuses.has(code) + !this.#setEndpoint.additionalSuccessStatuses.has(code) ) { - if (this._setEndpoint.debug) { + if (this.#setEndpoint.debug) { const data = []; res.on('data', chunk => { data.push(chunk); @@ -395,13 +396,13 @@ export default class HttpStore { return backOff(fn, { jitter: 'full', maxDelay: 30000, - numOfAttempts: this._getEndpoint.maxAttempts || Number.POSITIVE_INFINITY, + numOfAttempts: this.#getEndpoint.maxAttempts || Number.POSITIVE_INFINITY, retry: (e: Error) => { if (e instanceof HttpError) { - return this._getEndpoint.retryStatuses.has(e.code); + return this.#getEndpoint.retryStatuses.has(e.code); } return ( - e instanceof NetworkError && this._getEndpoint.retryNetworkErrors + e instanceof NetworkError && this.#getEndpoint.retryNetworkErrors ); }, }); diff --git a/packages/metro-cache/src/stores/NetworkError.js b/packages/metro-cache/src/stores/NetworkError.js index b3ec18cc16..4372b3ddc7 100644 --- a/packages/metro-cache/src/stores/NetworkError.js +++ b/packages/metro-cache/src/stores/NetworkError.js @@ -4,8 +4,8 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @format * @flow strict + * @format */ export default class NetworkError extends Error { diff --git a/packages/metro-cache/src/stores/__tests__/AutoCleanFileStore-test.js b/packages/metro-cache/src/stores/__tests__/AutoCleanFileStore-test.js index af314226b8..c4a9380ee2 100644 --- a/packages/metro-cache/src/stores/__tests__/AutoCleanFileStore-test.js +++ b/packages/metro-cache/src/stores/__tests__/AutoCleanFileStore-test.js @@ -9,7 +9,7 @@ * @oncall react_native */ -'use strict'; +import {memfs} from 'memfs'; describe('AutoCleanFileStore', () => { let AutoCleanFileStore; @@ -19,22 +19,23 @@ describe('AutoCleanFileStore', () => { jest .resetModules() .resetAllMocks() - .mock('fs', () => new (require('metro-memory-fs'))()); - + .mock('fs', () => memfs().fs); AutoCleanFileStore = require('../AutoCleanFileStore').default; fs = require('fs'); + jest.spyOn(fs, 'statSync'); jest.spyOn(fs, 'unlinkSync'); }); test('sets and writes into the cache', async () => { - // $FlowFixMe[underconstrained-implicit-instantiation] - const fileStore = new AutoCleanFileStore({ + const fileStore = new AutoCleanFileStore({ root: '/root', intervalMs: 49, - cleanupThresholdMs: 0, + cleanupThresholdMs: 90, }); const cache = Buffer.from([0xfa, 0xce, 0xb0, 0x0c]); + expect(fs.statSync).toHaveBeenCalledTimes(0); + await fileStore.set(cache, {foo: 42}); expect(await fileStore.get(cache)).toEqual({foo: 42}); @@ -43,17 +44,28 @@ describe('AutoCleanFileStore', () => { expect(await fileStore.get(cache)).toEqual({foo: 42}); + // And there should have been no cleanup + expect(fs.statSync).not.toHaveBeenCalled(); + // Run to 50ms so that we've exceeded the 49ms cleanup interval jest.advanceTimersByTime(20); - // mtime doesn't work very well in in-memory-store, so we couldn't test that - // functionality + expect(fs.statSync).toHaveBeenCalledTimes(1); + + // At 50ms we should have checked the file, but it's still fresh enough + expect(await fileStore.get(cache)).toEqual({foo: 42}); + expect(fs.unlinkSync).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(50); + + // After another 50ms, we should have checked the file again and deleted it + expect(fs.statSync).toHaveBeenCalledTimes(2); + expect(fs.unlinkSync).toHaveBeenCalledTimes(1); expect(await fileStore.get(cache)).toEqual(null); }); test('returns null when reading a non-existing file', async () => { - // $FlowFixMe[underconstrained-implicit-instantiation] - const fileStore = new AutoCleanFileStore({root: '/root'}); + const fileStore = new AutoCleanFileStore({root: '/root'}); const cache = Buffer.from([0xfa, 0xce, 0xb0, 0x0c]); expect(await fileStore.get(cache)).toEqual(null); diff --git a/packages/metro-cache/src/stores/__tests__/FileStore-test.js b/packages/metro-cache/src/stores/__tests__/FileStore-test.js index 027e7d3eb4..5f7a70a420 100644 --- a/packages/metro-cache/src/stores/__tests__/FileStore-test.js +++ b/packages/metro-cache/src/stores/__tests__/FileStore-test.js @@ -4,13 +4,12 @@ * 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 */ -'use strict'; - -const {dirname} = require('path'); +import {memfs} from 'memfs'; describe('FileStore', () => { let FileStore; @@ -20,7 +19,7 @@ describe('FileStore', () => { jest .resetModules() .resetAllMocks() - .mock('fs', () => new (require('metro-memory-fs'))()); + .mock('fs', () => memfs().fs); FileStore = require('../FileStore').default; fs = require('fs'); @@ -28,7 +27,7 @@ describe('FileStore', () => { }); test('sets and writes into the cache', async () => { - const fileStore = new FileStore({root: '/root'}); + const fileStore = new FileStore({root: '/root'}); const cache = Buffer.from([0xfa, 0xce, 0xb0, 0x0c]); await fileStore.set(cache, {foo: 42}); @@ -36,23 +35,22 @@ describe('FileStore', () => { }); test('returns null when reading a non-existing file', async () => { - const fileStore = new FileStore({root: '/root'}); + const fileStore = new FileStore({root: '/root'}); const cache = Buffer.from([0xfa, 0xce, 0xb0, 0x0c]); expect(await fileStore.get(cache)).toEqual(null); }); test('returns null when reading a empty file', async () => { - const fileStore = new FileStore({root: '/root'}); + const fileStore = new FileStore({root: '/root'}); const cache = Buffer.from([0xfa, 0xce, 0xb0, 0x0c]); - const filePath = fileStore._getFilePath(cache); - fs.mkdirSync(dirname(filePath), {recursive: true}); - fs.writeFileSync(filePath, ''); + jest.spyOn(fs.promises, 'readFile').mockImplementation(async () => ''); expect(await fileStore.get(cache)).toEqual(null); + expect(fs.promises.readFile).toHaveBeenCalledWith(expect.any(String)); }); test('writes into cache if folder is missing', async () => { - const fileStore = new FileStore({root: '/root'}); + const fileStore = new FileStore({root: '/root'}); const cache = Buffer.from([0xfa, 0xce, 0xb0, 0x0c]); const data = Buffer.from([0xca, 0xc4, 0xe5]); @@ -62,7 +60,7 @@ describe('FileStore', () => { }); test('reads and writes binary data', async () => { - const fileStore = new FileStore({root: '/root'}); + const fileStore = new FileStore({root: '/root'}); const cache = Buffer.from([0xfa, 0xce, 0xb0, 0x0c]); const data = Buffer.from([0xca, 0xc4, 0xe5]); diff --git a/packages/metro-cache/types/Cache.d.ts b/packages/metro-cache/types/Cache.d.ts index f97c4f41b7..f95aa554b3 100644 --- a/packages/metro-cache/types/Cache.d.ts +++ b/packages/metro-cache/types/Cache.d.ts @@ -8,8 +8,7 @@ * @oncall react_native */ -import {CacheStore} from './types'; - +import type {CacheStore} from './types'; /** * Main cache class. Receives an array of cache instances, and sequentially * traverses them to return a previously stored value. It also ensures setting @@ -17,9 +16,10 @@ import {CacheStore} from './types'; * * All get/set operations are logged via Metro's logger. */ -export default class Cache { +declare class Cache { constructor(stores: ReadonlyArray>); - get(key: Buffer): Promise; + get(key: Buffer): Promise; set(key: Buffer, value: T): Promise; get isDisabled(): boolean; } +export default Cache; diff --git a/packages/metro-cache/types/index.d.ts b/packages/metro-cache/types/index.d.ts index 417518ded1..f676d9adb4 100644 --- a/packages/metro-cache/types/index.d.ts +++ b/packages/metro-cache/types/index.d.ts @@ -26,16 +26,14 @@ export { HttpStore, stableHash, }; - export interface MetroCache { - AutoCleanFileStore: typeof AutoCleanFileStore; - Cache: typeof Cache; - FileStore: typeof FileStore; - HttpGetStore: typeof HttpGetStore; - HttpStore: typeof HttpStore; - stableHash: typeof stableHash; + readonly AutoCleanFileStore: typeof AutoCleanFileStore; + readonly Cache: typeof Cache; + readonly FileStore: typeof FileStore; + readonly HttpGetStore: typeof HttpGetStore; + readonly HttpStore: typeof HttpStore; + readonly stableHash: typeof stableHash; } - /** * Backwards-compatibility with CommonJS consumers using interopRequireDefault. * Do not add to this list. @@ -43,4 +41,6 @@ export interface MetroCache { * @deprecated Default import from 'metro-cache' is deprecated, use named exports. */ declare const $$EXPORT_DEFAULT_DECLARATION$$: MetroCache; +declare type $$EXPORT_DEFAULT_DECLARATION$$ = + typeof $$EXPORT_DEFAULT_DECLARATION$$; export default $$EXPORT_DEFAULT_DECLARATION$$; diff --git a/packages/metro-cache/types/stableHash.d.ts b/packages/metro-cache/types/stableHash.d.ts index a50001f301..6e9a72292f 100644 --- a/packages/metro-cache/types/stableHash.d.ts +++ b/packages/metro-cache/types/stableHash.d.ts @@ -8,4 +8,5 @@ * @oncall react_native */ -export default function stableHash(value: unknown): Buffer; +declare function stableHash(value: unknown): Buffer; +export default stableHash; diff --git a/packages/metro-cache/types/stores/AutoCleanFileStore.d.ts b/packages/metro-cache/types/stores/AutoCleanFileStore.d.ts index 693b81a6f4..22aa4ab6bd 100644 --- a/packages/metro-cache/types/stores/AutoCleanFileStore.d.ts +++ b/packages/metro-cache/types/stores/AutoCleanFileStore.d.ts @@ -8,6 +8,25 @@ * @oncall react_native */ -import type FileStore from './FileStore'; +import type {Options} from './FileStore'; -export default class AutoCleanFileStore extends FileStore {} +import FileStore from './FileStore'; + +type CleanOptions = Readonly< + Omit & { + intervalMs?: number; + cleanupThresholdMs?: number; + } +>; +/** + * A FileStore that, at a given interval, stats the content of the cache root + * and deletes any file last modified a set threshold in the past. + * + * @deprecated This is not efficiently implemented and may cause significant + * redundant I/O when caches are large. Prefer your own cleanup scripts, or a + * custom Metro cache that uses watches, hooks get/set, and/or implements LRU. + */ +declare class AutoCleanFileStore extends FileStore { + constructor(opts: CleanOptions); +} +export default AutoCleanFileStore; diff --git a/packages/metro-cache/types/stores/FileStore.d.ts b/packages/metro-cache/types/stores/FileStore.d.ts index 238814a30f..8a62ac3d7a 100644 --- a/packages/metro-cache/types/stores/FileStore.d.ts +++ b/packages/metro-cache/types/stores/FileStore.d.ts @@ -8,13 +8,11 @@ * @oncall react_native */ -export interface Options { - root: string; -} - -export default class FileStore { +export type Options = Readonly<{root: string}>; +declare class FileStore { constructor(options: Options); - get(key: Buffer): Promise; + get(key: Buffer): Promise; set(key: Buffer, value: T): Promise; clear(): void; } +export default FileStore; diff --git a/packages/metro-cache/types/stores/HttpError.d.ts b/packages/metro-cache/types/stores/HttpError.d.ts new file mode 100644 index 0000000000..5837c66df6 --- /dev/null +++ b/packages/metro-cache/types/stores/HttpError.d.ts @@ -0,0 +1,14 @@ +/** + * 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 class HttpError extends Error { + code: number; + constructor(message: string, code: number); +} +export default HttpError; diff --git a/packages/metro-cache/types/stores/HttpGetStore.d.ts b/packages/metro-cache/types/stores/HttpGetStore.d.ts index 52c3a59e35..70eaa16e79 100644 --- a/packages/metro-cache/types/stores/HttpGetStore.d.ts +++ b/packages/metro-cache/types/stores/HttpGetStore.d.ts @@ -8,11 +8,13 @@ * @oncall react_native */ -import type {Options} from './HttpStore'; +import type {Options as HttpOptions} from './HttpStore'; -export default class HttpGetStore { - constructor(options: Options); - get(key: Buffer): Promise; - set(key: Buffer, value: T): Promise; - clear(): void; +import HttpStore from './HttpStore'; + +declare class HttpGetStore extends HttpStore { + constructor(options: HttpOptions); + get(key: Buffer): Promise; + set(_key: Buffer, _value: T): Promise; } +export default HttpGetStore; diff --git a/packages/metro-cache/types/stores/HttpStore.d.ts b/packages/metro-cache/types/stores/HttpStore.d.ts index ac1bd54f62..bfc235d597 100644 --- a/packages/metro-cache/types/stores/HttpStore.d.ts +++ b/packages/metro-cache/types/stores/HttpStore.d.ts @@ -8,18 +8,41 @@ * @oncall react_native */ -export interface Options { +import HttpError from './HttpError'; +import NetworkError from './NetworkError'; + +export type Options = + | EndpointOptions + | {getOptions: EndpointOptions; setOptions: EndpointOptions}; +type EndpointOptions = { endpoint: string; family?: 4 | 6; timeout?: number; key?: string | ReadonlyArray | Buffer | ReadonlyArray; cert?: string | ReadonlyArray | Buffer | ReadonlyArray; ca?: string | ReadonlyArray | Buffer | ReadonlyArray; -} - -export default class HttpStore { + params?: URLSearchParams; + headers?: {[$$Key$$: string]: string}; + additionalSuccessStatuses?: ReadonlyArray; + /** + * Whether to include additional debug information in error messages. + */ + debug?: boolean; + /** + * Retry configuration + */ + maxAttempts?: number; + retryNetworkErrors?: boolean; + retryStatuses?: ReadonlySet; + socketPath?: string; + proxy?: string; +}; +declare class HttpStore { + static HttpError: typeof HttpError; + static NetworkError: typeof NetworkError; constructor(options: Options); - get(key: Buffer): Promise; + get(key: Buffer): Promise; set(key: Buffer, value: T): Promise; clear(): void; } +export default HttpStore; diff --git a/packages/metro-cache/types/stores/NetworkError.d.ts b/packages/metro-cache/types/stores/NetworkError.d.ts new file mode 100644 index 0000000000..3984acfb7f --- /dev/null +++ b/packages/metro-cache/types/stores/NetworkError.d.ts @@ -0,0 +1,14 @@ +/** + * 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 class NetworkError extends Error { + code: string; + constructor(message: string, code: string); +} +export default NetworkError; diff --git a/packages/metro-cache/types/types.d.ts b/packages/metro-cache/types/types.d.ts index f54c7a912c..e747c9fd49 100644 --- a/packages/metro-cache/types/types.d.ts +++ b/packages/metro-cache/types/types.d.ts @@ -9,7 +9,8 @@ */ export interface CacheStore { - get(key: Buffer): T | undefined | Promise | Promise; + name?: string; + get(key: Buffer): (null | undefined | T) | Promise; set(key: Buffer, value: T): void | Promise; clear(): void | Promise; } diff --git a/packages/metro-config/src/types.js b/packages/metro-config/src/types.js index 8465242cfa..134c39edf3 100644 --- a/packages/metro-config/src/types.js +++ b/packages/metro-config/src/types.js @@ -10,8 +10,7 @@ */ import type {HandleFunction, Server} from 'connect'; -import type {CacheStore} from 'metro-cache'; -import typeof * as MetroCache from 'metro-cache'; +import type {CacheStore, MetroCache} from 'metro-cache'; import type {CacheManagerFactory} from 'metro-file-map'; import type {CustomResolver} from 'metro-resolver'; import type {JsTransformerConfig} from 'metro-transform-worker'; diff --git a/packages/metro-config/types/defaults/createModuleIdFactory.d.ts b/packages/metro-config/types/defaults/createModuleIdFactory.d.ts new file mode 100644 index 0000000000..e9570aa491 --- /dev/null +++ b/packages/metro-config/types/defaults/createModuleIdFactory.d.ts @@ -0,0 +1,12 @@ +/** + * 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 + */ + +declare function createModuleIdFactory(): (path: string) => number; +export default createModuleIdFactory; diff --git a/packages/metro-config/types/defaults/defaults.d.ts b/packages/metro-config/types/defaults/defaults.d.ts new file mode 100644 index 0000000000..d1f90bafb2 --- /dev/null +++ b/packages/metro-config/types/defaults/defaults.d.ts @@ -0,0 +1,30 @@ +/** + * 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 {RootPerfLogger} from '../types'; + +export {default as defaultCreateModuleIdFactory} from './createModuleIdFactory'; +export declare const assetExts: Array; +export declare type assetExts = typeof assetExts; +export declare const assetResolutions: Array; +export declare type assetResolutions = typeof assetResolutions; +export declare const sourceExts: Array; +export declare type sourceExts = typeof sourceExts; +export declare const additionalExts: Array; +export declare type additionalExts = typeof additionalExts; +export declare const moduleSystem: string; +export declare type moduleSystem = typeof moduleSystem; +export declare const platforms: Array; +export declare type platforms = typeof platforms; +export declare const DEFAULT_METRO_MINIFIER_PATH: 'metro-minify-terser'; +export declare type DEFAULT_METRO_MINIFIER_PATH = + typeof DEFAULT_METRO_MINIFIER_PATH; +export declare const noopPerfLoggerFactory: () => RootPerfLogger; +export declare type noopPerfLoggerFactory = typeof noopPerfLoggerFactory; diff --git a/packages/metro-config/types/defaults/exclusionList.d.ts b/packages/metro-config/types/defaults/exclusionList.d.ts new file mode 100644 index 0000000000..8c9a30b664 --- /dev/null +++ b/packages/metro-config/types/defaults/exclusionList.d.ts @@ -0,0 +1,14 @@ +/** + * 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 + */ + +declare function exclusionList( + additionalExclusions?: ReadonlyArray, +): RegExp; +export default exclusionList; diff --git a/packages/metro-config/types/defaults/getMaxWorkers.d.ts b/packages/metro-config/types/defaults/getMaxWorkers.d.ts new file mode 100644 index 0000000000..af248faded --- /dev/null +++ b/packages/metro-config/types/defaults/getMaxWorkers.d.ts @@ -0,0 +1,12 @@ +/** + * 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 + */ + +declare function getMaxWorkers(workers: null | undefined | number): number; +export default getMaxWorkers; diff --git a/packages/metro-config/types/defaults/index.d.ts b/packages/metro-config/types/defaults/index.d.ts index 197af1891e..972c1041da 100644 --- a/packages/metro-config/types/defaults/index.d.ts +++ b/packages/metro-config/types/defaults/index.d.ts @@ -10,10 +10,10 @@ import type {ConfigT} from '../types'; -interface getDefaultConfig { - (rootPath: string | null): Promise; - getDefaultValues: (rootPath: string | null) => ConfigT; -} - -declare const getDefaultConfig: getDefaultConfig; -export default getDefaultConfig; +declare const $$EXPORT_DEFAULT_DECLARATION$$: { + (rootPath?: string): Promise; + getDefaultValues: (rootPath?: string) => ConfigT; +}; +declare type $$EXPORT_DEFAULT_DECLARATION$$ = + typeof $$EXPORT_DEFAULT_DECLARATION$$; +export default $$EXPORT_DEFAULT_DECLARATION$$; diff --git a/packages/metro-config/types/defaults/validConfig.d.ts b/packages/metro-config/types/defaults/validConfig.d.ts new file mode 100644 index 0000000000..e08113e571 --- /dev/null +++ b/packages/metro-config/types/defaults/validConfig.d.ts @@ -0,0 +1,14 @@ +/** + * 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 {InputConfigT} from '../types'; + +declare function validConfig(): Promise; +export default validConfig; diff --git a/packages/metro-config/types/index.d.ts b/packages/metro-config/types/index.d.ts index 3641391c6b..b20564ec10 100644 --- a/packages/metro-config/types/index.d.ts +++ b/packages/metro-config/types/index.d.ts @@ -13,7 +13,6 @@ import getDefaultConfig from './defaults'; import {loadConfig, mergeConfig, resolveConfig} from './loadConfig'; export {getDefaultConfig, loadConfig, mergeConfig, resolveConfig}; - /** * Backwards-compatibility with CommonJS consumers using interopRequireDefault. * Do not add to this list. diff --git a/packages/metro-config/types/loadConfig.d.ts b/packages/metro-config/types/loadConfig.d.ts index ce30f2815d..d0415fd459 100644 --- a/packages/metro-config/types/loadConfig.d.ts +++ b/packages/metro-config/types/loadConfig.d.ts @@ -10,26 +10,33 @@ import type {ConfigT, InputConfigT, YargArguments} from './types'; -export interface CosmiConfigResult { +type ResolveConfigResult = { filepath: string; isEmpty: boolean; config: - | ((partialConfig: ConfigT) => Promise) - | ((partialConfig: ConfigT) => ConfigT) + | (($$PARAM_0$$: ConfigT) => Promise) + | (($$PARAM_0$$: ConfigT) => ConfigT) | InputConfigT; -} - -export function loadConfig( - argv?: YargArguments, - defaultConfigOverrides?: InputConfigT, -): Promise; - -export function resolveConfig( +}; +declare function resolveConfig( filePath?: string, cwd?: string, -): Promise; - -export function mergeConfig( - defaultConfig: InputConfigT, - ...configs: InputConfigT[] -): ConfigT; +): Promise; +declare function mergeConfig>( + defaultConfig: T, + ...configs: Array +): T; +/** + * Load the metro configuration from disk + * @param {object} argv Arguments coming from the CLI, can be empty + * @param {object} defaultConfigOverrides A configuration that can override the default config + * @return {object} Configuration returned + */ +declare function loadConfig( + argvInput?: YargArguments, + defaultConfigOverrides?: InputConfigT, +): Promise; +export declare function loadConfigFile( + absolutePath: string, +): Promise; +export {loadConfig, resolveConfig, mergeConfig}; diff --git a/packages/metro-config/types/types.d.ts b/packages/metro-config/types/types.d.ts index a5c84143c9..072b2537ce 100644 --- a/packages/metro-config/types/types.d.ts +++ b/packages/metro-config/types/types.d.ts @@ -10,6 +10,7 @@ import type {HandleFunction, Server} from 'connect'; import type {CacheStore, MetroCache} from 'metro-cache'; +import type {CacheManagerFactory} from 'metro-file-map'; import type {CustomResolver} from 'metro-resolver'; import type {JsTransformerConfig} from 'metro-transform-worker'; import type { @@ -21,59 +22,52 @@ import type { } from 'metro/private/DeltaBundler/types'; import type {Reporter} from 'metro/private/lib/reporting'; import type MetroServer from 'metro/private/Server'; +import type {IntermediateStackFrame} from 'metro/private/Server/symbolicate'; -export interface ExtraTransformOptions { - readonly preloadedModules: Readonly<{[path: string]: true}> | false; - readonly ramGroups: ReadonlyArray; - readonly transform: Readonly<{ - experimentalImportSupport: boolean; - inlineRequires: - | Readonly<{blockList: Readonly<{[path: string]: true}>}> +export type ExtraTransformOptions = Readonly<{ + preloadedModules?: Readonly<{[path: string]: true}> | false; + ramGroups?: ReadonlyArray; + transform?: Readonly<{ + experimentalImportSupport?: boolean; + inlineRequires?: + | Readonly<{blockList: Readonly<{[$$Key$$: string]: true}>}> | boolean; nonInlinedRequires?: ReadonlyArray; unstable_disableES6Transforms?: boolean; unstable_memoizeInlineRequires?: boolean; + unstable_nonMemoizedInlineRequires?: ReadonlyArray; }>; -} - -export interface GetTransformOptionsOpts { +}>; +export type GetTransformOptionsOpts = { dev: boolean; - hot: boolean; - platform?: string; -} - + /** + * @deprecated Always true + */ + hot: true; + platform: null | undefined | string; +}; export type GetTransformOptions = ( entryPoints: ReadonlyArray, options: GetTransformOptionsOpts, - getDependenciesOf: (filePath: string) => Promise, + getDependenciesOf: ($$PARAM_0$$: string) => Promise>, ) => Promise>; - export type Middleware = HandleFunction; - -export type PerfAnnotations = Partial<{ - string: {[key: string]: string}; - int: {[key: string]: number}; - double: {[key: string]: number}; - bool: {[key: string]: boolean}; - string_array: {[key: string]: string[]}; - int_array: {[key: string]: number[]}; - double_array: {[key: string]: number[]}; - bool_array: {[key: string]: boolean[]}; -}>; - -export type PerfLoggerPointOptions = Readonly<{ - /** - * The time this event point occurred, if it differs from the time the point was logged. - */ - timestamp?: number; +type PerfAnnotations = Partial<{ + string: Readonly<{[key: string]: string}>; + int: Readonly<{[key: string]: number}>; + double: Readonly<{[key: string]: number}>; + bool: Readonly<{[key: string]: boolean}>; + string_array: Readonly<{[key: string]: ReadonlyArray}>; + int_array: Readonly<{[key: string]: ReadonlyArray}>; + double_array: Readonly<{[key: string]: ReadonlyArray}>; + bool_array: Readonly<{[key: string]: ReadonlyArray}>; }>; - +type PerfLoggerPointOptions = Readonly<{timestamp?: number}>; export interface PerfLogger { point(name: string, opts?: PerfLoggerPointOptions): void; annotate(annotations: PerfAnnotations): void; subSpan(label: string): PerfLogger; } - export interface RootPerfLogger extends PerfLogger { start(opts?: PerfLoggerPointOptions): void; end( @@ -81,30 +75,25 @@ export interface RootPerfLogger extends PerfLogger { opts?: PerfLoggerPointOptions, ): void; } - -export type PerfLoggerFactoryOptions = Readonly<{ - key?: number; -}>; - +export type PerfLoggerFactoryOptions = Readonly<{key?: number}>; export type PerfLoggerFactory = ( type: 'START_UP' | 'BUNDLING_REQUEST' | 'HMR', opts?: PerfLoggerFactoryOptions, ) => RootPerfLogger; - -export interface ResolverConfigT { +type ResolverConfigT = { assetExts: ReadonlyArray; assetResolutions: ReadonlyArray; - blacklistRE?: RegExp | RegExp[]; - blockList: RegExp | RegExp[]; - dependencyExtractor?: string; + blacklistRE?: RegExp | Array; + blockList: RegExp | Array; disableHierarchicalLookup: boolean; - extraNodeModules: {[name: string]: string}; + dependencyExtractor: null | undefined | string; emptyModulePath: string; enableGlobalPackages: boolean; - hasteImplModulePath?: string; + extraNodeModules: {[name: string]: string}; + hasteImplModulePath: null | undefined | string; nodeModulesPaths: ReadonlyArray; platforms: ReadonlyArray; - resolveRequest?: CustomResolver; + resolveRequest: null | undefined | CustomResolver; resolverMainFields: ReadonlyArray; sourceExts: ReadonlyArray; unstable_conditionNames: ReadonlyArray; @@ -114,144 +103,161 @@ export interface ResolverConfigT { unstable_enablePackageExports: boolean; useWatchman: boolean; requireCycleIgnorePatterns: ReadonlyArray; -} - -export interface SerializerConfigT { +}; +type SerializerConfigT = { createModuleIdFactory: () => (path: string) => number; customSerializer: + | null + | undefined | (( entryPoint: string, preModules: ReadonlyArray, graph: ReadOnlyGraph, options: SerializerOptions, - ) => Promise) - | null; + ) => Promise); experimentalSerializerHook: ( graph: ReadOnlyGraph, delta: DeltaResult, ) => unknown; - getModulesRunBeforeMainModule: (entryFilePath: string) => string[]; - getPolyfills: (options: {platform: string | null}) => ReadonlyArray; + getModulesRunBeforeMainModule: (entryFilePath: string) => Array; + getPolyfills: ($$PARAM_0$$: { + platform: null | undefined | string; + }) => ReadonlyArray; getRunModuleStatement: ( moduleId: number | string, globalPrefix: string, ) => string; polyfillModuleNames: ReadonlyArray; processModuleFilter: (modules: Module) => boolean; - isThirdPartyModule: (module: {readonly path: string}) => boolean; -} - -export interface TransformerConfigT extends JsTransformerConfig { + isThirdPartyModule: (module: Readonly<{path: string}>) => boolean; +}; +type TransformerConfigT = Omit< + JsTransformerConfig, + keyof { + getTransformOptions: GetTransformOptions; + transformVariants: { + readonly [name: string]: Partial; + }; + publicPath: string; + unstable_workerThreads: boolean; + } +> & { getTransformOptions: GetTransformOptions; - transformVariants: Readonly<{[name: string]: Partial}>; + transformVariants: { + readonly [name: string]: Partial; + }; publicPath: string; -} - -export interface MetalConfigT { + unstable_workerThreads: boolean; +}; +type MetalConfigT = { cacheVersion: string; fileMapCacheDirectory?: string; - /** Deprecated, alias of fileMapCacheDirectory */ hasteMapCacheDirectory?: string; + unstable_fileMapCacheManagerFactory?: CacheManagerFactory; maxWorkers: number; - unstable_perfLoggerFactory?: PerfLoggerFactory | null; + unstable_perfLoggerFactory?: null | undefined | PerfLoggerFactory; projectRoot: string; stickyWorkers: boolean; transformerPath: string; reporter: Reporter; resetCache: boolean; watchFolders: ReadonlyArray; -} - -export interface ServerConfigT { +}; +type CacheStoresConfigT = ReadonlyArray>; +type ServerConfigT = { /** @deprecated */ enhanceMiddleware: ( - metroMiddleware: Middleware, - metroServer: MetroServer, + $$PARAM_0$$: Middleware, + $$PARAM_1$$: MetroServer, ) => Middleware | Server; forwardClientLogs: boolean; port: number; - rewriteRequestUrl: (url: string) => string; - unstable_serverRoot: string | null; + rewriteRequestUrl: ($$PARAM_0$$: string) => string; + unstable_serverRoot: null | undefined | string; useGlobalHotkey: boolean; verifyConnections: boolean; -} - -export interface SymbolicatorConfigT { - customizeFrame: (frame: { - readonly file?: string; - readonly lineNumber?: number; - readonly column?: number; - readonly methodName?: string; +}; +type SymbolicatorConfigT = { + customizeFrame: ($$PARAM_0$$: { + readonly file: null | undefined | string; + readonly lineNumber: null | undefined | number; + readonly column: null | undefined | number; + readonly methodName: null | undefined | string; }) => - | {readonly collapse?: boolean} - | undefined - | Promise<{readonly collapse?: boolean}> - | Promise; -} - -export interface WatcherConfigT { + | (null | undefined | {readonly collapse?: boolean}) + | Promise; + customizeStack: ( + $$PARAM_0$$: Array, + $$PARAM_1$$: unknown, + ) => Array | Promise>; +}; +type WatcherConfigT = { additionalExts: ReadonlyArray; - watchman: { - deferStates: ReadonlyArray; - }; - healthCheck: { + healthCheck: Readonly<{ enabled: boolean; interval: number; timeout: number; filePrefix: string; - }; - unstable_autoSaveCache: { - enabled: boolean; - debounceMs?: number; - }; -} - -export interface WatcherInputConfigT - extends Partial< - Omit - > { - healthCheck?: Partial; - unstable_autoSaveCache?: Partial; -} - -export interface InputConfigT extends Partial { - readonly cacheStores?: - | ReadonlyArray> - | ((metroCache: MetroCache) => ReadonlyArray>); - readonly resolver?: Partial; - readonly server?: Partial; - readonly serializer?: Partial; - readonly symbolicator?: Partial; - readonly transformer?: Partial; - readonly watcher?: Partial; -} - + }>; + unstable_autoSaveCache: Readonly<{enabled: boolean; debounceMs?: number}>; + unstable_lazySha1: boolean; + unstable_workerThreads: boolean; + watchman: Readonly<{deferStates: ReadonlyArray}>; +}; +export type InputConfigT = Partial< + Readonly< + MetalConfigT & { + cacheStores: + | CacheStoresConfigT + | (($$PARAM_0$$: MetroCache) => CacheStoresConfigT); + resolver: Readonly>; + server: Readonly>; + serializer: Readonly>; + symbolicator: Readonly>; + transformer: Readonly>; + watcher: Partial< + Readonly< + Omit< + WatcherConfigT, + 'healthCheck' | 'unstable_autoSaveCache' | 'watchman' + > & { + healthCheck: Partial>; + unstable_autoSaveCache: Partial< + Readonly + >; + watchman: Partial>; + } + > + >; + } + > +>; export type MetroConfig = InputConfigT; - -export interface ConfigT extends Readonly { - readonly cacheStores: ReadonlyArray>; - readonly resolver: Readonly; - readonly server: Readonly; - readonly serializer: Readonly; - readonly symbolicator: Readonly; - readonly transformer: Readonly; - readonly watcher: Readonly; -} - -export interface YargArguments { +export type ConfigT = Readonly< + MetalConfigT & { + cacheStores: CacheStoresConfigT; + resolver: Readonly; + server: Readonly; + serializer: Readonly; + symbolicator: Readonly; + transformer: Readonly; + watcher: Readonly; + } +>; +export type YargArguments = Readonly<{ config?: string; cwd?: string; port?: string | number; host?: string; projectRoot?: string; - watchFolders?: string[]; - assetExts?: string[]; - sourceExts?: string[]; - platforms?: string[]; + watchFolders?: Array; + assetExts?: Array; + sourceExts?: Array; + platforms?: Array; 'max-workers'?: string | number; maxWorkers?: string | number; transformer?: string; 'reset-cache'?: boolean; resetCache?: boolean; verbose?: boolean; -} +}>; diff --git a/packages/metro-core/src/Terminal.js b/packages/metro-core/src/Terminal.js index f1b1b74049..3ccf5ba26d 100644 --- a/packages/metro-core/src/Terminal.js +++ b/packages/metro-core/src/Terminal.js @@ -89,32 +89,32 @@ function getTTYStream(stream: UnderlyingStream): ?tty.WriteStream { * single responsibility of handling status messages. */ export default class Terminal { - _logLines: Array; - _nextStatusStr: string; - _statusStr: string; - _stream: UnderlyingStream; - _ttyStream: ?tty.WriteStream; - _updatePromise: Promise | null; - _isUpdating: boolean; - _isPendingUpdate: boolean; - _shouldFlush: boolean; - _writeStatusThrottled: string => void; + #logLines: Array; + #nextStatusStr: string; + #statusStr: string; + #stream: UnderlyingStream; + #ttyStream: ?tty.WriteStream; + #updatePromise: Promise | null; + #isUpdating: boolean; + #isPendingUpdate: boolean; + #shouldFlush: boolean; + #writeStatusThrottled: string => void; constructor( stream: UnderlyingStream, {ttyPrint = true}: {ttyPrint?: boolean} = {}, ) { - this._logLines = []; - this._nextStatusStr = ''; - this._statusStr = ''; - this._stream = stream; - this._ttyStream = ttyPrint ? getTTYStream(stream) : null; - this._updatePromise = null; - this._isUpdating = false; - this._isPendingUpdate = false; - this._shouldFlush = false; - this._writeStatusThrottled = throttle( - status => this._stream.write(status), + this.#logLines = []; + this.#nextStatusStr = ''; + this.#statusStr = ''; + this.#stream = stream; + this.#ttyStream = ttyPrint ? getTTYStream(stream) : null; + this.#updatePromise = null; + this.#isUpdating = false; + this.#isPendingUpdate = false; + this.#shouldFlush = false; + this.#writeStatusThrottled = throttle( + status => this.#stream.write(status), 3500, ); } @@ -125,28 +125,28 @@ export default class Terminal { * If there are two updates scheduled, do nothing, as the second update will * take care of the latest status and log lines. */ - _scheduleUpdate() { - if (this._isUpdating) { - this._isPendingUpdate = true; + #scheduleUpdate() { + if (this.#isUpdating) { + this.#isPendingUpdate = true; return; } - this._isUpdating = true; - this._updatePromise = this._update().then(async () => { - while (this._isPendingUpdate) { - if (!this._shouldFlush) { + this.#isUpdating = true; + this.#updatePromise = this.#update().then(async () => { + while (this.#isPendingUpdate) { + if (!this.#shouldFlush) { await new Promise(resolve => setTimeout(resolve, 33)); } - this._isPendingUpdate = false; - await this._update(); + this.#isPendingUpdate = false; + await this.#update(); } - this._isUpdating = false; - this._shouldFlush = false; + this.#isUpdating = false; + this.#shouldFlush = false; }); } async waitForUpdates(): Promise { - await (this._updatePromise || Promise.resolve()); + await (this.#updatePromise || Promise.resolve()); } /** @@ -155,12 +155,12 @@ export default class Terminal { * update starts writing to stream after a delay. */ async flush(): Promise { - if (this._isUpdating) { - this._shouldFlush = true; + if (this.#isUpdating) { + this.#shouldFlush = true; } await this.waitForUpdates(); // $FlowFixMe[prop-missing] - this._writeStatusThrottled.flush(); + this.#writeStatusThrottled.flush(); } /** @@ -169,16 +169,16 @@ export default class Terminal { * `status()`) prevents us from repeatedly rewriting the status in case * `terminal.log()` is called several times. */ - async _update(): Promise { - const ttyStream = this._ttyStream; + async #update(): Promise { + const ttyStream = this.#ttyStream; - const nextStatusStr = this._nextStatusStr; - const statusStr = this._statusStr; - const logLines = this._logLines; + const nextStatusStr = this.#nextStatusStr; + const statusStr = this.#statusStr; + const logLines = this.#logLines; // reset these here to not have them changed while updating - this._statusStr = nextStatusStr; - this._logLines = []; + this.#statusStr = nextStatusStr; + this.#logLines = []; if (statusStr === nextStatusStr && logLines.length === 0) { return; @@ -192,15 +192,15 @@ export default class Terminal { } if (logLines.length > 0) { - await streamWrite(this._stream, logLines.join('\n') + '\n'); + await streamWrite(this.#stream, logLines.join('\n') + '\n'); } if (ttyStream) { if (nextStatusStr.length > 0) { - await streamWrite(this._stream, nextStatusStr + '\n'); + await streamWrite(this.#stream, nextStatusStr + '\n'); } } else { - this._writeStatusThrottled( + this.#writeStatusThrottled( nextStatusStr.length > 0 ? nextStatusStr + '\n' : '', ); } @@ -214,16 +214,16 @@ export default class Terminal { * file, then we don't care too much about having a progress bar. */ status(format: string, ...args: Array): string { - const {_nextStatusStr} = this; + const nextStatusStr = this.#nextStatusStr; const statusStr = util.format(format, ...args); - this._nextStatusStr = this._ttyStream - ? chunkString(statusStr, this._ttyStream.columns).join('\n') + this.#nextStatusStr = this.#ttyStream + ? chunkString(statusStr, this.#ttyStream.columns).join('\n') : statusStr; - this._scheduleUpdate(); + this.#scheduleUpdate(); - return _nextStatusStr; + return nextStatusStr; } /** @@ -232,8 +232,8 @@ export default class Terminal { * `console.log`. */ log(format: string, ...args: Array): void { - this._logLines.push(util.format(format, ...args)); - this._scheduleUpdate(); + this.#logLines.push(util.format(format, ...args)); + this.#scheduleUpdate(); } /** @@ -241,7 +241,7 @@ export default class Terminal { * status was the last one of a series of updates. */ persistStatus(): void { - this.log(this._nextStatusStr); - this._nextStatusStr = ''; + this.log(this.#nextStatusStr); + this.#nextStatusStr = ''; } } diff --git a/packages/metro-core/types/Logger.d.ts b/packages/metro-core/types/Logger.d.ts new file mode 100644 index 0000000000..6493ac4f88 --- /dev/null +++ b/packages/metro-core/types/Logger.d.ts @@ -0,0 +1,52 @@ +/** + * 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 {BundleOptions} from 'metro/private/shared/types'; + +export type ActionLogEntryData = { + action_name: string; + log_entry_label?: string; +}; +export type ActionStartLogEntry = { + action_name?: string; + action_phase?: string; + log_entry_label: string; + log_session?: string; + start_timestamp?: [number, number]; +}; +export type LogEntry = { + action_name?: string; + action_phase?: string; + action_result?: string; + duration_ms?: number; + entry_point?: string; + file_name?: string; + log_entry_label: string; + log_session?: string; + start_timestamp?: [number, number]; + outdated_modules?: number; + bundle_size?: number; + bundle_options?: BundleOptions; + bundle_hash?: string; + build_id?: string; + error_message?: string; + error_stack?: string; +}; +declare function on(event: string, handler: (logEntry: LogEntry) => void): void; +declare function createEntry(data: LogEntry | string): LogEntry; +declare function createActionStartEntry( + data: ActionLogEntryData | string, +): LogEntry; +declare function createActionEndEntry( + logEntry: ActionStartLogEntry, + error?: null | undefined | Error, +): LogEntry; +declare function log(logEntry: LogEntry): LogEntry; +export {on, createEntry, createActionStartEntry, createActionEndEntry, log}; diff --git a/packages/metro-core/types/Terminal.d.ts b/packages/metro-core/types/Terminal.d.ts index ad33e25b46..9c28a446b9 100644 --- a/packages/metro-core/types/Terminal.d.ts +++ b/packages/metro-core/types/Terminal.d.ts @@ -8,13 +8,44 @@ * @oncall react_native */ -import * as net from 'net'; -import * as stream from 'stream'; - -export type UnderlyingStream = net.Socket | stream.Writable; - -export class Terminal { - constructor(stream: UnderlyingStream); +type UnderlyingStream = net$Socket | stream$Writable; +/** + * We don't just print things to the console, sometimes we also want to show + * and update progress. This utility just ensures the output stays neat: no + * missing newlines, no mangled log lines. + * + * const terminal = Terminal.default; + * terminal.status('Updating... 38%'); + * terminal.log('warning: Something happened.'); + * terminal.status('Updating, done.'); + * terminal.persistStatus(); + * + * The final output: + * + * warning: Something happened. + * Updating, done. + * + * Without the status feature, we may get a mangled output: + * + * Updating... 38%warning: Something happened. + * Updating, done. + * + * This is meant to be user-readable and TTY-oriented. We use stdout by default + * because it's more about status information than diagnostics/errors (stderr). + * + * Do not add any higher-level functionality in this class such as "warning" and + * "error" printers, as it is not meant for formatting/reporting. It has the + * single responsibility of handling status messages. + */ +declare class Terminal { + constructor(stream: UnderlyingStream, $$PARAM_1$$?: {ttyPrint?: boolean}); + waitForUpdates(): Promise; + /** + * Useful for calling console/stdout directly after terminal logs + * Otherwise, you could end up with mangled output when the queued + * update starts writing to stream after a delay. + */ + flush(): Promise; /** * Shows some text that is meant to be overriden later. Return the previous * status that was shown and is no more. Calling `status()` with no argument @@ -22,17 +53,17 @@ export class Terminal { * non-interactive terminal: for example, if the output is redirected to a * file, then we don't care too much about having a progress bar. */ - status(format: string, ...args: unknown[]): string; + status(format: string, ...args: Array): string; /** * Similar to `console.log`, except it moves the status/progress text out of * the way correctly. In non-interactive terminals this is the same as * `console.log`. */ - log(format: string, ...args: unknown[]): void; + log(format: string, ...args: Array): void; /** * Log the current status and start from scratch. This is useful if the last * status was the last one of a series of updates. */ persistStatus(): void; - flush(): void; } +export default Terminal; diff --git a/packages/metro-core/types/canonicalize.d.ts b/packages/metro-core/types/canonicalize.d.ts new file mode 100644 index 0000000000..6edfe16ad2 --- /dev/null +++ b/packages/metro-core/types/canonicalize.d.ts @@ -0,0 +1,12 @@ +/** + * 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 + */ + +declare function canonicalize(key: string, value: unknown): unknown; +export default canonicalize; diff --git a/packages/metro-core/types/errors.d.ts b/packages/metro-core/types/errors.d.ts new file mode 100644 index 0000000000..55b7f5f7df --- /dev/null +++ b/packages/metro-core/types/errors.d.ts @@ -0,0 +1,14 @@ +/** + * 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 AmbiguousModuleResolutionError from './errors/AmbiguousModuleResolutionError'; +import PackageResolutionError from './errors/PackageResolutionError'; + +export {AmbiguousModuleResolutionError, PackageResolutionError}; diff --git a/packages/metro-core/types/errors/AmbiguousModuleResolutionError.d.ts b/packages/metro-core/types/errors/AmbiguousModuleResolutionError.d.ts new file mode 100644 index 0000000000..1966412a61 --- /dev/null +++ b/packages/metro-core/types/errors/AmbiguousModuleResolutionError.d.ts @@ -0,0 +1,21 @@ +/** + * 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 {DuplicateHasteCandidatesError} from 'metro-file-map'; + +declare class AmbiguousModuleResolutionError extends Error { + fromModulePath: string; + hasteError: DuplicateHasteCandidatesError; + constructor( + fromModulePath: string, + hasteError: DuplicateHasteCandidatesError, + ); +} +export default AmbiguousModuleResolutionError; diff --git a/packages/metro-core/types/errors/PackageResolutionError.d.ts b/packages/metro-core/types/errors/PackageResolutionError.d.ts new file mode 100644 index 0000000000..84d59f7417 --- /dev/null +++ b/packages/metro-core/types/errors/PackageResolutionError.d.ts @@ -0,0 +1,23 @@ +/** + * 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 {InvalidPackageError} from 'metro-resolver'; + +declare class PackageResolutionError extends Error { + originModulePath: string; + packageError: InvalidPackageError; + targetModuleName: string; + constructor(opts: { + readonly originModulePath: string; + readonly packageError: InvalidPackageError; + readonly targetModuleName: string; + }); +} +export default PackageResolutionError; diff --git a/packages/metro-core/types/index.d.ts b/packages/metro-core/types/index.d.ts index 70b94ce115..73f3d39071 100644 --- a/packages/metro-core/types/index.d.ts +++ b/packages/metro-core/types/index.d.ts @@ -8,10 +8,17 @@ * @oncall react_native */ -import {Terminal} from './Terminal'; - -export {Terminal}; +import AmbiguousModuleResolutionError from './errors/AmbiguousModuleResolutionError'; +import PackageResolutionError from './errors/PackageResolutionError'; +import * as Logger from './Logger'; +import Terminal from './Terminal'; +export { + AmbiguousModuleResolutionError, + Logger, + PackageResolutionError, + Terminal, +}; /** * Backwards-compatibility with CommonJS consumers using interopRequireDefault. * Do not add to this list. @@ -19,6 +26,9 @@ export {Terminal}; * @deprecated Default import from 'metro-core' is deprecated, use named exports. */ declare const $$EXPORT_DEFAULT_DECLARATION$$: { + AmbiguousModuleResolutionError: typeof AmbiguousModuleResolutionError; + Logger: typeof Logger; + PackageResolutionError: typeof PackageResolutionError; Terminal: typeof Terminal; }; declare type $$EXPORT_DEFAULT_DECLARATION$$ = diff --git a/packages/metro-resolver/types/PackageExportsResolve.d.ts b/packages/metro-resolver/types/PackageExportsResolve.d.ts new file mode 100644 index 0000000000..033ec0be26 --- /dev/null +++ b/packages/metro-resolver/types/PackageExportsResolve.d.ts @@ -0,0 +1,35 @@ +/** + * 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 {ExportsField, FileResolution, ResolutionContext} from './types'; +/** + * Resolve a package subpath based on the entry points defined in the package's + * "exports" field. If there is no match for the given subpath (which may be + * augmented by resolution of conditional exports for the passed `context`), + * throws a `PackagePathNotExportedError`. + * + * Implements modern package resolution behaviour based on the [Package Entry + * Points spec](https://nodejs.org/docs/latest-v19.x/api/packages.html#package-entry-points). + * + * @throws {InvalidPackageConfigurationError} Raised if configuration specified + * by `exportsField` is invalid. + * @throws {InvalidModuleSpecifierError} Raised if the resolved module specifier + * is invalid. + * @throws {PackagePathNotExportedError} Raised when the requested subpath is + * not exported. + */ +export declare function resolvePackageTargetFromExports( + context: ResolutionContext, + packagePath: string, + modulePath: string, + packageRelativePath: string, + exportsField: ExportsField, + platform: string | null, +): FileResolution; diff --git a/packages/metro-resolver/types/PackageImportsResolve.d.ts b/packages/metro-resolver/types/PackageImportsResolve.d.ts new file mode 100644 index 0000000000..124f531778 --- /dev/null +++ b/packages/metro-resolver/types/PackageImportsResolve.d.ts @@ -0,0 +1,29 @@ +/** + * 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 {ExportsLikeMap, FileResolution, ResolutionContext} from './types'; +/** + * Resolve a package subpath based on the entry points defined in the package's + * "imports" field. If there is no match for the given subpath (which may be + * augmented by resolution of conditional exports for the passed `context`), + * throws a `PackagePathNotExportedError`. + * + * Implementation of PACKAGE_IMPORTS_RESOLVE described in https://nodejs.org/api/esm.html + * + * @throws {InvalidPackageConfigurationError} Raised if configuration specified + * by `importsMap` is invalid. + */ +export declare function resolvePackageTargetFromImports( + context: ResolutionContext, + packagePath: string, + importPath: string, + importsMap: ExportsLikeMap, + platform: string | null, +): FileResolution; diff --git a/packages/metro-resolver/types/PackageResolve.d.ts b/packages/metro-resolver/types/PackageResolve.d.ts new file mode 100644 index 0000000000..c4e674ccd4 --- /dev/null +++ b/packages/metro-resolver/types/PackageResolve.d.ts @@ -0,0 +1,40 @@ +/** + * 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 {PackageInfo, ResolutionContext} from './types'; +/** + * Resolve the main entry point subpath 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 declare function getPackageEntryPoint( + context: ResolutionContext, + packageInfo: PackageInfo, + platform: string | null, +): string; +/** + * Get the resolved file path for the given import specifier based on any + * `package.json` rules. Returns `false` if the module should be + * [ignored](https://github.com/defunctzombie/package-browser-field-spec#ignore-a-module), + * and returns the original path if no `package.json` mapping is matched. Does + * not test file existence. + * + * Implements legacy (non-exports) package resolution behaviour based on the + * ["browser" field spec](https://github.com/defunctzombie/package-browser-field-spec). + */ +export declare function redirectModulePath( + context: Readonly<{ + getPackageForModule: ResolutionContext['getPackageForModule']; + mainFields: ResolutionContext['mainFields']; + originModulePath: ResolutionContext['originModulePath']; + }>, + modulePath: string, +): string | false; diff --git a/packages/metro-resolver/types/createDefaultContext.d.ts b/packages/metro-resolver/types/createDefaultContext.d.ts new file mode 100644 index 0000000000..86096cec2b --- /dev/null +++ b/packages/metro-resolver/types/createDefaultContext.d.ts @@ -0,0 +1,29 @@ +/** + * 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 {ResolutionContext} from './types'; +import type {TransformResultDependency} from 'metro/private/DeltaBundler/types'; + +type PartialContext = Readonly< + Omit< + ResolutionContext, + keyof {redirectModulePath?: ResolutionContext['redirectModulePath']} + > & {redirectModulePath?: ResolutionContext['redirectModulePath']} +>; +/** + * Helper used by the `metro` package to create the `ResolutionContext` object. + * As context values can be overridden by callers, this occurs externally to + * `resolve.js`. + */ +declare function createDefaultContext( + context: PartialContext, + dependency: TransformResultDependency, +): ResolutionContext; +export default createDefaultContext; diff --git a/packages/metro-resolver/types/errors/FailedToResolveNameError.d.ts b/packages/metro-resolver/types/errors/FailedToResolveNameError.d.ts new file mode 100644 index 0000000000..e114242cbc --- /dev/null +++ b/packages/metro-resolver/types/errors/FailedToResolveNameError.d.ts @@ -0,0 +1,19 @@ +/** + * 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 + */ + +declare class FailedToResolveNameError extends Error { + dirPaths: ReadonlyArray; + extraPaths: ReadonlyArray; + constructor( + dirPaths: ReadonlyArray, + extraPaths: ReadonlyArray, + ); +} +export default FailedToResolveNameError; diff --git a/packages/metro-resolver/types/errors/FailedToResolvePathError.d.ts b/packages/metro-resolver/types/errors/FailedToResolvePathError.d.ts new file mode 100644 index 0000000000..ec865d28be --- /dev/null +++ b/packages/metro-resolver/types/errors/FailedToResolvePathError.d.ts @@ -0,0 +1,17 @@ +/** + * 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 {FileAndDirCandidates} from '../types'; + +declare class FailedToResolvePathError extends Error { + candidates: FileAndDirCandidates; + constructor(candidates: FileAndDirCandidates); +} +export default FailedToResolvePathError; diff --git a/packages/metro-resolver/types/errors/FailedToResolveUnsupportedError.d.ts b/packages/metro-resolver/types/errors/FailedToResolveUnsupportedError.d.ts new file mode 100644 index 0000000000..2eec91a8da --- /dev/null +++ b/packages/metro-resolver/types/errors/FailedToResolveUnsupportedError.d.ts @@ -0,0 +1,14 @@ +/** + * 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 + */ + +declare class FailedToResolveUnsupportedError extends Error { + constructor(message: string); +} +export default FailedToResolveUnsupportedError; diff --git a/packages/metro-resolver/types/errors/InvalidPackageConfigurationError.d.ts b/packages/metro-resolver/types/errors/InvalidPackageConfigurationError.d.ts new file mode 100644 index 0000000000..a5e6995a60 --- /dev/null +++ b/packages/metro-resolver/types/errors/InvalidPackageConfigurationError.d.ts @@ -0,0 +1,25 @@ +/** + * 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 + */ + +/** + * Raised when a package contains an invalid `package.json` configuration. + */ +declare class InvalidPackageConfigurationError extends Error { + /** + * The description of the error cause. + */ + reason: string; + /** + * Absolute path of the package being resolved. + */ + packagePath: string; + constructor(opts: Readonly<{reason: string; packagePath: string}>); +} +export default InvalidPackageConfigurationError; diff --git a/packages/metro-resolver/types/errors/InvalidPackageError.d.ts b/packages/metro-resolver/types/errors/InvalidPackageError.d.ts new file mode 100644 index 0000000000..51b42b178b --- /dev/null +++ b/packages/metro-resolver/types/errors/InvalidPackageError.d.ts @@ -0,0 +1,42 @@ +/** + * 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 {FileCandidates} from '../types'; + +declare class InvalidPackageError extends Error { + /** + * The file candidates we tried to find to resolve the `main` field of the + * package. Ex. `/js/foo/beep(.js|.json)?` if `main` is specifying `./beep` + * as the entry point. + */ + fileCandidates: FileCandidates; + /** + * The 'index' file candidates we tried to find to resolve the `main` field of + * the package. Ex. `/js/foo/beep/index(.js|.json)?` if `main` is specifying + * `./beep` as the entry point. + */ + indexCandidates: FileCandidates; + /** + * The full path to the main module that was attempted. + */ + mainModulePath: string; + /** + * Full path the package we were trying to resolve. + * Ex. `/js/foo/package.json`. + */ + packageJsonPath: string; + constructor(opts: { + readonly fileCandidates: FileCandidates; + readonly indexCandidates: FileCandidates; + readonly mainModulePath: string; + readonly packageJsonPath: string; + }); +} +export default InvalidPackageError; diff --git a/packages/metro-resolver/types/errors/PackageImportNotResolvedError.d.ts b/packages/metro-resolver/types/errors/PackageImportNotResolvedError.d.ts new file mode 100644 index 0000000000..8ac2c5b863 --- /dev/null +++ b/packages/metro-resolver/types/errors/PackageImportNotResolvedError.d.ts @@ -0,0 +1,27 @@ +/** + * 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 + */ + +/** + * Raised when package imports do not define or permit a target subpath in the + * package for the given import specifier. + */ +declare class PackageImportNotResolvedError extends Error { + /** + * Either the import specifier read, or the absolute path of the module being + * resolved (used when import specifier is externally remapped). + */ + readonly importSpecifier: string; + /** + * The description of the error cause. + */ + readonly reason: string; + constructor(opts: Readonly<{importSpecifier: string; reason: string}>); +} +export default PackageImportNotResolvedError; diff --git a/packages/metro-resolver/types/errors/PackagePathNotExportedError.d.ts b/packages/metro-resolver/types/errors/PackagePathNotExportedError.d.ts new file mode 100644 index 0000000000..591e4297db --- /dev/null +++ b/packages/metro-resolver/types/errors/PackagePathNotExportedError.d.ts @@ -0,0 +1,16 @@ +/** + * 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 + */ + +/** + * Raised when package exports do not define or permit a target subpath in the + * package for the given module. + */ +declare class PackagePathNotExportedError extends Error {} +export default PackagePathNotExportedError; diff --git a/packages/metro-resolver/types/errors/formatFileCandidates.d.ts b/packages/metro-resolver/types/errors/formatFileCandidates.d.ts new file mode 100644 index 0000000000..48d518d0e6 --- /dev/null +++ b/packages/metro-resolver/types/errors/formatFileCandidates.d.ts @@ -0,0 +1,14 @@ +/** + * 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 {FileCandidates} from '../types'; + +declare function formatFileCandidates(candidates: FileCandidates): string; +export default formatFileCandidates; diff --git a/packages/metro-resolver/types/index.d.ts b/packages/metro-resolver/types/index.d.ts index 3f60b23551..d363312715 100644 --- a/packages/metro-resolver/types/index.d.ts +++ b/packages/metro-resolver/types/index.d.ts @@ -8,16 +8,36 @@ * @oncall react_native */ -export * from './types'; - -import {Resolution, ResolutionContext} from './types'; - -export function resolve( - context: ResolutionContext, - moduleName: string, - platform: string | null, -): Resolution; +export type { + AssetFileResolution, + CustomResolutionContext, + CustomResolver, + CustomResolverOptions, + DoesFileExist, + FileAndDirCandidates, + FileCandidates, + FileResolution, + FileSystemLookup, + ResolutionContext, + Resolution, + ResolveAsset, + Result, +} from './types'; +import FailedToResolveNameError from './errors/FailedToResolveNameError'; +import FailedToResolvePathError from './errors/FailedToResolvePathError'; +import FailedToResolveUnsupportedError from './errors/FailedToResolveUnsupportedError'; +import formatFileCandidates from './errors/formatFileCandidates'; +import InvalidPackageError from './errors/InvalidPackageError'; +import resolve from './resolve'; +export { + FailedToResolveNameError, + FailedToResolvePathError, + FailedToResolveUnsupportedError, + formatFileCandidates, + InvalidPackageError, + resolve, +}; /** * Backwards-compatibility with CommonJS consumers using interopRequireDefault. * Do not add to this list. @@ -25,6 +45,11 @@ export function resolve( * @deprecated Default import from 'metro-resolver' is deprecated, use named exports. */ declare const $$EXPORT_DEFAULT_DECLARATION$$: { + FailedToResolveNameError: typeof FailedToResolveNameError; + FailedToResolvePathError: typeof FailedToResolvePathError; + FailedToResolveUnsupportedError: typeof FailedToResolveUnsupportedError; + formatFileCandidates: typeof formatFileCandidates; + InvalidPackageError: typeof InvalidPackageError; resolve: typeof resolve; }; declare type $$EXPORT_DEFAULT_DECLARATION$$ = diff --git a/packages/metro-resolver/types/resolve.d.ts b/packages/metro-resolver/types/resolve.d.ts new file mode 100644 index 0000000000..574056539d --- /dev/null +++ b/packages/metro-resolver/types/resolve.d.ts @@ -0,0 +1,18 @@ +/** + * 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 {Resolution, ResolutionContext} from './types'; + +declare function resolve( + context: ResolutionContext, + moduleName: string, + platform: string | null, +): Resolution; +export default resolve; diff --git a/packages/metro-resolver/types/resolveAsset.d.ts b/packages/metro-resolver/types/resolveAsset.d.ts new file mode 100644 index 0000000000..e3b3ff4f0d --- /dev/null +++ b/packages/metro-resolver/types/resolveAsset.d.ts @@ -0,0 +1,21 @@ +/** + * 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 {AssetResolution, ResolutionContext} from './types'; +/** + * Resolve a file path as an asset. Returns the set of files found after + * expanding asset resolutions (e.g. `icon@2x.png`). Users may override this + * behaviour via `context.resolveAsset`. + */ +declare function resolveAsset( + context: ResolutionContext, + filePath: string, +): AssetResolution | null; +export default resolveAsset; diff --git a/packages/metro-resolver/types/types.d.ts b/packages/metro-resolver/types/types.d.ts index 0b2a82db34..315b88c945 100644 --- a/packages/metro-resolver/types/types.d.ts +++ b/packages/metro-resolver/types/types.d.ts @@ -8,14 +8,12 @@ * @oncall react_native */ -import {TransformResultDependency} from 'metro'; +import type {TransformResultDependency} from 'metro/private/DeltaBundler/types'; export type Result = | {readonly type: 'resolved'; readonly resolution: TResolution} | {readonly type: 'failed'; readonly candidates: TCandidates}; - -export type Resolution = FileResolution | Readonly<{type: 'empty'}>; - +export type Resolution = FileResolution | {readonly type: 'empty'}; export type SourceFileResolution = Readonly<{ type: 'sourceFile'; filePath: string; @@ -26,55 +24,65 @@ export type AssetResolution = Readonly<{ filePaths: AssetFileResolution; }>; export type FileResolution = AssetResolution | SourceFileResolution; - -export interface FileAndDirCandidates { - readonly dir: FileCandidates; - readonly file: FileCandidates; -} - +export type FileAndDirCandidates = { + readonly dir: null | undefined | FileCandidates; + readonly file: null | undefined | FileCandidates; +}; /** * This is a way to describe what files we tried to look for when resolving * a module name as file. This is mainly used for error reporting, so that * we can explain why we cannot resolve a module. */ export type FileCandidates = - // We only tried to resolve a specific asset. | {readonly type: 'asset'; readonly name: string} - // We attempted to resolve a name as being a source file (ex. JavaScript, - // JSON...), in which case there can be several extensions we tried, for - // example `/js/foo.ios.js`, `/js/foo.js`, etc. for a single prefix '/js/foo'. | { readonly type: 'sourceFile'; filePathPrefix: string; readonly candidateExts: ReadonlyArray; }; - -export type ExportMap = Readonly<{ - [subpathOrCondition: string]: ExportMap | string | null; +export type ExportsLikeMap = Readonly<{ + [subpathOrCondition: string]: string | ExportsLikeMap | null; }>; - -export interface PackageJson { - readonly name?: string; - readonly main?: string; - readonly exports?: string | ExportMap; -} - -export interface PackageInfo { - readonly packageJson: PackageJson; - readonly rootPath: string; -} - -export interface PackageForModule extends PackageInfo { - /* A system-separated subpath (with no './' prefix) that reflects the subpath - of the given candidate relative to the returned rootPath. */ - readonly packageRelativePath: string; -} - +/** "exports" mapping where values may be legacy Node.js <13.7 array format. */ +export type ExportMapWithFallbacks = Readonly<{ + [subpath: string]: + | ExportsLikeMap[keyof ExportsLikeMap] + | ExportValueWithFallback; +}>; +/** "exports" subpath value when in legacy Node.js <13.7 array format. */ +export type ExportValueWithFallback = + | ReadonlyArray + | ReadonlyArray>; +export type ExportsField = + | string + | ReadonlyArray + | ExportValueWithFallback + | ExportsLikeMap + | ExportMapWithFallbacks; +export type FlattenedExportMap = ReadonlyMap; +export type NormalizedExportsLikeMap = Map< + string, + null | string | ExportsLikeMap +>; +export type PackageJson = Readonly<{ + name?: string; + main?: string; + exports?: ExportsField; + imports?: ExportsLikeMap; +}>; +export type PackageInfo = Readonly<{ + packageJson: PackageJson; + rootPath: string; +}>; +export type PackageForModule = Readonly< + Omit & { + packageRelativePath: string; + } +>; /** * Check existence of a single file. */ export type DoesFileExist = (filePath: string) => boolean; -export type IsAssetFile = (fileName: string) => boolean; /** * Performs a lookup against an absolute or project-relative path to determine * whether it exists as a file or directory. Follows any symlinks, and returns @@ -83,7 +91,6 @@ export type IsAssetFile = (fileName: string) => boolean; export type FileSystemLookup = ( absoluteOrProjectRelativePath: string, ) => {exists: false} | {exists: true; type: 'f' | 'd'; realPath: string}; - /** * Given a directory path and the base asset name, return a list of all the * asset file names that match the given base name in that directory. Return @@ -94,27 +101,25 @@ export type ResolveAsset = ( dirPath: string, assetName: string, extension: string, -) => ReadonlyArray | undefined; - -export interface ResolutionContext { - readonly assetExts: ReadonlyArray; - readonly allowHaste: boolean; - readonly customResolverOptions: CustomResolverOptions; - readonly disableHierarchicalLookup: boolean; - +) => null | undefined | ReadonlyArray; +export type ResolutionContext = Readonly<{ + allowHaste: boolean; + assetExts: ReadonlySet; + customResolverOptions: CustomResolverOptions; + disableHierarchicalLookup: boolean; /** * Determine whether a regular file exists at the given path. * * @deprecated, prefer `fileSystemLookup` */ - readonly doesFileExist: DoesFileExist; - readonly extraNodeModules?: {[key: string]: string}; - + doesFileExist: DoesFileExist; + extraNodeModules: null | undefined | {[$$Key$$: string]: string}; + /** Is resolving for a development bundle. */ + dev: boolean; /** * Get the parsed contents of the specified `package.json` file. */ - readonly getPackage: (packageJsonPath: string) => PackageJson | null; - + getPackage: (packageJsonPath: string) => null | undefined | PackageJson; /** * Get the closest package scope, parsed `package.json` and relative subpath * for a given absolute candidate path (which need not exist), or null if @@ -122,17 +127,15 @@ export interface ResolutionContext { * * @deprecated See https://github.com/facebook/metro/commit/29c77bff31e2475a086bc3f04073f485da8f9ff0 */ - readonly getPackageForModule: ( + getPackageForModule: ( absoluteModulePath: string, - ) => PackageForModule | null; - + ) => null | undefined | PackageForModule; /** * The dependency descriptor, within the origin module, corresponding to the * current resolution request. This is provided for diagnostic purposes ONLY * and may not be used for resolution purposes. */ - readonly dependency?: TransformResultDependency; - + dependency?: TransformResultDependency; /** * Whether the dependency to be resolved was declared with an ESM import, * ("import x from 'y'" or "await import('z')"), or a CommonJS "require". @@ -142,66 +145,59 @@ export interface ResolutionContext { * Always equal to dependency.data.isESMImport where dependency is provided, * but may be used for resolution. */ - readonly isESMImport?: boolean; - + isESMImport?: boolean; /** * Synchonously returns information about a given absolute path, including * whether it exists, whether it is a file or directory, and its absolute * real path. */ - readonly fileSystemLookup: FileSystemLookup; - + fileSystemLookup: FileSystemLookup; /** * The ordered list of fields to read in `package.json` to resolve a main * entry point based on the "browser" field spec. */ - readonly mainFields: ReadonlyArray; - + mainFields: ReadonlyArray; /** * Full path of the module that is requiring or importing the module to be * resolved. This may not be the only place this dependency was found, * as resolutions can be cached. */ - readonly originModulePath: string; - - readonly nodeModulesPaths: ReadonlyArray; - readonly preferNativePlatform: boolean; - readonly resolveAsset: ResolveAsset; - readonly redirectModulePath: (modulePath: string) => string | false; - + originModulePath: string; + nodeModulesPaths: ReadonlyArray; + preferNativePlatform: boolean; + resolveAsset: ResolveAsset; + redirectModulePath: (modulePath: string) => string | false; /** * 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`. */ - readonly resolveHasteModule: (name: string) => string | undefined; - + resolveHasteModule: (name: string) => null | undefined | 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`. */ - readonly resolveHastePackage: (name: string) => string | undefined; - - readonly resolveRequest?: CustomResolver; - readonly sourceExts: ReadonlyArray; + resolveHastePackage: (name: string) => null | undefined | string; + resolveRequest?: null | undefined | CustomResolver; + sourceExts: ReadonlyArray; unstable_conditionNames: ReadonlyArray; unstable_conditionsByPlatform: Readonly<{ [platform: string]: ReadonlyArray; }>; unstable_enablePackageExports: boolean; unstable_logWarning: (message: string) => void; -} - -export interface CustomResolutionContext extends ResolutionContext { - readonly resolveRequest: CustomResolver; -} - +}>; +export type CustomResolutionContext = Readonly< + Omit & { + resolveRequest: CustomResolver; + } +>; export type CustomResolver = ( context: CustomResolutionContext, moduleName: string, platform: string | null, ) => Resolution; - -export type CustomResolverOptions = Readonly<{ - [option: string]: unknown; -}>; +export type CustomResolverOptions = { + __proto__: null; + readonly [$$Key$$: string]: unknown; +}; diff --git a/packages/metro-resolver/types/utils/isAssetFile.d.ts b/packages/metro-resolver/types/utils/isAssetFile.d.ts new file mode 100644 index 0000000000..f09d7bd8d8 --- /dev/null +++ b/packages/metro-resolver/types/utils/isAssetFile.d.ts @@ -0,0 +1,19 @@ +/** + * 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 + */ + +/** + * Determine if a file path should be considered an asset file based on the + * given `assetExts`. + */ +declare function isAssetFile( + filePath: string, + assetExts: ReadonlySet, +): boolean; +export default isAssetFile; diff --git a/packages/metro-resolver/types/utils/isSubpathDefinedInExportsLike.d.ts b/packages/metro-resolver/types/utils/isSubpathDefinedInExportsLike.d.ts new file mode 100644 index 0000000000..15f71e28f7 --- /dev/null +++ b/packages/metro-resolver/types/utils/isSubpathDefinedInExportsLike.d.ts @@ -0,0 +1,21 @@ +/** + * 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 + */ + +/** + * Identifies whether the given subpath is defined in the given "exports"-like + * mapping. Does not reduce exports conditions (therefore does not identify + * whether the subpath is mapped to a value). + */ +import type {NormalizedExportsLikeMap} from '../types'; + +export declare function isSubpathDefinedInExportsLike( + exportsLikeMap: NormalizedExportsLikeMap, + subpath: string, +): boolean; diff --git a/packages/metro-resolver/types/utils/matchSubpathFromExportsLike.d.ts b/packages/metro-resolver/types/utils/matchSubpathFromExportsLike.d.ts new file mode 100644 index 0000000000..8e3e95a8b8 --- /dev/null +++ b/packages/metro-resolver/types/utils/matchSubpathFromExportsLike.d.ts @@ -0,0 +1,24 @@ +/** + * 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 {NormalizedExportsLikeMap, ResolutionContext} from '../types'; +/** + * Get the mapped replacement for the given subpath. + * + * Implements modern package resolution behaviour based on the [Package Entry + * Points spec](https://nodejs.org/docs/latest-v19.x/api/packages.html#package-entry-points). + */ +export declare function matchSubpathFromExportsLike( + context: ResolutionContext, + subpath: string, + exportsLikeMap: NormalizedExportsLikeMap, + platform: string | null, + createConfigError: (reason: string) => Error, +): Readonly<{target: string | null; patternMatch: string | null}>; diff --git a/packages/metro-resolver/types/utils/matchSubpathPattern.d.ts b/packages/metro-resolver/types/utils/matchSubpathPattern.d.ts new file mode 100644 index 0000000000..b4d2e774e3 --- /dev/null +++ b/packages/metro-resolver/types/utils/matchSubpathPattern.d.ts @@ -0,0 +1,20 @@ +/** + * 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 + */ + +/** + * If a subpath pattern expands to the passed subpath, return the subpath match + * (value to substitute for '*'). Otherwise, return `null`. + * + * See https://nodejs.org/docs/latest-v19.x/api/packages.html#subpath-patterns. + */ +export declare function matchSubpathPattern( + subpathPattern: string, + subpath: string, +): string | null; diff --git a/packages/metro-resolver/types/utils/reduceExportsLikeMap.d.ts b/packages/metro-resolver/types/utils/reduceExportsLikeMap.d.ts new file mode 100644 index 0000000000..4add966aa9 --- /dev/null +++ b/packages/metro-resolver/types/utils/reduceExportsLikeMap.d.ts @@ -0,0 +1,21 @@ +/** + * 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 + */ + +/** + * Reduce an "exports"-like mapping to a flat subpath mapping after resolving + * conditional exports. + */ +import type {FlattenedExportMap, NormalizedExportsLikeMap} from '../types'; + +export declare function reduceExportsLikeMap( + exportsLikeMap: NormalizedExportsLikeMap, + conditionNames: ReadonlySet, + createConfigError: (reason: string) => Error, +): FlattenedExportMap; diff --git a/packages/metro-resolver/types/utils/toPosixPath.d.ts b/packages/metro-resolver/types/utils/toPosixPath.d.ts new file mode 100644 index 0000000000..2f87e35ad7 --- /dev/null +++ b/packages/metro-resolver/types/utils/toPosixPath.d.ts @@ -0,0 +1,16 @@ +/** + * 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 + */ + +/** + * Replace path separators in the passed string to coerce to a POSIX path. This + * is a no-op on POSIX systems. + */ +declare function toPosixPath(relativePathOrSpecifier: string): string; +export default toPosixPath; diff --git a/packages/metro-source-map/src/Consumer/constants.js b/packages/metro-source-map/src/Consumer/constants.js index 57277a730c..d4c2adb62a 100644 --- a/packages/metro-source-map/src/Consumer/constants.js +++ b/packages/metro-source-map/src/Consumer/constants.js @@ -24,7 +24,12 @@ export opaque type LookupBias = 'GREATEST_LOWER_BOUND' | 'LEAST_UPPER_BOUND'; const GREATEST_LOWER_BOUND: LookupBias = 'GREATEST_LOWER_BOUND'; const LEAST_UPPER_BOUND: LookupBias = 'LEAST_UPPER_BOUND'; -const EMPTY_POSITION = Object.freeze({ +const EMPTY_POSITION: $ReadOnly<{ + source: null, + name: null, + line: null, + column: null, +}> = Object.freeze({ source: null, name: null, line: null, diff --git a/packages/metro-source-map/src/encode.js b/packages/metro-source-map/src/encode.js index 3b0f415e54..a1da7dfb8b 100644 --- a/packages/metro-source-map/src/encode.js +++ b/packages/metro-source-map/src/encode.js @@ -100,7 +100,11 @@ function toVLQSigned(value: number) { * DON'T ADD MORE COMMENTS TO THIS FUNCTION TO KEEP ITS LENGTH SHORT ENOUGH FOR * V8 OPTIMIZATION! */ -function encode(value: number, buffer: Buffer, position: number): number { +export default function encode( + value: number, + buffer: Buffer, + position: number, +): number { let vlq = toVLQSigned(value); let digit; do { @@ -116,5 +120,3 @@ function encode(value: number, buffer: Buffer, position: number): number { return position; } - -export default encode; diff --git a/packages/metro-source-map/types/B64Builder.d.ts b/packages/metro-source-map/types/B64Builder.d.ts new file mode 100644 index 0000000000..b6402525ee --- /dev/null +++ b/packages/metro-source-map/types/B64Builder.d.ts @@ -0,0 +1,47 @@ +/** + * 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 + */ + +/** + * Efficient builder for base64 VLQ mappings strings. + * + * This class uses a buffer that is preallocated with one megabyte and is + * reallocated dynamically as needed, doubling its size. + * + * Encoding never creates any complex value types (strings, objects), and only + * writes character values to the buffer. + * + * For details about source map terminology and specification, check + * https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit + */ +declare class B64Builder { + buffer: Buffer; + pos: number; + hasSegment: boolean; + constructor(); + /** + * Adds `n` markers for generated lines to the mappings. + */ + markLines(n: number): this; + /** + * Starts a segment at the specified column offset in the current line. + */ + startSegment(column: number): this; + /** + * Appends a single number to the mappings. + */ + append(value: number): this; + /** + * Returns the string representation of the mappings. + */ + toString(): string; + _writeByte(byte: number): void; + _realloc(): void; +} +export default B64Builder; diff --git a/packages/metro-source-map/types/BundleBuilder.d.ts b/packages/metro-source-map/types/BundleBuilder.d.ts new file mode 100644 index 0000000000..d63cc0ad89 --- /dev/null +++ b/packages/metro-source-map/types/BundleBuilder.d.ts @@ -0,0 +1,43 @@ +/** + * 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 {IndexMap, IndexMapSection, MixedSourceMap} from './source-map'; +/** + * Builds a source-mapped bundle by concatenating strings and their + * corresponding source maps (if any). + * + * Usage: + * + * const builder = new BundleBuilder('bundle.js'); + * builder + * .append('foo\n', fooMap) + * .append('bar\n') + * // ... + * const code = builder.getCode(); + * const map = builder.getMap(); + */ +export declare class BundleBuilder { + _file: string; + _sections: Array; + _line: number; + _column: number; + _code: string; + _afterMappedContent: boolean; + constructor(file: string); + _pushMapSection(map: MixedSourceMap): void; + _endMappedContent(): void; + append(code: string, map: null | undefined | MixedSourceMap): this; + getMap(): MixedSourceMap; + getCode(): string; +} +export declare function createIndexMap( + file: string, + sections: Array, +): IndexMap; diff --git a/packages/metro-source-map/types/Consumer/AbstractConsumer.d.ts b/packages/metro-source-map/types/Consumer/AbstractConsumer.d.ts new file mode 100644 index 0000000000..c51c71a7d1 --- /dev/null +++ b/packages/metro-source-map/types/Consumer/AbstractConsumer.d.ts @@ -0,0 +1,37 @@ +/** + * 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 { + GeneratedPositionLookup, + IConsumer, + IterationOrder, + Mapping, + SourcePosition, +} from './types'; + +declare class AbstractConsumer implements IConsumer { + _sourceMap: {readonly file?: string}; + constructor(sourceMap: {readonly file?: string}); + originalPositionFor( + generatedPosition: GeneratedPositionLookup, + ): SourcePosition; + generatedMappings(): Iterable; + eachMapping( + callback: (mapping: Mapping) => unknown, + context?: unknown, + order?: IterationOrder, + ): void; + get file(): null | undefined | string; + sourceContentFor( + source: string, + nullOnMissing: true, + ): null | undefined | string; +} +export default AbstractConsumer; diff --git a/packages/metro-source-map/types/Consumer/DelegatingConsumer.d.ts b/packages/metro-source-map/types/Consumer/DelegatingConsumer.d.ts new file mode 100644 index 0000000000..40381a996f --- /dev/null +++ b/packages/metro-source-map/types/Consumer/DelegatingConsumer.d.ts @@ -0,0 +1,47 @@ +/** + * 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 {MixedSourceMap} from '../source-map'; +import type {LookupBias} from './constants.js'; +import type { + GeneratedPositionLookup, + IConsumer, + IterationOrder, + Mapping, + SourcePosition, +} from './types'; +/** + * A source map consumer that supports both "basic" and "indexed" source maps. + * Uses `MappingsConsumer` and `SectionsConsumer` under the hood (via + * `createConsumer`). + */ +declare class DelegatingConsumer implements IConsumer { + static readonly GENERATED_ORDER: IterationOrder; + static readonly ORIGINAL_ORDER: IterationOrder; + static readonly GREATEST_LOWER_BOUND: LookupBias; + static readonly LEAST_UPPER_BOUND: LookupBias; + _rootConsumer: IConsumer; + constructor(sourceMap: MixedSourceMap); + originalPositionFor( + generatedPosition: GeneratedPositionLookup, + ): SourcePosition; + generatedMappings(): Iterable; + eachMapping( + callback: (mapping: Mapping) => unknown, + context?: unknown, + order?: IterationOrder, + ): void; + get file(): null | undefined | string; + sourceContentFor( + source: string, + nullOnMissing: true, + ): null | undefined | string; +} +export default DelegatingConsumer; diff --git a/packages/metro-source-map/types/Consumer/MappingsConsumer.d.ts b/packages/metro-source-map/types/Consumer/MappingsConsumer.d.ts new file mode 100644 index 0000000000..dfadbaa5a8 --- /dev/null +++ b/packages/metro-source-map/types/Consumer/MappingsConsumer.d.ts @@ -0,0 +1,43 @@ +/** + * 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 {BasicSourceMap} from '../source-map'; +import type { + GeneratedPositionLookup, + IConsumer, + Mapping, + SourcePosition, +} from './types'; +import type {Number0} from 'ob1'; + +import AbstractConsumer from './AbstractConsumer'; +/** + * A source map consumer that supports "basic" source maps (that have a + * `mappings` field and no sections). + */ +declare class MappingsConsumer extends AbstractConsumer implements IConsumer { + _sourceMap: BasicSourceMap; + _decodedMappings: null | undefined | ReadonlyArray; + _normalizedSources: null | undefined | ReadonlyArray; + constructor(sourceMap: BasicSourceMap); + originalPositionFor( + generatedPosition: GeneratedPositionLookup, + ): SourcePosition; + _decodeMappings(): Generator; + _normalizeAndCacheSources(): ReadonlyArray; + _decodeAndCacheMappings(): ReadonlyArray; + generatedMappings(): Iterable; + _indexOfSource(source: string): null | undefined | Number0; + sourceContentFor( + source: string, + nullOnMissing: true, + ): null | undefined | string; +} +export default MappingsConsumer; diff --git a/packages/metro-source-map/types/Consumer/SectionsConsumer.d.ts b/packages/metro-source-map/types/Consumer/SectionsConsumer.d.ts new file mode 100644 index 0000000000..865199acd4 --- /dev/null +++ b/packages/metro-source-map/types/Consumer/SectionsConsumer.d.ts @@ -0,0 +1,40 @@ +/** + * 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 {IndexMap} from '../source-map'; +import type { + GeneratedOffset, + GeneratedPositionLookup, + IConsumer, + Mapping, + SourcePosition, +} from './types'; + +import AbstractConsumer from './AbstractConsumer'; +/** + * A source map consumer that supports "indexed" source maps (that have a + * `sections` field and no top-level mappings). + */ +declare class SectionsConsumer extends AbstractConsumer implements IConsumer { + _consumers: ReadonlyArray<[GeneratedOffset, IConsumer]>; + constructor(sourceMap: IndexMap); + originalPositionFor( + generatedPosition: GeneratedPositionLookup, + ): SourcePosition; + generatedMappings(): Iterable; + _consumerForPosition( + generatedPosition: GeneratedPositionLookup, + ): null | undefined | [GeneratedOffset, IConsumer]; + sourceContentFor( + source: string, + nullOnMissing: true, + ): null | undefined | string; +} +export default SectionsConsumer; diff --git a/packages/metro-source-map/types/Consumer/constants.d.ts b/packages/metro-source-map/types/Consumer/constants.d.ts new file mode 100644 index 0000000000..145c66c6c8 --- /dev/null +++ b/packages/metro-source-map/types/Consumer/constants.d.ts @@ -0,0 +1,39 @@ +/** + * 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 {Number0, Number1} from 'ob1'; + +declare const FIRST_COLUMN: Number0; +declare const FIRST_LINE: Number1; +export declare type IterationOrder = symbol & {__IterationOrder__: string}; +declare const GENERATED_ORDER: IterationOrder; +declare const ORIGINAL_ORDER: IterationOrder; +export declare type LookupBias = symbol & {__LookupBias__: string}; +declare const GREATEST_LOWER_BOUND: LookupBias; +declare const LEAST_UPPER_BOUND: LookupBias; +declare const EMPTY_POSITION: Readonly<{ + source: null; + name: null; + line: null; + column: null; +}>; +declare function iterationOrderToString(x: IterationOrder): string; +declare function lookupBiasToString(x: LookupBias): string; +export { + FIRST_COLUMN, + FIRST_LINE, + GENERATED_ORDER, + ORIGINAL_ORDER, + GREATEST_LOWER_BOUND, + LEAST_UPPER_BOUND, + EMPTY_POSITION, + iterationOrderToString, + lookupBiasToString, +}; diff --git a/packages/metro-source-map/types/Consumer/createConsumer.d.ts b/packages/metro-source-map/types/Consumer/createConsumer.d.ts new file mode 100644 index 0000000000..47e9856133 --- /dev/null +++ b/packages/metro-source-map/types/Consumer/createConsumer.d.ts @@ -0,0 +1,15 @@ +/** + * 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 {MixedSourceMap} from '../source-map'; +import type {IConsumer} from './types'; + +declare function createConsumer(sourceMap: MixedSourceMap): IConsumer; +export default createConsumer; diff --git a/packages/metro-source-map/types/Consumer/index.d.ts b/packages/metro-source-map/types/Consumer/index.d.ts new file mode 100644 index 0000000000..bb85e48e1a --- /dev/null +++ b/packages/metro-source-map/types/Consumer/index.d.ts @@ -0,0 +1,16 @@ +/** + * 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 DelegatingConsumer from './DelegatingConsumer'; + +declare const $$EXPORT_DEFAULT_DECLARATION$$: typeof DelegatingConsumer; +declare type $$EXPORT_DEFAULT_DECLARATION$$ = + typeof $$EXPORT_DEFAULT_DECLARATION$$; +export default $$EXPORT_DEFAULT_DECLARATION$$; diff --git a/packages/metro-source-map/types/Consumer/normalizeSourcePath.d.ts b/packages/metro-source-map/types/Consumer/normalizeSourcePath.d.ts new file mode 100644 index 0000000000..cd6deb84e4 --- /dev/null +++ b/packages/metro-source-map/types/Consumer/normalizeSourcePath.d.ts @@ -0,0 +1,15 @@ +/** + * 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 + */ + +declare function normalizeSourcePath( + sourceInput: string, + map: {readonly sourceRoot?: null | undefined | string}, +): string; +export default normalizeSourcePath; diff --git a/packages/metro-source-map/types/Consumer/positionMath.d.ts b/packages/metro-source-map/types/Consumer/positionMath.d.ts new file mode 100644 index 0000000000..6af98d4c34 --- /dev/null +++ b/packages/metro-source-map/types/Consumer/positionMath.d.ts @@ -0,0 +1,25 @@ +/** + * 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 {GeneratedOffset} from './types'; +import type {Number0, Number1} from 'ob1'; + +export declare function shiftPositionByOffset< + T extends { + readonly line: null | undefined | Number1; + readonly column: null | undefined | Number0; + }, +>(pos: T, offset: GeneratedOffset): T; +export declare function subtractOffsetFromPosition< + T extends { + readonly line: null | undefined | Number1; + readonly column: null | undefined | Number0; + }, +>(pos: T, offset: GeneratedOffset): T; diff --git a/packages/metro-source-map/types/Consumer/search.d.ts b/packages/metro-source-map/types/Consumer/search.d.ts new file mode 100644 index 0000000000..c23db5c75c --- /dev/null +++ b/packages/metro-source-map/types/Consumer/search.d.ts @@ -0,0 +1,15 @@ +/** + * 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 + */ + +export declare function greatestLowerBound( + elements: ReadonlyArray, + target: U, + comparator: ($$PARAM_0$$: U, $$PARAM_1$$: T) => number, +): null | undefined | number; diff --git a/packages/metro-source-map/types/Consumer/types.d.ts b/packages/metro-source-map/types/Consumer/types.d.ts new file mode 100644 index 0000000000..88132dab6f --- /dev/null +++ b/packages/metro-source-map/types/Consumer/types.d.ts @@ -0,0 +1,57 @@ +/** + * 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 {IterationOrder, LookupBias} from './constants'; +import type {Number0, Number1} from 'ob1'; + +export type {IterationOrder, LookupBias}; +export type GeneratedOffset = { + readonly lines: Number0; + readonly columns: Number0; +}; +export type SourcePosition = { + source: null | undefined | string; + line: null | undefined | Number1; + column: null | undefined | Number0; + name: null | undefined | string; +}; +export type GeneratedPosition = { + readonly line: Number1; + readonly column: Number0; +}; +export type GeneratedPositionLookup = { + readonly line: null | undefined | Number1; + readonly column: null | undefined | Number0; + readonly bias?: LookupBias; +}; +export type Mapping = Readonly<{ + source: null | undefined | string; + generatedLine: Number1; + generatedColumn: Number0; + originalLine: null | undefined | Number1; + originalColumn: null | undefined | Number0; + name: null | undefined | string; +}>; +export interface IConsumer { + originalPositionFor( + generatedPosition: GeneratedPositionLookup, + ): SourcePosition; + generatedMappings(): Iterable; + eachMapping( + callback: (mapping: Mapping) => unknown, + context?: unknown, + order?: IterationOrder, + ): void; + get file(): null | undefined | string; + sourceContentFor( + source: string, + nullOnMissing: true, + ): null | undefined | string; +} diff --git a/packages/metro-source-map/types/Generator.d.ts b/packages/metro-source-map/types/Generator.d.ts new file mode 100644 index 0000000000..2aa73e7761 --- /dev/null +++ b/packages/metro-source-map/types/Generator.d.ts @@ -0,0 +1,107 @@ +/** + * 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 { + BasicSourceMap, + FBSourceFunctionMap, + FBSourceMetadata, +} from './source-map'; + +import B64Builder from './B64Builder'; + +type FileFlags = Readonly<{addToIgnoreList?: boolean}>; +/** + * Generates a source map from raw mappings. + * + * Raw mappings are a set of 2, 4, or five elements: + * + * - line and column number in the generated source + * - line and column number in the original source + * - symbol name in the original source + * + * Mappings have to be passed in the order appearance in the generated source. + */ +declare class Generator { + builder: B64Builder; + last: { + generatedColumn: number; + generatedLine: number; + name: number; + source: number; + sourceColumn: number; + sourceLine: number; + }; + names: IndexedSet; + source: number; + sources: Array; + sourcesContent: Array; + x_facebook_sources: Array; + x_google_ignoreList: Array; + constructor(); + /** + * Mark the beginning of a new source file. + */ + startFile( + file: string, + code: string, + functionMap: null | undefined | FBSourceFunctionMap, + flags?: FileFlags, + ): void; + /** + * Mark the end of the current source file + */ + endFile(): void; + /** + * Adds a mapping for generated code without a corresponding source location. + */ + addSimpleMapping(generatedLine: number, generatedColumn: number): void; + /** + * Adds a mapping for generated code with a corresponding source location. + */ + addSourceMapping( + generatedLine: number, + generatedColumn: number, + sourceLine: number, + sourceColumn: number, + ): void; + /** + * Adds a mapping for code with a corresponding source location + symbol name. + */ + addNamedSourceMapping( + generatedLine: number, + generatedColumn: number, + sourceLine: number, + sourceColumn: number, + name: string, + ): void; + /** + * Return the source map as object. + */ + toMap(file?: string, options?: {excludeSource?: boolean}): BasicSourceMap; + /** + * Return the source map as string. + * + * This is ~2.5x faster than calling `JSON.stringify(generator.toMap())` + */ + toString(file?: string, options?: {excludeSource?: boolean}): string; + /** + * Determine whether we need to write the `x_facebook_sources` field. + * If the metadata is all `null`s, we can omit the field entirely. + */ + hasSourcesMetadata(): boolean; +} +export default Generator; +declare class IndexedSet { + map: Map; + nextIndex: number; + constructor(); + indexFor(x: string): number; + items(): Array; +} diff --git a/packages/metro-source-map/types/composeSourceMaps.d.ts b/packages/metro-source-map/types/composeSourceMaps.d.ts new file mode 100644 index 0000000000..bf5002f055 --- /dev/null +++ b/packages/metro-source-map/types/composeSourceMaps.d.ts @@ -0,0 +1,16 @@ +/** + * 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 {MixedSourceMap} from './source-map'; + +declare function composeSourceMaps( + maps: ReadonlyArray, +): MixedSourceMap; +export default composeSourceMaps; diff --git a/packages/metro-source-map/types/encode.d.ts b/packages/metro-source-map/types/encode.d.ts new file mode 100644 index 0000000000..900abed5cc --- /dev/null +++ b/packages/metro-source-map/types/encode.d.ts @@ -0,0 +1,25 @@ +/** + * Portions 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 + */ + +/** + * Encodes a number to base64 VLQ format and appends it to the passed-in buffer + * + * DON'T USE COMPOUND OPERATORS (eg `>>>=`) ON `let`-DECLARED VARIABLES! + * V8 WILL DEOPTIMIZE THIS FUNCTION AND MAP CREATION WILL BE 25% SLOWER! + * + * DON'T ADD MORE COMMENTS TO THIS FUNCTION TO KEEP ITS LENGTH SHORT ENOUGH FOR + * V8 OPTIMIZATION! + */ +declare function encode( + value: number, + buffer: Buffer, + position: number, +): number; +export default encode; diff --git a/packages/metro-source-map/types/generateFunctionMap.d.ts b/packages/metro-source-map/types/generateFunctionMap.d.ts new file mode 100644 index 0000000000..bdf39c7b77 --- /dev/null +++ b/packages/metro-source-map/types/generateFunctionMap.d.ts @@ -0,0 +1,44 @@ +/** + * 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 {FBSourceFunctionMap} from './source-map'; +import type {PluginObj} from '@babel/core'; + +type Position = {line: number; column: number}; +type RangeMapping = {name: string; start: Position}; +export type Context = {filename?: null | undefined | string}; +/** + * Generate a map of source positions to function names. The names are meant to + * describe the stack frame in an error trace and may contain more contextual + * information than just the actual name of the function. + * + * The output is encoded for use in a source map. For details about the format, + * see MappingEncoder below. + */ +declare function generateFunctionMap( + ast: BabelNode, + context?: Context, +): FBSourceFunctionMap; +/** + * Same as generateFunctionMap, but returns the raw array of mappings instead + * of encoding it for use in a source map. + * + * Lines are 1-based and columns are 0-based. + */ +declare function generateFunctionMappingsArray( + ast: BabelNode, + context?: Context, +): ReadonlyArray; +declare function functionMapBabelPlugin(): PluginObj; +export { + functionMapBabelPlugin, + generateFunctionMap, + generateFunctionMappingsArray, +}; diff --git a/packages/metro-source-map/types/source-map.d.ts b/packages/metro-source-map/types/source-map.d.ts index a855bdcd47..6d1c21f5a5 100644 --- a/packages/metro-source-map/types/source-map.d.ts +++ b/packages/metro-source-map/types/source-map.d.ts @@ -8,54 +8,143 @@ * @oncall react_native */ -export type GeneratedCodeMapping = [number, number]; -export type SourceMapping = [number, number, number, number]; -export type SourceMappingWithName = [number, number, number, number, string]; +import type {IConsumer} from './Consumer/types'; +import type {BabelSourceMapSegment} from '@babel/generator'; +import {BundleBuilder, createIndexMap} from './BundleBuilder'; +import composeSourceMaps from './composeSourceMaps'; +import Consumer from './Consumer'; +import normalizeSourcePath from './Consumer/normalizeSourcePath'; +import { + functionMapBabelPlugin, + generateFunctionMap, +} from './generateFunctionMap'; +import Generator from './Generator'; + +export type {IConsumer}; +type GeneratedCodeMapping = [number, number]; +type SourceMapping = [number, number, number, number]; +type SourceMappingWithName = [number, number, number, number, string]; export type MetroSourceMapSegmentTuple = | SourceMappingWithName | SourceMapping | GeneratedCodeMapping; - -export interface HermesFunctionOffsets { - [id: number]: ReadonlyArray; -} - -export type FBSourcesArray = ReadonlyArray; -export type FBSourceMetadata = [FBSourceFunctionMap | null]; -export interface FBSourceFunctionMap { +export type HermesFunctionOffsets = { + [$$Key$$: number]: ReadonlyArray; +}; +export type FBSourcesArray = ReadonlyArray; +export type FBSourceMetadata = [null | undefined | FBSourceFunctionMap]; +export type FBSourceFunctionMap = { readonly names: ReadonlyArray; readonly mappings: string; -} - -export interface FBSegmentMap { - [id: string]: MixedSourceMap; -} - -export interface BasicSourceMap { +}; +export type FBSegmentMap = {[id: string]: MixedSourceMap}; +export type BasicSourceMap = { readonly file?: string; readonly mappings: string; - readonly names: string[]; + readonly names: Array; readonly sourceRoot?: string; - readonly sources: string[]; - readonly sourcesContent?: Array; + readonly sources: Array; + readonly sourcesContent?: Array; readonly version: number; -} - -export interface IndexMapSection { + readonly x_facebook_offsets?: Array; + readonly x_metro_module_paths?: Array; + readonly x_facebook_sources?: FBSourcesArray; + readonly x_facebook_segments?: FBSegmentMap; + readonly x_hermes_function_offsets?: HermesFunctionOffsets; + readonly x_google_ignoreList?: Array; +}; +export type IndexMapSection = { map: IndexMap | BasicSourceMap; - offset: { - line: number; - column: number; - }; -} - -export interface IndexMap { + offset: {line: number; column: number}; +}; +export type IndexMap = { readonly file?: string; - readonly mappings?: unknown; - readonly sourcesContent?: unknown; - readonly sections: IndexMapSection[]; + readonly mappings?: void; + readonly sourcesContent?: void; + readonly sections: Array; readonly version: number; -} - + readonly x_facebook_offsets?: Array; + readonly x_metro_module_paths?: Array; + readonly x_facebook_sources?: void; + readonly x_facebook_segments?: FBSegmentMap; + readonly x_hermes_function_offsets?: HermesFunctionOffsets; + readonly x_google_ignoreList?: void; +}; export type MixedSourceMap = IndexMap | BasicSourceMap; +/** + * Creates a source map from modules with "raw mappings", i.e. an array of + * tuples with either 2, 4, or 5 elements: + * generated line, generated column, source line, source line, symbol name. + * Accepts an `offsetLines` argument in case modules' code is to be offset in + * the resulting bundle, e.g. by some prefix code. + */ +declare function fromRawMappings( + modules: ReadonlyArray<{ + readonly map: null | undefined | ReadonlyArray; + readonly functionMap: null | undefined | FBSourceFunctionMap; + readonly path: string; + readonly source: string; + readonly code: string; + readonly isIgnored: boolean; + readonly lineCount?: number; + }>, + offsetLines?: number, +): Generator; +declare function fromRawMappingsNonBlocking( + modules: ReadonlyArray<{ + readonly map: null | undefined | ReadonlyArray; + readonly functionMap: null | undefined | FBSourceFunctionMap; + readonly path: string; + readonly source: string; + readonly code: string; + readonly isIgnored: boolean; + readonly lineCount?: number; + }>, + offsetLines?: number, +): Promise; +/** + * Transforms a standard source map object into a Raw Mappings object, to be + * used across the bundler. + */ +declare function toBabelSegments( + sourceMap: BasicSourceMap, +): Array; +declare function toSegmentTuple( + mapping: BabelSourceMapSegment, +): MetroSourceMapSegmentTuple; +export { + BundleBuilder, + composeSourceMaps, + Consumer, + createIndexMap, + generateFunctionMap, + fromRawMappings, + fromRawMappingsNonBlocking, + functionMapBabelPlugin, + normalizeSourcePath, + toBabelSegments, + toSegmentTuple, +}; +/** + * Backwards-compatibility with CommonJS consumers using interopRequireDefault. + * Do not add to this list. + * + * @deprecated Default import from 'metro-source-map' is deprecated, use named exports. + */ +declare const $$EXPORT_DEFAULT_DECLARATION$$: { + BundleBuilder: typeof BundleBuilder; + composeSourceMaps: typeof composeSourceMaps; + Consumer: typeof Consumer; + createIndexMap: typeof createIndexMap; + generateFunctionMap: typeof generateFunctionMap; + fromRawMappings: typeof fromRawMappings; + fromRawMappingsNonBlocking: typeof fromRawMappingsNonBlocking; + functionMapBabelPlugin: typeof functionMapBabelPlugin; + normalizeSourcePath: typeof normalizeSourcePath; + toBabelSegments: typeof toBabelSegments; + toSegmentTuple: typeof toSegmentTuple; +}; +declare type $$EXPORT_DEFAULT_DECLARATION$$ = + typeof $$EXPORT_DEFAULT_DECLARATION$$; +export default $$EXPORT_DEFAULT_DECLARATION$$; diff --git a/packages/metro/types/DeltaBundler/Serializers/getExplodedSourceMap.d.ts b/packages/metro/types/DeltaBundler/Serializers/getExplodedSourceMap.d.ts new file mode 100644 index 0000000000..cf3bc0cbc4 --- /dev/null +++ b/packages/metro/types/DeltaBundler/Serializers/getExplodedSourceMap.d.ts @@ -0,0 +1,26 @@ +/** + * 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 {Module} from '../types'; +import type { + FBSourceFunctionMap, + MetroSourceMapSegmentTuple, +} from 'metro-source-map'; + +export type ExplodedSourceMap = ReadonlyArray<{ + readonly map: Array; + readonly firstLine1Based: number; + readonly functionMap: null | undefined | FBSourceFunctionMap; + readonly path: string; +}>; +export declare function getExplodedSourceMap( + modules: ReadonlyArray, + options: {readonly processModuleFilter: (module: Module) => boolean}, +): ExplodedSourceMap; diff --git a/packages/metro/types/Server/symbolicate.d.ts b/packages/metro/types/Server/symbolicate.d.ts new file mode 100644 index 0000000000..0d8616112e --- /dev/null +++ b/packages/metro/types/Server/symbolicate.d.ts @@ -0,0 +1,31 @@ +/** + * 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 {ExplodedSourceMap} from '../DeltaBundler/Serializers/getExplodedSourceMap'; +import type {ConfigT} from 'metro-config'; + +export type StackFrameInput = { + readonly file: null | undefined | string; + readonly lineNumber: null | undefined | number; + readonly column: null | undefined | number; + readonly methodName: null | undefined | string; +}; +export type IntermediateStackFrame = Omit< + StackFrameInput, + keyof {collapse?: boolean} +> & {collapse?: boolean}; +export type StackFrameOutput = Readonly; +declare function symbolicate( + stack: ReadonlyArray, + maps: Iterable<[string, ExplodedSourceMap]>, + config: ConfigT, + extraData: unknown, +): Promise>; +export default symbolicate; diff --git a/packages/ob1/types/ob1.d.ts b/packages/ob1/types/ob1.d.ts new file mode 100644 index 0000000000..e701489038 --- /dev/null +++ b/packages/ob1/types/ob1.d.ts @@ -0,0 +1,21 @@ +/** + * 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 + */ + +export declare type Number0 = symbol & {__Number0__: string}; +export declare type Number1 = symbol & {__Number1__: string}; +export declare function add(a: number, b: number): number; +export declare function sub(a: number, b: number): number; +export declare function get0(x: number): number; +export declare function get1(x: number): number; +export declare function add1(x: Number0 | number): Number1; +export declare function sub1(x: Number1): Number0; +export declare function neg(x: Number0): Number0; +export declare function add0(x: number): Number0; +export declare function inc(x: number): number; diff --git a/scripts/generateTypeScriptDefinitions.js b/scripts/generateTypeScriptDefinitions.js index afb0a8b978..9341927f6e 100644 --- a/scripts/generateTypeScriptDefinitions.js +++ b/scripts/generateTypeScriptDefinitions.js @@ -27,7 +27,16 @@ const WORKSPACE_ROOT = path.resolve(__dirname, '..'); const TYPES_DIR = 'types'; export const AUTO_GENERATED_PATTERNS: $ReadOnlyArray = [ - // TODO: Add globs + 'packages/metro-cache/**', + 'packages/metro-config/**', + 'packages/metro-core/**', + 'packages/metro-resolver/**', + 'packages/metro-source-map/**', + 'packages/ob1/**', + + // Dependencies of metro-config + 'packages/metro/src/Server/symbolicate.js', + 'packages/metro/src/DeltaBundler/Serializers/getExplodedSourceMap.js', ]; // Globs of paths for which we do not generate TypeScript definitions, @@ -39,6 +48,7 @@ const IGNORED_PATTERNS = [ '**/__fixtures__/**', '**/node_modules/**', 'packages/metro-babel-register/**', + 'packages/*/build/**', 'packages/metro/src/integration_tests/**', ]; diff --git a/scripts/jestFilter.js b/scripts/jestFilter.js index 1e295fcce7..bd3de0aef9 100644 --- a/scripts/jestFilter.js +++ b/scripts/jestFilter.js @@ -26,7 +26,6 @@ const BROKEN_ON_WINDOWS = [ 'packages/metro-file-map/src/crawlers/__tests__/node-test.js', // resolveModulePath failed - 'packages/metro-cache/src/stores/__tests__/FileStore-test.js', 'packages/metro-resolver/src/__tests__/assets-test.js', 'packages/metro-resolver/src/__tests__/platform-extensions-test.js', 'packages/metro-resolver/src/__tests__/symlinks-test.js', diff --git a/tsconfig.json b/tsconfig.json index 1e6315a369..439b371a41 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,8 @@ // Mappings for main entry points which are not "src/index.js". Required // only for this tsconfig - external consumers will read the "main" field // and find the adjacent typedef file. - "metro-source-map": ["./packages/metro-source-map/types/source-map.d.ts"] + "metro-source-map": ["./packages/metro-source-map/types/source-map.d.ts"], + "ob1": ["./packages/ob1/types"] } } } diff --git a/yarn.lock b/yarn.lock index 3e7f8dd099..7abf2311d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1112,6 +1112,50 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jsonjoy.com/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578" + integrity sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA== + +"@jsonjoy.com/buffers@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/buffers/-/buffers-1.0.0.tgz#ade6895b7d3883d70f87b5743efaa12c71dfef7a" + integrity sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q== + +"@jsonjoy.com/codegen@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz#5c23f796c47675f166d23b948cdb889184b93207" + integrity sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g== + +"@jsonjoy.com/json-pack@^1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-1.11.0.tgz#3d40d3d8042f5e9eeb005658a76b788e8ca84ac0" + integrity sha512-nLqSTAYwpk+5ZQIoVp7pfd/oSKNWlEdvTq2LzVA4r2wtWZg6v+5u0VgBOaDJuUfNOuw/4Ysq6glN5QKSrOCgrA== + dependencies: + "@jsonjoy.com/base64" "^1.1.2" + "@jsonjoy.com/buffers" "^1.0.0" + "@jsonjoy.com/codegen" "^1.0.0" + "@jsonjoy.com/json-pointer" "^1.0.1" + "@jsonjoy.com/util" "^1.9.0" + hyperdyperid "^1.2.0" + thingies "^2.5.0" + +"@jsonjoy.com/json-pointer@^1.0.1": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz#049cb530ac24e84cba08590c5e36b431c4843408" + integrity sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg== + dependencies: + "@jsonjoy.com/codegen" "^1.0.0" + "@jsonjoy.com/util" "^1.9.0" + +"@jsonjoy.com/util@^1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-1.9.0.tgz#7ee95586aed0a766b746cd8d8363e336c3c47c46" + integrity sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ== + dependencies: + "@jsonjoy.com/buffers" "^1.0.0" + "@jsonjoy.com/codegen" "^1.0.0" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -2940,6 +2984,11 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +glob-to-regex.js@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/glob-to-regex.js/-/glob-to-regex.js-1.0.1.tgz#f71cc9cb8441471a9318626160bc8a35e1306b21" + integrity sha512-CG/iEvgQqfzoVsMUbxSJcwbG2JwyZ3naEqPkeltwl0BSS8Bp83k3xlGms+0QdWFUAwV+uvo80wNswKF6FWEkKg== + glob@^7.0.3, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -3104,6 +3153,11 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +hyperdyperid@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz#59668d323ada92228d2a869d3e474d5a33b69e6b" + integrity sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A== + ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" @@ -4194,6 +4248,18 @@ makeerror@1.0.x: dependencies: tmpl "1.0.x" +memfs@^4.38.2: + version "4.38.2" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.38.2.tgz#e3a3a0362032c3ab7093cc7c179bd5fa8abc94c3" + integrity sha512-FpWsVHpAkoSh/LfY1BgAl72BVd374ooMRtDi2VqzBycX4XEfvC0XKACCe0C9VRZoYq5viuoyTv6lYXZ/Q7TrLQ== + dependencies: + "@jsonjoy.com/json-pack" "^1.11.0" + "@jsonjoy.com/util" "^1.9.0" + glob-to-regex.js "^1.0.1" + thingies "^2.5.0" + tree-dump "^1.0.3" + tslib "^2.0.0" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -5206,6 +5272,11 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +thingies@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/thingies/-/thingies-2.5.0.tgz#5f7b882c933b85989f8466b528a6247a6881e04f" + integrity sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw== + throat@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" @@ -5238,6 +5309,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +tree-dump@^1.0.3: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.1.0.tgz#ab29129169dc46004414f5a9d4a3c6e89f13e8a4" + integrity sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA== + ts-api-utils@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" @@ -5253,6 +5329,11 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" +tslib@^2.0.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tslib@^2.0.1: version "2.6.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410"