feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,12 @@
# \[Experimental\] Metro File Map
🚇 File system crawling, watching and mapping for [Metro](https://metrobundler.dev/).
Originally a fork of [`jest-haste-map`](https://github.com/facebook/jest/tree/main/packages/jest-haste-map).
This entire package should be considered "experimental" for the time being -
the API is considered internal and changes will not be semver-breaking.
If you need to rely on `metro-file-map` APIs directly please
[raise an issue](https://github.com/facebook/metro/issues/new) to discuss your
use case.

View File

@@ -0,0 +1 @@
repo_token: SIAeZjKYlHK74rbcFvNHMUzjRiMpflxve

View File

@@ -0,0 +1,11 @@
{
"env": {
"browser": true,
"node": true
},
"rules": {
"no-console": 0,
"no-empty": [1, { "allowEmptyCatch": true }]
},
"extends": "eslint:recommended"
}

View File

@@ -0,0 +1,9 @@
support
test
examples
example
*.sock
dist
yarn.lock
coverage
bower.json

View File

@@ -0,0 +1,14 @@
language: node_js
node_js:
- "6"
- "5"
- "4"
install:
- make node_modules
script:
- make lint
- make test
- make coveralls

View File

@@ -0,0 +1,362 @@
2.6.9 / 2017-09-22
==================
* remove ReDoS regexp in %o formatter (#504)
2.6.8 / 2017-05-18
==================
* Fix: Check for undefined on browser globals (#462, @marbemac)
2.6.7 / 2017-05-16
==================
* Fix: Update ms to 2.0.0 to fix regular expression denial of service vulnerability (#458, @hubdotcom)
* Fix: Inline extend function in node implementation (#452, @dougwilson)
* Docs: Fix typo (#455, @msasad)
2.6.5 / 2017-04-27
==================
* Fix: null reference check on window.documentElement.style.WebkitAppearance (#447, @thebigredgeek)
* Misc: clean up browser reference checks (#447, @thebigredgeek)
* Misc: add npm-debug.log to .gitignore (@thebigredgeek)
2.6.4 / 2017-04-20
==================
* Fix: bug that would occure if process.env.DEBUG is a non-string value. (#444, @LucianBuzzo)
* Chore: ignore bower.json in npm installations. (#437, @joaovieira)
* Misc: update "ms" to v0.7.3 (@tootallnate)
2.6.3 / 2017-03-13
==================
* Fix: Electron reference to `process.env.DEBUG` (#431, @paulcbetts)
* Docs: Changelog fix (@thebigredgeek)
2.6.2 / 2017-03-10
==================
* Fix: DEBUG_MAX_ARRAY_LENGTH (#420, @slavaGanzin)
* Docs: Add backers and sponsors from Open Collective (#422, @piamancini)
* Docs: Add Slackin invite badge (@tootallnate)
2.6.1 / 2017-02-10
==================
* Fix: Module's `export default` syntax fix for IE8 `Expected identifier` error
* Fix: Whitelist DEBUG_FD for values 1 and 2 only (#415, @pi0)
* Fix: IE8 "Expected identifier" error (#414, @vgoma)
* Fix: Namespaces would not disable once enabled (#409, @musikov)
2.6.0 / 2016-12-28
==================
* Fix: added better null pointer checks for browser useColors (@thebigredgeek)
* Improvement: removed explicit `window.debug` export (#404, @tootallnate)
* Improvement: deprecated `DEBUG_FD` environment variable (#405, @tootallnate)
2.5.2 / 2016-12-25
==================
* Fix: reference error on window within webworkers (#393, @KlausTrainer)
* Docs: fixed README typo (#391, @lurch)
* Docs: added notice about v3 api discussion (@thebigredgeek)
2.5.1 / 2016-12-20
==================
* Fix: babel-core compatibility
2.5.0 / 2016-12-20
==================
* Fix: wrong reference in bower file (@thebigredgeek)
* Fix: webworker compatibility (@thebigredgeek)
* Fix: output formatting issue (#388, @kribblo)
* Fix: babel-loader compatibility (#383, @escwald)
* Misc: removed built asset from repo and publications (@thebigredgeek)
* Misc: moved source files to /src (#378, @yamikuronue)
* Test: added karma integration and replaced babel with browserify for browser tests (#378, @yamikuronue)
* Test: coveralls integration (#378, @yamikuronue)
* Docs: simplified language in the opening paragraph (#373, @yamikuronue)
2.4.5 / 2016-12-17
==================
* Fix: `navigator` undefined in Rhino (#376, @jochenberger)
* Fix: custom log function (#379, @hsiliev)
* Improvement: bit of cleanup + linting fixes (@thebigredgeek)
* Improvement: rm non-maintainted `dist/` dir (#375, @freewil)
* Docs: simplified language in the opening paragraph. (#373, @yamikuronue)
2.4.4 / 2016-12-14
==================
* Fix: work around debug being loaded in preload scripts for electron (#368, @paulcbetts)
2.4.3 / 2016-12-14
==================
* Fix: navigation.userAgent error for react native (#364, @escwald)
2.4.2 / 2016-12-14
==================
* Fix: browser colors (#367, @tootallnate)
* Misc: travis ci integration (@thebigredgeek)
* Misc: added linting and testing boilerplate with sanity check (@thebigredgeek)
2.4.1 / 2016-12-13
==================
* Fix: typo that broke the package (#356)
2.4.0 / 2016-12-13
==================
* Fix: bower.json references unbuilt src entry point (#342, @justmatt)
* Fix: revert "handle regex special characters" (@tootallnate)
* Feature: configurable util.inspect()`options for NodeJS (#327, @tootallnate)
* Feature: %O`(big O) pretty-prints objects (#322, @tootallnate)
* Improvement: allow colors in workers (#335, @botverse)
* Improvement: use same color for same namespace. (#338, @lchenay)
2.3.3 / 2016-11-09
==================
* Fix: Catch `JSON.stringify()` errors (#195, Jovan Alleyne)
* Fix: Returning `localStorage` saved values (#331, Levi Thomason)
* Improvement: Don't create an empty object when no `process` (Nathan Rajlich)
2.3.2 / 2016-11-09
==================
* Fix: be super-safe in index.js as well (@TooTallNate)
* Fix: should check whether process exists (Tom Newby)
2.3.1 / 2016-11-09
==================
* Fix: Added electron compatibility (#324, @paulcbetts)
* Improvement: Added performance optimizations (@tootallnate)
* Readme: Corrected PowerShell environment variable example (#252, @gimre)
* Misc: Removed yarn lock file from source control (#321, @fengmk2)
2.3.0 / 2016-11-07
==================
* Fix: Consistent placement of ms diff at end of output (#215, @gorangajic)
* Fix: Escaping of regex special characters in namespace strings (#250, @zacronos)
* Fix: Fixed bug causing crash on react-native (#282, @vkarpov15)
* Feature: Enabled ES6+ compatible import via default export (#212 @bucaran)
* Feature: Added %O formatter to reflect Chrome's console.log capability (#279, @oncletom)
* Package: Update "ms" to 0.7.2 (#315, @DevSide)
* Package: removed superfluous version property from bower.json (#207 @kkirsche)
* Readme: fix USE_COLORS to DEBUG_COLORS
* Readme: Doc fixes for format string sugar (#269, @mlucool)
* Readme: Updated docs for DEBUG_FD and DEBUG_COLORS environment variables (#232, @mattlyons0)
* Readme: doc fixes for PowerShell (#271 #243, @exoticknight @unreadable)
* Readme: better docs for browser support (#224, @matthewmueller)
* Tooling: Added yarn integration for development (#317, @thebigredgeek)
* Misc: Renamed History.md to CHANGELOG.md (@thebigredgeek)
* Misc: Added license file (#226 #274, @CantemoInternal @sdaitzman)
* Misc: Updated contributors (@thebigredgeek)
2.2.0 / 2015-05-09
==================
* package: update "ms" to v0.7.1 (#202, @dougwilson)
* README: add logging to file example (#193, @DanielOchoa)
* README: fixed a typo (#191, @amir-s)
* browser: expose `storage` (#190, @stephenmathieson)
* Makefile: add a `distclean` target (#189, @stephenmathieson)
2.1.3 / 2015-03-13
==================
* Updated stdout/stderr example (#186)
* Updated example/stdout.js to match debug current behaviour
* Renamed example/stderr.js to stdout.js
* Update Readme.md (#184)
* replace high intensity foreground color for bold (#182, #183)
2.1.2 / 2015-03-01
==================
* dist: recompile
* update "ms" to v0.7.0
* package: update "browserify" to v9.0.3
* component: fix "ms.js" repo location
* changed bower package name
* updated documentation about using debug in a browser
* fix: security error on safari (#167, #168, @yields)
2.1.1 / 2014-12-29
==================
* browser: use `typeof` to check for `console` existence
* browser: check for `console.log` truthiness (fix IE 8/9)
* browser: add support for Chrome apps
* Readme: added Windows usage remarks
* Add `bower.json` to properly support bower install
2.1.0 / 2014-10-15
==================
* node: implement `DEBUG_FD` env variable support
* package: update "browserify" to v6.1.0
* package: add "license" field to package.json (#135, @panuhorsmalahti)
2.0.0 / 2014-09-01
==================
* package: update "browserify" to v5.11.0
* node: use stderr rather than stdout for logging (#29, @stephenmathieson)
1.0.4 / 2014-07-15
==================
* dist: recompile
* example: remove `console.info()` log usage
* example: add "Content-Type" UTF-8 header to browser example
* browser: place %c marker after the space character
* browser: reset the "content" color via `color: inherit`
* browser: add colors support for Firefox >= v31
* debug: prefer an instance `log()` function over the global one (#119)
* Readme: update documentation about styled console logs for FF v31 (#116, @wryk)
1.0.3 / 2014-07-09
==================
* Add support for multiple wildcards in namespaces (#122, @seegno)
* browser: fix lint
1.0.2 / 2014-06-10
==================
* browser: update color palette (#113, @gscottolson)
* common: make console logging function configurable (#108, @timoxley)
* node: fix %o colors on old node <= 0.8.x
* Makefile: find node path using shell/which (#109, @timoxley)
1.0.1 / 2014-06-06
==================
* browser: use `removeItem()` to clear localStorage
* browser, node: don't set DEBUG if namespaces is undefined (#107, @leedm777)
* package: add "contributors" section
* node: fix comment typo
* README: list authors
1.0.0 / 2014-06-04
==================
* make ms diff be global, not be scope
* debug: ignore empty strings in enable()
* node: make DEBUG_COLORS able to disable coloring
* *: export the `colors` array
* npmignore: don't publish the `dist` dir
* Makefile: refactor to use browserify
* package: add "browserify" as a dev dependency
* Readme: add Web Inspector Colors section
* node: reset terminal color for the debug content
* node: map "%o" to `util.inspect()`
* browser: map "%j" to `JSON.stringify()`
* debug: add custom "formatters"
* debug: use "ms" module for humanizing the diff
* Readme: add "bash" syntax highlighting
* browser: add Firebug color support
* browser: add colors for WebKit browsers
* node: apply log to `console`
* rewrite: abstract common logic for Node & browsers
* add .jshintrc file
0.8.1 / 2014-04-14
==================
* package: re-add the "component" section
0.8.0 / 2014-03-30
==================
* add `enable()` method for nodejs. Closes #27
* change from stderr to stdout
* remove unnecessary index.js file
0.7.4 / 2013-11-13
==================
* remove "browserify" key from package.json (fixes something in browserify)
0.7.3 / 2013-10-30
==================
* fix: catch localStorage security error when cookies are blocked (Chrome)
* add debug(err) support. Closes #46
* add .browser prop to package.json. Closes #42
0.7.2 / 2013-02-06
==================
* fix package.json
* fix: Mobile Safari (private mode) is broken with debug
* fix: Use unicode to send escape character to shell instead of octal to work with strict mode javascript
0.7.1 / 2013-02-05
==================
* add repository URL to package.json
* add DEBUG_COLORED to force colored output
* add browserify support
* fix component. Closes #24
0.7.0 / 2012-05-04
==================
* Added .component to package.json
* Added debug.component.js build
0.6.0 / 2012-03-16
==================
* Added support for "-" prefix in DEBUG [Vinay Pulim]
* Added `.enabled` flag to the node version [TooTallNate]
0.5.0 / 2012-02-02
==================
* Added: humanize diffs. Closes #8
* Added `debug.disable()` to the CS variant
* Removed padding. Closes #10
* Fixed: persist client-side variant again. Closes #9
0.4.0 / 2012-02-01
==================
* Added browser variant support for older browsers [TooTallNate]
* Added `debug.enable('project:*')` to browser variant [TooTallNate]
* Added padding to diff (moved it to the right)
0.3.0 / 2012-01-26
==================
* Added millisecond diff when isatty, otherwise UTC string
0.2.0 / 2012-01-22
==================
* Added wildcard support
0.1.0 / 2011-12-02
==================
* Added: remove colors unless stderr isatty [TooTallNate]
0.0.1 / 2010-01-03
==================
* Initial release

View File

@@ -0,0 +1,19 @@
(The MIT License)
Copyright (c) 2014 TJ Holowaychuk <tj@vision-media.ca>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the 'Software'), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,50 @@
# get Makefile directory name: http://stackoverflow.com/a/5982798/376773
THIS_MAKEFILE_PATH:=$(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))
THIS_DIR:=$(shell cd $(dir $(THIS_MAKEFILE_PATH));pwd)
# BIN directory
BIN := $(THIS_DIR)/node_modules/.bin
# Path
PATH := node_modules/.bin:$(PATH)
SHELL := /bin/bash
# applications
NODE ?= $(shell which node)
YARN ?= $(shell which yarn)
PKG ?= $(if $(YARN),$(YARN),$(NODE) $(shell which npm))
BROWSERIFY ?= $(NODE) $(BIN)/browserify
.FORCE:
install: node_modules
node_modules: package.json
@NODE_ENV= $(PKG) install
@touch node_modules
lint: .FORCE
eslint browser.js debug.js index.js node.js
test-node: .FORCE
istanbul cover node_modules/mocha/bin/_mocha -- test/**.js
test-browser: .FORCE
mkdir -p dist
@$(BROWSERIFY) \
--standalone debug \
. > dist/debug.js
karma start --single-run
rimraf dist
test: .FORCE
concurrently \
"make test-node" \
"make test-browser"
coveralls:
cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
.PHONY: all install clean distclean

View File

@@ -0,0 +1,312 @@
# debug
[![Build Status](https://travis-ci.org/visionmedia/debug.svg?branch=master)](https://travis-ci.org/visionmedia/debug) [![Coverage Status](https://coveralls.io/repos/github/visionmedia/debug/badge.svg?branch=master)](https://coveralls.io/github/visionmedia/debug?branch=master) [![Slack](https://visionmedia-community-slackin.now.sh/badge.svg)](https://visionmedia-community-slackin.now.sh/) [![OpenCollective](https://opencollective.com/debug/backers/badge.svg)](#backers)
[![OpenCollective](https://opencollective.com/debug/sponsors/badge.svg)](#sponsors)
A tiny node.js debugging utility modelled after node core's debugging technique.
**Discussion around the V3 API is under way [here](https://github.com/visionmedia/debug/issues/370)**
## Installation
```bash
$ npm install debug
```
## Usage
`debug` exposes a function; simply pass this function the name of your module, and it will return a decorated version of `console.error` for you to pass debug statements to. This will allow you to toggle the debug output for different parts of your module as well as the module as a whole.
Example _app.js_:
```js
var debug = require('debug')('http')
, http = require('http')
, name = 'My App';
// fake app
debug('booting %s', name);
http.createServer(function(req, res){
debug(req.method + ' ' + req.url);
res.end('hello\n');
}).listen(3000, function(){
debug('listening');
});
// fake worker of some kind
require('./worker');
```
Example _worker.js_:
```js
var debug = require('debug')('worker');
setInterval(function(){
debug('doing some work');
}, 1000);
```
The __DEBUG__ environment variable is then used to enable these based on space or comma-delimited names. Here are some examples:
![debug http and worker](http://f.cl.ly/items/18471z1H402O24072r1J/Screenshot.png)
![debug worker](http://f.cl.ly/items/1X413v1a3M0d3C2c1E0i/Screenshot.png)
#### Windows note
On Windows the environment variable is set using the `set` command.
```cmd
set DEBUG=*,-not_this
```
Note that PowerShell uses different syntax to set environment variables.
```cmd
$env:DEBUG = "*,-not_this"
```
Then, run the program to be debugged as usual.
## Millisecond diff
When actively developing an application it can be useful to see when the time spent between one `debug()` call and the next. Suppose for example you invoke `debug()` before requesting a resource, and after as well, the "+NNNms" will show you how much time was spent between calls.
![](http://f.cl.ly/items/2i3h1d3t121M2Z1A3Q0N/Screenshot.png)
When stdout is not a TTY, `Date#toUTCString()` is used, making it more useful for logging the debug information as shown below:
![](http://f.cl.ly/items/112H3i0e0o0P0a2Q2r11/Screenshot.png)
## Conventions
If you're using this in one or more of your libraries, you _should_ use the name of your library so that developers may toggle debugging as desired without guessing names. If you have more than one debuggers you _should_ prefix them with your library name and use ":" to separate features. For example "bodyParser" from Connect would then be "connect:bodyParser".
## Wildcards
The `*` character may be used as a wildcard. Suppose for example your library has debuggers named "connect:bodyParser", "connect:compress", "connect:session", instead of listing all three with `DEBUG=connect:bodyParser,connect:compress,connect:session`, you may simply do `DEBUG=connect:*`, or to run everything using this module simply use `DEBUG=*`.
You can also exclude specific debuggers by prefixing them with a "-" character. For example, `DEBUG=*,-connect:*` would include all debuggers except those starting with "connect:".
## Environment Variables
When running through Node.js, you can set a few environment variables that will
change the behavior of the debug logging:
| Name | Purpose |
|-----------|-------------------------------------------------|
| `DEBUG` | Enables/disables specific debugging namespaces. |
| `DEBUG_COLORS`| Whether or not to use colors in the debug output. |
| `DEBUG_DEPTH` | Object inspection depth. |
| `DEBUG_SHOW_HIDDEN` | Shows hidden properties on inspected objects. |
__Note:__ The environment variables beginning with `DEBUG_` end up being
converted into an Options object that gets used with `%o`/`%O` formatters.
See the Node.js documentation for
[`util.inspect()`](https://nodejs.org/api/util.html#util_util_inspect_object_options)
for the complete list.
## Formatters
Debug uses [printf-style](https://wikipedia.org/wiki/Printf_format_string) formatting. Below are the officially supported formatters:
| Formatter | Representation |
|-----------|----------------|
| `%O` | Pretty-print an Object on multiple lines. |
| `%o` | Pretty-print an Object all on a single line. |
| `%s` | String. |
| `%d` | Number (both integer and float). |
| `%j` | JSON. Replaced with the string '[Circular]' if the argument contains circular references. |
| `%%` | Single percent sign ('%'). This does not consume an argument. |
### Custom formatters
You can add custom formatters by extending the `debug.formatters` object. For example, if you wanted to add support for rendering a Buffer as hex with `%h`, you could do something like:
```js
const createDebug = require('debug')
createDebug.formatters.h = (v) => {
return v.toString('hex')
}
// …elsewhere
const debug = createDebug('foo')
debug('this is hex: %h', new Buffer('hello world'))
// foo this is hex: 68656c6c6f20776f726c6421 +0ms
```
## Browser support
You can build a browser-ready script using [browserify](https://github.com/substack/node-browserify),
or just use the [browserify-as-a-service](https://wzrd.in/) [build](https://wzrd.in/standalone/debug@latest),
if you don't want to build it yourself.
Debug's enable state is currently persisted by `localStorage`.
Consider the situation shown below where you have `worker:a` and `worker:b`,
and wish to debug both. You can enable this using `localStorage.debug`:
```js
localStorage.debug = 'worker:*'
```
And then refresh the page.
```js
a = debug('worker:a');
b = debug('worker:b');
setInterval(function(){
a('doing some work');
}, 1000);
setInterval(function(){
b('doing some work');
}, 1200);
```
#### Web Inspector Colors
Colors are also enabled on "Web Inspectors" that understand the `%c` formatting
option. These are WebKit web inspectors, Firefox ([since version
31](https://hacks.mozilla.org/2014/05/editable-box-model-multiple-selection-sublime-text-keys-much-more-firefox-developer-tools-episode-31/))
and the Firebug plugin for Firefox (any version).
Colored output looks something like:
![](https://cloud.githubusercontent.com/assets/71256/3139768/b98c5fd8-e8ef-11e3-862a-f7253b6f47c6.png)
## Output streams
By default `debug` will log to stderr, however this can be configured per-namespace by overriding the `log` method:
Example _stdout.js_:
```js
var debug = require('debug');
var error = debug('app:error');
// by default stderr is used
error('goes to stderr!');
var log = debug('app:log');
// set this namespace to log via console.log
log.log = console.log.bind(console); // don't forget to bind to console!
log('goes to stdout');
error('still goes to stderr!');
// set all output to go via console.info
// overrides all per-namespace log settings
debug.log = console.info.bind(console);
error('now goes to stdout via console.info');
log('still goes to stdout, but via console.info now');
```
## Authors
- TJ Holowaychuk
- Nathan Rajlich
- Andrew Rhyne
## Backers
Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/debug#backer)]
<a href="https://opencollective.com/debug/backer/0/website" target="_blank"><img src="https://opencollective.com/debug/backer/0/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/1/website" target="_blank"><img src="https://opencollective.com/debug/backer/1/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/2/website" target="_blank"><img src="https://opencollective.com/debug/backer/2/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/3/website" target="_blank"><img src="https://opencollective.com/debug/backer/3/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/4/website" target="_blank"><img src="https://opencollective.com/debug/backer/4/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/5/website" target="_blank"><img src="https://opencollective.com/debug/backer/5/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/6/website" target="_blank"><img src="https://opencollective.com/debug/backer/6/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/7/website" target="_blank"><img src="https://opencollective.com/debug/backer/7/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/8/website" target="_blank"><img src="https://opencollective.com/debug/backer/8/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/9/website" target="_blank"><img src="https://opencollective.com/debug/backer/9/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/10/website" target="_blank"><img src="https://opencollective.com/debug/backer/10/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/11/website" target="_blank"><img src="https://opencollective.com/debug/backer/11/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/12/website" target="_blank"><img src="https://opencollective.com/debug/backer/12/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/13/website" target="_blank"><img src="https://opencollective.com/debug/backer/13/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/14/website" target="_blank"><img src="https://opencollective.com/debug/backer/14/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/15/website" target="_blank"><img src="https://opencollective.com/debug/backer/15/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/16/website" target="_blank"><img src="https://opencollective.com/debug/backer/16/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/17/website" target="_blank"><img src="https://opencollective.com/debug/backer/17/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/18/website" target="_blank"><img src="https://opencollective.com/debug/backer/18/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/19/website" target="_blank"><img src="https://opencollective.com/debug/backer/19/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/20/website" target="_blank"><img src="https://opencollective.com/debug/backer/20/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/21/website" target="_blank"><img src="https://opencollective.com/debug/backer/21/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/22/website" target="_blank"><img src="https://opencollective.com/debug/backer/22/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/23/website" target="_blank"><img src="https://opencollective.com/debug/backer/23/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/24/website" target="_blank"><img src="https://opencollective.com/debug/backer/24/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/25/website" target="_blank"><img src="https://opencollective.com/debug/backer/25/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/26/website" target="_blank"><img src="https://opencollective.com/debug/backer/26/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/27/website" target="_blank"><img src="https://opencollective.com/debug/backer/27/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/28/website" target="_blank"><img src="https://opencollective.com/debug/backer/28/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/29/website" target="_blank"><img src="https://opencollective.com/debug/backer/29/avatar.svg"></a>
## Sponsors
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/debug#sponsor)]
<a href="https://opencollective.com/debug/sponsor/0/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/1/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/2/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/3/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/4/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/5/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/6/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/7/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/8/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/9/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/9/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/10/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/10/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/11/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/11/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/12/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/12/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/13/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/13/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/14/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/14/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/15/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/15/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/16/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/16/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/17/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/17/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/18/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/18/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/19/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/19/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/20/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/20/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/21/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/21/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/22/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/22/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/23/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/23/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/24/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/24/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/25/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/25/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/26/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/26/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/27/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/27/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/28/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/28/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/29/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/29/avatar.svg"></a>
## License
(The MIT License)
Copyright (c) 2014-2016 TJ Holowaychuk &lt;tj@vision-media.ca&gt;
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,19 @@
{
"name": "debug",
"repo": "visionmedia/debug",
"description": "small debugging utility",
"version": "2.6.9",
"keywords": [
"debug",
"log",
"debugger"
],
"main": "src/browser.js",
"scripts": [
"src/browser.js",
"src/debug.js"
],
"dependencies": {
"rauchg/ms.js": "0.7.1"
}
}

View File

@@ -0,0 +1,70 @@
// Karma configuration
// Generated on Fri Dec 16 2016 13:09:51 GMT+0000 (UTC)
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['mocha', 'chai', 'sinon'],
// list of files / patterns to load in the browser
files: [
'dist/debug.js',
'test/*spec.js'
],
// list of files to exclude
exclude: [
'src/node.js'
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['PhantomJS'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: false,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity
})
}

View File

@@ -0,0 +1 @@
module.exports = require('./src/node');

View File

@@ -0,0 +1,49 @@
{
"name": "debug",
"version": "2.6.9",
"repository": {
"type": "git",
"url": "git://github.com/visionmedia/debug.git"
},
"description": "small debugging utility",
"keywords": [
"debug",
"log",
"debugger"
],
"author": "TJ Holowaychuk <tj@vision-media.ca>",
"contributors": [
"Nathan Rajlich <nathan@tootallnate.net> (http://n8.io)",
"Andrew Rhyne <rhyneandrew@gmail.com>"
],
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
},
"devDependencies": {
"browserify": "9.0.3",
"chai": "^3.5.0",
"concurrently": "^3.1.0",
"coveralls": "^2.11.15",
"eslint": "^3.12.1",
"istanbul": "^0.4.5",
"karma": "^1.3.0",
"karma-chai": "^0.1.0",
"karma-mocha": "^1.3.0",
"karma-phantomjs-launcher": "^1.0.2",
"karma-sinon": "^1.0.5",
"mocha": "^3.2.0",
"mocha-lcov-reporter": "^1.2.0",
"rimraf": "^2.5.4",
"sinon": "^1.17.6",
"sinon-chai": "^2.8.0"
},
"main": "./src/index.js",
"browser": "./src/browser.js",
"component": {
"scripts": {
"debug/index.js": "browser.js",
"debug/debug.js": "debug.js"
}
}
}

View File

@@ -0,0 +1,185 @@
/**
* This is the web browser implementation of `debug()`.
*
* Expose `debug()` as the module.
*/
exports = module.exports = require('./debug');
exports.log = log;
exports.formatArgs = formatArgs;
exports.save = save;
exports.load = load;
exports.useColors = useColors;
exports.storage = 'undefined' != typeof chrome
&& 'undefined' != typeof chrome.storage
? chrome.storage.local
: localstorage();
/**
* Colors.
*/
exports.colors = [
'lightseagreen',
'forestgreen',
'goldenrod',
'dodgerblue',
'darkorchid',
'crimson'
];
/**
* Currently only WebKit-based Web Inspectors, Firefox >= v31,
* and the Firebug extension (any Firefox version) are known
* to support "%c" CSS customizations.
*
* TODO: add a `localStorage` variable to explicitly enable/disable colors
*/
function useColors() {
// NB: In an Electron preload script, document will be defined but not fully
// initialized. Since we know we're in Chrome, we'll just detect this case
// explicitly
if (typeof window !== 'undefined' && window.process && window.process.type === 'renderer') {
return true;
}
// is webkit? http://stackoverflow.com/a/16459606/376773
// document is undefined in react-native: https://github.com/facebook/react-native/pull/1632
return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) ||
// is firebug? http://stackoverflow.com/a/398120/376773
(typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) ||
// is firefox >= v31?
// https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages
(typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31) ||
// double check webkit in userAgent just in case we are in a worker
(typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/));
}
/**
* Map %j to `JSON.stringify()`, since no Web Inspectors do that by default.
*/
exports.formatters.j = function(v) {
try {
return JSON.stringify(v);
} catch (err) {
return '[UnexpectedJSONParseError]: ' + err.message;
}
};
/**
* Colorize log arguments if enabled.
*
* @api public
*/
function formatArgs(args) {
var useColors = this.useColors;
args[0] = (useColors ? '%c' : '')
+ this.namespace
+ (useColors ? ' %c' : ' ')
+ args[0]
+ (useColors ? '%c ' : ' ')
+ '+' + exports.humanize(this.diff);
if (!useColors) return;
var c = 'color: ' + this.color;
args.splice(1, 0, c, 'color: inherit')
// the final "%c" is somewhat tricky, because there could be other
// arguments passed either before or after the %c, so we need to
// figure out the correct index to insert the CSS into
var index = 0;
var lastC = 0;
args[0].replace(/%[a-zA-Z%]/g, function(match) {
if ('%%' === match) return;
index++;
if ('%c' === match) {
// we only are interested in the *last* %c
// (the user may have provided their own)
lastC = index;
}
});
args.splice(lastC, 0, c);
}
/**
* Invokes `console.log()` when available.
* No-op when `console.log` is not a "function".
*
* @api public
*/
function log() {
// this hackery is required for IE8/9, where
// the `console.log` function doesn't have 'apply'
return 'object' === typeof console
&& console.log
&& Function.prototype.apply.call(console.log, console, arguments);
}
/**
* Save `namespaces`.
*
* @param {String} namespaces
* @api private
*/
function save(namespaces) {
try {
if (null == namespaces) {
exports.storage.removeItem('debug');
} else {
exports.storage.debug = namespaces;
}
} catch(e) {}
}
/**
* Load `namespaces`.
*
* @return {String} returns the previously persisted debug modes
* @api private
*/
function load() {
var r;
try {
r = exports.storage.debug;
} catch(e) {}
// If debug isn't set in LS, and we're in Electron, try to load $DEBUG
if (!r && typeof process !== 'undefined' && 'env' in process) {
r = process.env.DEBUG;
}
return r;
}
/**
* Enable namespaces listed in `localStorage.debug` initially.
*/
exports.enable(load());
/**
* Localstorage attempts to return the localstorage.
*
* This is necessary because safari throws
* when a user disables cookies/localstorage
* and you attempt to access it.
*
* @return {LocalStorage}
* @api private
*/
function localstorage() {
try {
return window.localStorage;
} catch (e) {}
}

View File

@@ -0,0 +1,202 @@
/**
* This is the common logic for both the Node.js and web browser
* implementations of `debug()`.
*
* Expose `debug()` as the module.
*/
exports = module.exports = createDebug.debug = createDebug['default'] = createDebug;
exports.coerce = coerce;
exports.disable = disable;
exports.enable = enable;
exports.enabled = enabled;
exports.humanize = require('ms');
/**
* The currently active debug mode names, and names to skip.
*/
exports.names = [];
exports.skips = [];
/**
* Map of special "%n" handling functions, for the debug "format" argument.
*
* Valid key names are a single, lower or upper-case letter, i.e. "n" and "N".
*/
exports.formatters = {};
/**
* Previous log timestamp.
*/
var prevTime;
/**
* Select a color.
* @param {String} namespace
* @return {Number}
* @api private
*/
function selectColor(namespace) {
var hash = 0, i;
for (i in namespace) {
hash = ((hash << 5) - hash) + namespace.charCodeAt(i);
hash |= 0; // Convert to 32bit integer
}
return exports.colors[Math.abs(hash) % exports.colors.length];
}
/**
* Create a debugger with the given `namespace`.
*
* @param {String} namespace
* @return {Function}
* @api public
*/
function createDebug(namespace) {
function debug() {
// disabled?
if (!debug.enabled) return;
var self = debug;
// set `diff` timestamp
var curr = +new Date();
var ms = curr - (prevTime || curr);
self.diff = ms;
self.prev = prevTime;
self.curr = curr;
prevTime = curr;
// turn the `arguments` into a proper Array
var args = new Array(arguments.length);
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
args[0] = exports.coerce(args[0]);
if ('string' !== typeof args[0]) {
// anything else let's inspect with %O
args.unshift('%O');
}
// apply any `formatters` transformations
var index = 0;
args[0] = args[0].replace(/%([a-zA-Z%])/g, function(match, format) {
// if we encounter an escaped % then don't increase the array index
if (match === '%%') return match;
index++;
var formatter = exports.formatters[format];
if ('function' === typeof formatter) {
var val = args[index];
match = formatter.call(self, val);
// now we need to remove `args[index]` since it's inlined in the `format`
args.splice(index, 1);
index--;
}
return match;
});
// apply env-specific formatting (colors, etc.)
exports.formatArgs.call(self, args);
var logFn = debug.log || exports.log || console.log.bind(console);
logFn.apply(self, args);
}
debug.namespace = namespace;
debug.enabled = exports.enabled(namespace);
debug.useColors = exports.useColors();
debug.color = selectColor(namespace);
// env-specific initialization logic for debug instances
if ('function' === typeof exports.init) {
exports.init(debug);
}
return debug;
}
/**
* Enables a debug mode by namespaces. This can include modes
* separated by a colon and wildcards.
*
* @param {String} namespaces
* @api public
*/
function enable(namespaces) {
exports.save(namespaces);
exports.names = [];
exports.skips = [];
var split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/);
var len = split.length;
for (var i = 0; i < len; i++) {
if (!split[i]) continue; // ignore empty strings
namespaces = split[i].replace(/\*/g, '.*?');
if (namespaces[0] === '-') {
exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$'));
} else {
exports.names.push(new RegExp('^' + namespaces + '$'));
}
}
}
/**
* Disable debug output.
*
* @api public
*/
function disable() {
exports.enable('');
}
/**
* Returns true if the given mode name is enabled, false otherwise.
*
* @param {String} name
* @return {Boolean}
* @api public
*/
function enabled(name) {
var i, len;
for (i = 0, len = exports.skips.length; i < len; i++) {
if (exports.skips[i].test(name)) {
return false;
}
}
for (i = 0, len = exports.names.length; i < len; i++) {
if (exports.names[i].test(name)) {
return true;
}
}
return false;
}
/**
* Coerce `val`.
*
* @param {Mixed} val
* @return {Mixed}
* @api private
*/
function coerce(val) {
if (val instanceof Error) return val.stack || val.message;
return val;
}

View File

@@ -0,0 +1,10 @@
/**
* Detect Electron renderer process, which is node, but we should
* treat as a browser.
*/
if (typeof process !== 'undefined' && process.type === 'renderer') {
module.exports = require('./browser.js');
} else {
module.exports = require('./node.js');
}

View File

@@ -0,0 +1,15 @@
module.exports = inspectorLog;
// black hole
const nullStream = new (require('stream').Writable)();
nullStream._write = () => {};
/**
* Outputs a `console.log()` to the Node.js Inspector console *only*.
*/
function inspectorLog() {
const stdout = console._stdout;
console._stdout = nullStream;
console.log.apply(console, arguments);
console._stdout = stdout;
}

View File

@@ -0,0 +1,248 @@
/**
* Module dependencies.
*/
var tty = require('tty');
var util = require('util');
/**
* This is the Node.js implementation of `debug()`.
*
* Expose `debug()` as the module.
*/
exports = module.exports = require('./debug');
exports.init = init;
exports.log = log;
exports.formatArgs = formatArgs;
exports.save = save;
exports.load = load;
exports.useColors = useColors;
/**
* Colors.
*/
exports.colors = [6, 2, 3, 4, 5, 1];
/**
* Build up the default `inspectOpts` object from the environment variables.
*
* $ DEBUG_COLORS=no DEBUG_DEPTH=10 DEBUG_SHOW_HIDDEN=enabled node script.js
*/
exports.inspectOpts = Object.keys(process.env).filter(function (key) {
return /^debug_/i.test(key);
}).reduce(function (obj, key) {
// camel-case
var prop = key
.substring(6)
.toLowerCase()
.replace(/_([a-z])/g, function (_, k) { return k.toUpperCase() });
// coerce string value into JS value
var val = process.env[key];
if (/^(yes|on|true|enabled)$/i.test(val)) val = true;
else if (/^(no|off|false|disabled)$/i.test(val)) val = false;
else if (val === 'null') val = null;
else val = Number(val);
obj[prop] = val;
return obj;
}, {});
/**
* The file descriptor to write the `debug()` calls to.
* Set the `DEBUG_FD` env variable to override with another value. i.e.:
*
* $ DEBUG_FD=3 node script.js 3>debug.log
*/
var fd = parseInt(process.env.DEBUG_FD, 10) || 2;
if (1 !== fd && 2 !== fd) {
util.deprecate(function(){}, 'except for stderr(2) and stdout(1), any other usage of DEBUG_FD is deprecated. Override debug.log if you want to use a different log function (https://git.io/debug_fd)')()
}
var stream = 1 === fd ? process.stdout :
2 === fd ? process.stderr :
createWritableStdioStream(fd);
/**
* Is stdout a TTY? Colored output is enabled when `true`.
*/
function useColors() {
return 'colors' in exports.inspectOpts
? Boolean(exports.inspectOpts.colors)
: tty.isatty(fd);
}
/**
* Map %o to `util.inspect()`, all on a single line.
*/
exports.formatters.o = function(v) {
this.inspectOpts.colors = this.useColors;
return util.inspect(v, this.inspectOpts)
.split('\n').map(function(str) {
return str.trim()
}).join(' ');
};
/**
* Map %o to `util.inspect()`, allowing multiple lines if needed.
*/
exports.formatters.O = function(v) {
this.inspectOpts.colors = this.useColors;
return util.inspect(v, this.inspectOpts);
};
/**
* Adds ANSI color escape codes if enabled.
*
* @api public
*/
function formatArgs(args) {
var name = this.namespace;
var useColors = this.useColors;
if (useColors) {
var c = this.color;
var prefix = ' \u001b[3' + c + ';1m' + name + ' ' + '\u001b[0m';
args[0] = prefix + args[0].split('\n').join('\n' + prefix);
args.push('\u001b[3' + c + 'm+' + exports.humanize(this.diff) + '\u001b[0m');
} else {
args[0] = new Date().toUTCString()
+ ' ' + name + ' ' + args[0];
}
}
/**
* Invokes `util.format()` with the specified arguments and writes to `stream`.
*/
function log() {
return stream.write(util.format.apply(util, arguments) + '\n');
}
/**
* Save `namespaces`.
*
* @param {String} namespaces
* @api private
*/
function save(namespaces) {
if (null == namespaces) {
// If you set a process.env field to null or undefined, it gets cast to the
// string 'null' or 'undefined'. Just delete instead.
delete process.env.DEBUG;
} else {
process.env.DEBUG = namespaces;
}
}
/**
* Load `namespaces`.
*
* @return {String} returns the previously persisted debug modes
* @api private
*/
function load() {
return process.env.DEBUG;
}
/**
* Copied from `node/src/node.js`.
*
* XXX: It's lame that node doesn't expose this API out-of-the-box. It also
* relies on the undocumented `tty_wrap.guessHandleType()` which is also lame.
*/
function createWritableStdioStream (fd) {
var stream;
var tty_wrap = process.binding('tty_wrap');
// Note stream._type is used for test-module-load-list.js
switch (tty_wrap.guessHandleType(fd)) {
case 'TTY':
stream = new tty.WriteStream(fd);
stream._type = 'tty';
// Hack to have stream not keep the event loop alive.
// See https://github.com/joyent/node/issues/1726
if (stream._handle && stream._handle.unref) {
stream._handle.unref();
}
break;
case 'FILE':
var fs = require('fs');
stream = new fs.SyncWriteStream(fd, { autoClose: false });
stream._type = 'fs';
break;
case 'PIPE':
case 'TCP':
var net = require('net');
stream = new net.Socket({
fd: fd,
readable: false,
writable: true
});
// FIXME Should probably have an option in net.Socket to create a
// stream from an existing fd which is writable only. But for now
// we'll just add this hack and set the `readable` member to false.
// Test: ./node test/fixtures/echo.js < /etc/passwd
stream.readable = false;
stream.read = null;
stream._type = 'pipe';
// FIXME Hack to have stream not keep the event loop alive.
// See https://github.com/joyent/node/issues/1726
if (stream._handle && stream._handle.unref) {
stream._handle.unref();
}
break;
default:
// Probably an error on in uv_guess_handle()
throw new Error('Implement me. Unknown stream file type!');
}
// For supporting legacy API we put the FD here.
stream.fd = fd;
stream._isStdio = true;
return stream;
}
/**
* Init logic for `debug` instances.
*
* Create a new `inspectOpts` object in case `useColors` is set
* differently for a particular `debug` instance.
*/
function init (debug) {
debug.inspectOpts = {};
var keys = Object.keys(exports.inspectOpts);
for (var i = 0; i < keys.length; i++) {
debug.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]];
}
}
/**
* Enable namespaces listed in `process.env.DEBUG` initially.
*/
exports.enable(load());

View File

@@ -0,0 +1,152 @@
/**
* Helpers.
*/
var s = 1000;
var m = s * 60;
var h = m * 60;
var d = h * 24;
var y = d * 365.25;
/**
* Parse or format the given `val`.
*
* Options:
*
* - `long` verbose formatting [false]
*
* @param {String|Number} val
* @param {Object} [options]
* @throws {Error} throw an error if val is not a non-empty string or a number
* @return {String|Number}
* @api public
*/
module.exports = function(val, options) {
options = options || {};
var type = typeof val;
if (type === 'string' && val.length > 0) {
return parse(val);
} else if (type === 'number' && isNaN(val) === false) {
return options.long ? fmtLong(val) : fmtShort(val);
}
throw new Error(
'val is not a non-empty string or a valid number. val=' +
JSON.stringify(val)
);
};
/**
* Parse the given `str` and return milliseconds.
*
* @param {String} str
* @return {Number}
* @api private
*/
function parse(str) {
str = String(str);
if (str.length > 100) {
return;
}
var match = /^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(
str
);
if (!match) {
return;
}
var n = parseFloat(match[1]);
var type = (match[2] || 'ms').toLowerCase();
switch (type) {
case 'years':
case 'year':
case 'yrs':
case 'yr':
case 'y':
return n * y;
case 'days':
case 'day':
case 'd':
return n * d;
case 'hours':
case 'hour':
case 'hrs':
case 'hr':
case 'h':
return n * h;
case 'minutes':
case 'minute':
case 'mins':
case 'min':
case 'm':
return n * m;
case 'seconds':
case 'second':
case 'secs':
case 'sec':
case 's':
return n * s;
case 'milliseconds':
case 'millisecond':
case 'msecs':
case 'msec':
case 'ms':
return n;
default:
return undefined;
}
}
/**
* Short format for `ms`.
*
* @param {Number} ms
* @return {String}
* @api private
*/
function fmtShort(ms) {
if (ms >= d) {
return Math.round(ms / d) + 'd';
}
if (ms >= h) {
return Math.round(ms / h) + 'h';
}
if (ms >= m) {
return Math.round(ms / m) + 'm';
}
if (ms >= s) {
return Math.round(ms / s) + 's';
}
return ms + 'ms';
}
/**
* Long format for `ms`.
*
* @param {Number} ms
* @return {String}
* @api private
*/
function fmtLong(ms) {
return plural(ms, d, 'day') ||
plural(ms, h, 'hour') ||
plural(ms, m, 'minute') ||
plural(ms, s, 'second') ||
ms + ' ms';
}
/**
* Pluralization helper.
*/
function plural(ms, n, name) {
if (ms < n) {
return;
}
if (ms < n * 1.5) {
return Math.floor(ms / n) + ' ' + name;
}
return Math.ceil(ms / n) + ' ' + name + 's';
}

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Zeit, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,37 @@
{
"name": "ms",
"version": "2.0.0",
"description": "Tiny milisecond conversion utility",
"repository": "zeit/ms",
"main": "./index",
"files": [
"index.js"
],
"scripts": {
"precommit": "lint-staged",
"lint": "eslint lib/* bin/*",
"test": "mocha tests.js"
},
"eslintConfig": {
"extends": "eslint:recommended",
"env": {
"node": true,
"es6": true
}
},
"lint-staged": {
"*.js": [
"npm run lint",
"prettier --single-quote --write",
"git add"
]
},
"license": "MIT",
"devDependencies": {
"eslint": "3.19.0",
"expect.js": "0.3.1",
"husky": "0.13.3",
"lint-staged": "3.4.1",
"mocha": "3.4.1"
}
}

View File

@@ -0,0 +1,51 @@
# ms
[![Build Status](https://travis-ci.org/zeit/ms.svg?branch=master)](https://travis-ci.org/zeit/ms)
[![Slack Channel](http://zeit-slackin.now.sh/badge.svg)](https://zeit.chat/)
Use this package to easily convert various time formats to milliseconds.
## Examples
```js
ms('2 days') // 172800000
ms('1d') // 86400000
ms('10h') // 36000000
ms('2.5 hrs') // 9000000
ms('2h') // 7200000
ms('1m') // 60000
ms('5s') // 5000
ms('1y') // 31557600000
ms('100') // 100
```
### Convert from milliseconds
```js
ms(60000) // "1m"
ms(2 * 60000) // "2m"
ms(ms('10 hours')) // "10h"
```
### Time format written-out
```js
ms(60000, { long: true }) // "1 minute"
ms(2 * 60000, { long: true }) // "2 minutes"
ms(ms('10 hours'), { long: true }) // "10 hours"
```
## Features
- Works both in [node](https://nodejs.org) and in the browser.
- If a number is supplied to `ms`, a string with a unit is returned.
- If a string that contains the number is supplied, it returns it as a number (e.g.: it returns `100` for `'100'`).
- If you pass a string with a number and a valid unit, the number of equivalent ms is returned.
## Caught a bug?
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device
2. Link the package to the global module directory: `npm link`
3. Within the module you want to test your local development instance of ms, just link it to the dependencies: `npm link ms`. Instead of the default one from npm, node will now use your clone of ms!
As always, you can run the tests using: `npm test`

View File

@@ -0,0 +1,37 @@
{
"name": "metro-file-map",
"version": "0.80.12",
"description": "[Experimental] - 🚇 File crawling, watching and mapping for Metro",
"main": "src/index.js",
"repository": {
"type": "git",
"url": "git@github.com:facebook/metro.git"
},
"scripts": {
"prepare-release": "test -d build && rm -rf src.real && mv src src.real && mv build src",
"cleanup-release": "test ! -e build && mv src build && mv src.real src"
},
"license": "MIT",
"dependencies": {
"anymatch": "^3.0.3",
"debug": "^2.2.0",
"fb-watchman": "^2.0.0",
"flow-enums-runtime": "^0.0.6",
"graceful-fs": "^4.2.4",
"invariant": "^2.2.4",
"jest-worker": "^29.6.3",
"micromatch": "^4.0.4",
"node-abort-controller": "^3.1.1",
"nullthrows": "^1.1.1",
"walker": "^1.0.7"
},
"devDependencies": {
"slash": "^3.0.0"
},
"optionalDependencies": {
"fsevents": "^2.3.2"
},
"engines": {
"node": ">=18"
}
}

View File

@@ -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
*/
export type HealthCheckResult =
| {type: 'error'; timeout: number; error: Error; watcher: string | null}
| {
type: 'success';
timeout: number;
timeElapsed: number;
watcher: string | null;
}
| {
type: 'timeout';
timeout: number;
watcher: string | null;
pauseReason: string | null;
};

View File

@@ -0,0 +1,281 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.Watcher = void 0;
var _node = _interopRequireDefault(require("./crawlers/node"));
var _watchman = _interopRequireDefault(require("./crawlers/watchman"));
var _common = require("./watchers/common");
var _FSEventsWatcher = _interopRequireDefault(
require("./watchers/FSEventsWatcher")
);
var _NodeWatcher = _interopRequireDefault(require("./watchers/NodeWatcher"));
var _WatchmanWatcher = _interopRequireDefault(
require("./watchers/WatchmanWatcher")
);
var _events = _interopRequireDefault(require("events"));
var fs = _interopRequireWildcard(require("fs"));
var _nullthrows = _interopRequireDefault(require("nullthrows"));
var path = _interopRequireWildcard(require("path"));
var _perf_hooks = require("perf_hooks");
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function (nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interopRequireWildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) {
return { default: obj };
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {};
var hasPropertyDescriptor =
Object.defineProperty && Object.getOwnPropertyDescriptor;
for (var key in obj) {
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor
? Object.getOwnPropertyDescriptor(obj, key)
: null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const debug = require("debug")("Metro:Watcher");
const MAX_WAIT_TIME = 240000;
let nextInstanceId = 0;
class Watcher extends _events.default {
_backends = [];
_nextHealthCheckId = 0;
_pendingHealthChecks = new Map();
constructor(options) {
super();
this._options = options;
this._instanceId = nextInstanceId++;
}
async crawl() {
this._options.perfLogger?.point("crawl_start");
const options = this._options;
const ignore = (filePath) =>
options.ignore(filePath) ||
path.basename(filePath).startsWith(this._options.healthCheckFilePrefix);
const crawl = options.useWatchman ? _watchman.default : _node.default;
let crawler = crawl === _watchman.default ? "watchman" : "node";
options.abortSignal.throwIfAborted();
const crawlerOptions = {
abortSignal: options.abortSignal,
computeSha1: options.computeSha1,
console: options.console,
includeSymlinks: options.enableSymlinks,
extensions: options.extensions,
forceNodeFilesystemAPI: options.forceNodeFilesystemAPI,
ignore,
onStatus: (status) => {
this.emit("status", status);
},
perfLogger: options.perfLogger,
previousState: options.previousState,
rootDir: options.rootDir,
roots: options.roots,
};
const retry = (error) => {
if (crawl === _watchman.default) {
crawler = "node";
options.console.warn(
"metro-file-map: Watchman crawl failed. Retrying once with node " +
"crawler.\n" +
" Usually this happens when watchman isn't running. Create an " +
"empty `.watchmanconfig` file in your project's root folder or " +
"initialize a git or hg repository in your project.\n" +
" " +
error.toString()
);
return (0, _node.default)(crawlerOptions).catch((e) => {
throw new Error(
"Crawler retry failed:\n" +
` Original error: ${error.message}\n` +
` Retry error: ${e.message}\n`
);
});
}
throw error;
};
const logEnd = (delta) => {
debug(
'Crawler "%s" returned %d added/modified, %d removed, %d clock(s).',
crawler,
delta.changedFiles.size,
delta.removedFiles.size,
delta.clocks?.size ?? 0
);
this._options.perfLogger?.point("crawl_end");
return delta;
};
debug('Beginning crawl with "%s".', crawler);
try {
return crawl(crawlerOptions).catch(retry).then(logEnd);
} catch (error) {
return retry(error).then(logEnd);
}
}
async watch(onChange) {
const { extensions, ignorePattern, useWatchman } = this._options;
const WatcherImpl = useWatchman
? _WatchmanWatcher.default
: _FSEventsWatcher.default.isSupported()
? _FSEventsWatcher.default
: _NodeWatcher.default;
let watcher = "node";
if (WatcherImpl === _WatchmanWatcher.default) {
watcher = "watchman";
} else if (WatcherImpl === _FSEventsWatcher.default) {
watcher = "fsevents";
}
debug(`Using watcher: ${watcher}`);
this._options.perfLogger?.annotate({
string: {
watcher,
},
});
this._activeWatcher = watcher;
const createWatcherBackend = (root) => {
const watcherOptions = {
dot: true,
glob: [
"**/package.json",
"**/" + this._options.healthCheckFilePrefix + "*",
...extensions.map((extension) => "**/*." + extension),
],
ignored: ignorePattern,
watchmanDeferStates: this._options.watchmanDeferStates,
};
const watcher = new WatcherImpl(root, watcherOptions);
return new Promise((resolve, reject) => {
const rejectTimeout = setTimeout(
() => reject(new Error("Failed to start watch mode.")),
MAX_WAIT_TIME
);
watcher.once("ready", () => {
clearTimeout(rejectTimeout);
watcher.on("all", (type, filePath, root, metadata) => {
const basename = path.basename(filePath);
if (basename.startsWith(this._options.healthCheckFilePrefix)) {
if (type === _common.ADD_EVENT || type === _common.CHANGE_EVENT) {
debug(
"Observed possible health check cookie: %s in %s",
filePath,
root
);
this._handleHealthCheckObservation(basename);
}
return;
}
onChange(type, filePath, root, metadata);
});
resolve(watcher);
});
});
};
this._backends = await Promise.all(
this._options.roots.map(createWatcherBackend)
);
}
_handleHealthCheckObservation(basename) {
const resolveHealthCheck = this._pendingHealthChecks.get(basename);
if (!resolveHealthCheck) {
return;
}
resolveHealthCheck();
}
async close() {
await Promise.all(this._backends.map((watcher) => watcher.close()));
this._activeWatcher = null;
}
async checkHealth(timeout) {
const healthCheckId = this._nextHealthCheckId++;
if (healthCheckId === Number.MAX_SAFE_INTEGER) {
this._nextHealthCheckId = 0;
}
const watcher = this._activeWatcher;
const basename =
this._options.healthCheckFilePrefix +
"-" +
process.pid +
"-" +
this._instanceId +
"-" +
healthCheckId;
const healthCheckPath = path.join(this._options.rootDir, basename);
let result;
const timeoutPromise = new Promise((resolve) =>
setTimeout(resolve, timeout)
).then(() => {
if (!result) {
result = {
type: "timeout",
pauseReason: this._backends[0]?.getPauseReason(),
timeout,
watcher,
};
}
});
const startTime = _perf_hooks.performance.now();
debug("Creating health check cookie: %s", healthCheckPath);
const creationPromise = fs.promises
.writeFile(healthCheckPath, String(startTime))
.catch((error) => {
if (!result) {
result = {
type: "error",
error,
timeout,
watcher,
};
}
});
const observationPromise = new Promise((resolve) => {
this._pendingHealthChecks.set(basename, resolve);
}).then(() => {
if (!result) {
result = {
type: "success",
timeElapsed: _perf_hooks.performance.now() - startTime,
timeout,
watcher,
};
}
});
await Promise.race([
timeoutPromise,
creationPromise.then(() => observationPromise),
]);
this._pendingHealthChecks.delete(basename);
creationPromise.then(() =>
fs.promises.unlink(healthCheckPath).catch(() => {})
);
debug("Health check result: %o", result);
return (0, _nullthrows.default)(result);
}
}
exports.Watcher = Watcher;

View File

@@ -0,0 +1,331 @@
/**
* 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
* @flow strict-local
*/
import type {
ChangeEventMetadata,
Console,
CrawlerOptions,
FileData,
Path,
PerfLogger,
WatchmanClocks,
} from './flow-types';
import type {WatcherOptions as WatcherBackendOptions} from './watchers/common';
import type {AbortSignal} from 'node-abort-controller';
import nodeCrawl from './crawlers/node';
import watchmanCrawl from './crawlers/watchman';
import {ADD_EVENT, CHANGE_EVENT} from './watchers/common';
import FSEventsWatcher from './watchers/FSEventsWatcher';
import NodeWatcher from './watchers/NodeWatcher';
import WatchmanWatcher from './watchers/WatchmanWatcher';
import EventEmitter from 'events';
import * as fs from 'fs';
import nullthrows from 'nullthrows';
import * as path from 'path';
import {performance} from 'perf_hooks';
const debug = require('debug')('Metro:Watcher');
const MAX_WAIT_TIME = 240000;
type CrawlResult = {
changedFiles: FileData,
clocks?: WatchmanClocks,
removedFiles: Set<Path>,
};
type WatcherOptions = {
abortSignal: AbortSignal,
computeSha1: boolean,
console: Console,
enableSymlinks: boolean,
extensions: $ReadOnlyArray<string>,
forceNodeFilesystemAPI: boolean,
healthCheckFilePrefix: string,
ignore: string => boolean,
ignorePattern: RegExp,
previousState: CrawlerOptions['previousState'],
perfLogger: ?PerfLogger,
roots: $ReadOnlyArray<string>,
rootDir: string,
useWatchman: boolean,
watch: boolean,
watchmanDeferStates: $ReadOnlyArray<string>,
};
interface WatcherBackend {
getPauseReason(): ?string;
close(): Promise<void>;
}
let nextInstanceId = 0;
export type HealthCheckResult =
| {type: 'error', timeout: number, error: Error, watcher: ?string}
| {type: 'success', timeout: number, timeElapsed: number, watcher: ?string}
| {type: 'timeout', timeout: number, watcher: ?string, pauseReason: ?string};
export class Watcher extends EventEmitter {
_options: WatcherOptions;
_backends: $ReadOnlyArray<WatcherBackend> = [];
_instanceId: number;
_nextHealthCheckId: number = 0;
_pendingHealthChecks: Map</* basename */ string, /* resolve */ () => void> =
new Map();
_activeWatcher: ?string;
constructor(options: WatcherOptions) {
super();
this._options = options;
this._instanceId = nextInstanceId++;
}
async crawl(): Promise<CrawlResult> {
this._options.perfLogger?.point('crawl_start');
const options = this._options;
const ignore = (filePath: string) =>
options.ignore(filePath) ||
path.basename(filePath).startsWith(this._options.healthCheckFilePrefix);
const crawl = options.useWatchman ? watchmanCrawl : nodeCrawl;
let crawler = crawl === watchmanCrawl ? 'watchman' : 'node';
options.abortSignal.throwIfAborted();
const crawlerOptions: CrawlerOptions = {
abortSignal: options.abortSignal,
computeSha1: options.computeSha1,
console: options.console,
includeSymlinks: options.enableSymlinks,
extensions: options.extensions,
forceNodeFilesystemAPI: options.forceNodeFilesystemAPI,
ignore,
onStatus: status => {
this.emit('status', status);
},
perfLogger: options.perfLogger,
previousState: options.previousState,
rootDir: options.rootDir,
roots: options.roots,
};
const retry = (error: Error): Promise<CrawlResult> => {
if (crawl === watchmanCrawl) {
crawler = 'node';
options.console.warn(
'metro-file-map: Watchman crawl failed. Retrying once with node ' +
'crawler.\n' +
" Usually this happens when watchman isn't running. Create an " +
"empty `.watchmanconfig` file in your project's root folder or " +
'initialize a git or hg repository in your project.\n' +
' ' +
error.toString(),
);
// $FlowFixMe[prop-missing] Found when updating Promise type definition
return nodeCrawl(crawlerOptions).catch<CrawlResult>(e => {
throw new Error(
'Crawler retry failed:\n' +
` Original error: ${error.message}\n` +
` Retry error: ${e.message}\n`,
);
});
}
throw error;
};
const logEnd = (delta: CrawlResult): CrawlResult => {
debug(
'Crawler "%s" returned %d added/modified, %d removed, %d clock(s).',
crawler,
delta.changedFiles.size,
delta.removedFiles.size,
delta.clocks?.size ?? 0,
);
this._options.perfLogger?.point('crawl_end');
return delta;
};
debug('Beginning crawl with "%s".', crawler);
try {
// $FlowFixMe[incompatible-call] Found when updating Promise type definition
return crawl(crawlerOptions).catch<CrawlResult>(retry).then(logEnd);
} catch (error) {
return retry(error).then(logEnd);
}
}
async watch(
onChange: (
type: string,
filePath: string,
root: string,
metadata: ChangeEventMetadata,
) => void,
) {
const {extensions, ignorePattern, useWatchman} = this._options;
// WatchmanWatcher > FSEventsWatcher > sane.NodeWatcher
const WatcherImpl = useWatchman
? WatchmanWatcher
: FSEventsWatcher.isSupported()
? FSEventsWatcher
: NodeWatcher;
let watcher = 'node';
if (WatcherImpl === WatchmanWatcher) {
watcher = 'watchman';
} else if (WatcherImpl === FSEventsWatcher) {
watcher = 'fsevents';
}
debug(`Using watcher: ${watcher}`);
this._options.perfLogger?.annotate({string: {watcher}});
this._activeWatcher = watcher;
const createWatcherBackend = (root: Path): Promise<WatcherBackend> => {
const watcherOptions: WatcherBackendOptions = {
dot: true,
glob: [
// Ensure we always include package.json files, which are crucial for
/// module resolution.
'**/package.json',
// Ensure we always watch any health check files
'**/' + this._options.healthCheckFilePrefix + '*',
...extensions.map(extension => '**/*.' + extension),
],
ignored: ignorePattern,
watchmanDeferStates: this._options.watchmanDeferStates,
};
const watcher = new WatcherImpl(root, watcherOptions);
return new Promise((resolve, reject) => {
const rejectTimeout = setTimeout(
() => reject(new Error('Failed to start watch mode.')),
MAX_WAIT_TIME,
);
watcher.once('ready', () => {
clearTimeout(rejectTimeout);
watcher.on(
'all',
(
type: string,
filePath: string,
root: string,
metadata: ChangeEventMetadata,
) => {
const basename = path.basename(filePath);
if (basename.startsWith(this._options.healthCheckFilePrefix)) {
if (type === ADD_EVENT || type === CHANGE_EVENT) {
debug(
'Observed possible health check cookie: %s in %s',
filePath,
root,
);
this._handleHealthCheckObservation(basename);
}
return;
}
onChange(type, filePath, root, metadata);
},
);
resolve(watcher);
});
});
};
this._backends = await Promise.all(
this._options.roots.map(createWatcherBackend),
);
}
_handleHealthCheckObservation(basename: string) {
const resolveHealthCheck = this._pendingHealthChecks.get(basename);
if (!resolveHealthCheck) {
return;
}
resolveHealthCheck();
}
async close() {
await Promise.all(this._backends.map(watcher => watcher.close()));
this._activeWatcher = null;
}
async checkHealth(timeout: number): Promise<HealthCheckResult> {
const healthCheckId = this._nextHealthCheckId++;
if (healthCheckId === Number.MAX_SAFE_INTEGER) {
this._nextHealthCheckId = 0;
}
const watcher = this._activeWatcher;
const basename =
this._options.healthCheckFilePrefix +
'-' +
process.pid +
'-' +
this._instanceId +
'-' +
healthCheckId;
const healthCheckPath = path.join(this._options.rootDir, basename);
let result: ?HealthCheckResult;
const timeoutPromise = new Promise(resolve =>
setTimeout(resolve, timeout),
).then(() => {
if (!result) {
result = {
type: 'timeout',
pauseReason: this._backends[0]?.getPauseReason(),
timeout,
watcher,
};
}
});
const startTime = performance.now();
debug('Creating health check cookie: %s', healthCheckPath);
const creationPromise = fs.promises
.writeFile(healthCheckPath, String(startTime))
.catch(error => {
if (!result) {
result = {
type: 'error',
error,
timeout,
watcher,
};
}
});
const observationPromise = new Promise(resolve => {
this._pendingHealthChecks.set(basename, resolve);
}).then(() => {
if (!result) {
result = {
type: 'success',
timeElapsed: performance.now() - startTime,
timeout,
watcher,
};
}
});
await Promise.race([
timeoutPromise,
creationPromise.then(() => observationPromise),
]);
this._pendingHealthChecks.delete(basename);
// Chain a deletion to the creation promise (which may not have even settled yet!),
// don't await it, and swallow errors. This is just best-effort cleanup.
// $FlowFixMe[unused-promise]
creationPromise.then(() =>
fs.promises.unlink(healthCheckPath).catch(() => {}),
);
debug('Health check result: %o', result);
return nullthrows(result);
}
}

View File

@@ -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 {
BuildParameters,
CacheData,
CacheManager,
FileData,
} from '../flow-types';
export interface DiskCacheConfig {
buildParameters: BuildParameters;
cacheFilePrefix?: string | null;
cacheDirectory?: string | null;
}
export class DiskCacheManager implements CacheManager {
constructor(options: DiskCacheConfig);
static getCacheFilePath(
buildParameters: BuildParameters,
cacheFilePrefix?: string | null,
cacheDirectory?: string | null,
): string;
getCacheFilePath(): string;
read(): Promise<CacheData | null>;
write(
dataSnapshot: CacheData,
{changed, removed}: Readonly<{changed: FileData; removed: FileData}>,
): Promise<void>;
}

View File

@@ -0,0 +1,61 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.DiskCacheManager = void 0;
var _rootRelativeCacheKeys = _interopRequireDefault(
require("../lib/rootRelativeCacheKeys")
);
var _gracefulFs = require("graceful-fs");
var _os = require("os");
var _path = _interopRequireDefault(require("path"));
var _v = require("v8");
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const DEFAULT_PREFIX = "metro-file-map";
const DEFAULT_DIRECTORY = (0, _os.tmpdir)();
class DiskCacheManager {
constructor({ buildParameters, cacheDirectory, cacheFilePrefix }) {
this._cachePath = DiskCacheManager.getCacheFilePath(
buildParameters,
cacheFilePrefix,
cacheDirectory
);
}
static getCacheFilePath(buildParameters, cacheFilePrefix, cacheDirectory) {
const { rootDirHash, relativeConfigHash } = (0,
_rootRelativeCacheKeys.default)(buildParameters);
return _path.default.join(
cacheDirectory ?? DEFAULT_DIRECTORY,
`${
cacheFilePrefix ?? DEFAULT_PREFIX
}-${rootDirHash}-${relativeConfigHash}`
);
}
getCacheFilePath() {
return this._cachePath;
}
async read() {
try {
return (0, _v.deserialize)(
(0, _gracefulFs.readFileSync)(this._cachePath)
);
} catch (e) {
if (e?.code === "ENOENT") {
return null;
}
throw e;
}
}
async write(dataSnapshot, { changed, removed }) {
if (changed.size > 0 || removed.size > 0) {
(0, _gracefulFs.writeFileSync)(
this._cachePath,
(0, _v.serialize)(dataSnapshot)
);
}
}
}
exports.DiskCacheManager = DiskCacheManager;

View File

@@ -0,0 +1,90 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import type {
BuildParameters,
CacheData,
CacheDelta,
CacheManager,
} from '../flow-types';
import rootRelativeCacheKeys from '../lib/rootRelativeCacheKeys';
import {readFileSync, writeFileSync} from 'graceful-fs';
import {tmpdir} from 'os';
import path from 'path';
import {deserialize, serialize} from 'v8';
type DiskCacheConfig = {
buildParameters: BuildParameters,
cacheFilePrefix?: ?string,
cacheDirectory?: ?string,
};
const DEFAULT_PREFIX = 'metro-file-map';
const DEFAULT_DIRECTORY = tmpdir();
export class DiskCacheManager implements CacheManager {
_cachePath: string;
constructor({
buildParameters,
cacheDirectory,
cacheFilePrefix,
}: DiskCacheConfig) {
this._cachePath = DiskCacheManager.getCacheFilePath(
buildParameters,
cacheFilePrefix,
cacheDirectory,
);
}
static getCacheFilePath(
buildParameters: BuildParameters,
cacheFilePrefix?: ?string,
cacheDirectory?: ?string,
): string {
const {rootDirHash, relativeConfigHash} =
rootRelativeCacheKeys(buildParameters);
return path.join(
cacheDirectory ?? DEFAULT_DIRECTORY,
`${
cacheFilePrefix ?? DEFAULT_PREFIX
}-${rootDirHash}-${relativeConfigHash}`,
);
}
getCacheFilePath(): string {
return this._cachePath;
}
async read(): Promise<?CacheData> {
try {
return deserialize(readFileSync(this._cachePath));
} catch (e) {
if (e?.code === 'ENOENT') {
// Cache file not found - not considered an error.
return null;
}
// Rethrow anything else.
throw e;
}
}
async write(
dataSnapshot: CacheData,
{changed, removed}: CacheDelta,
): Promise<void> {
if (changed.size > 0 || removed.size > 0) {
writeFileSync(this._cachePath, serialize(dataSnapshot));
}
}
}

View File

@@ -0,0 +1,19 @@
"use strict";
const constants = {
DEPENDENCY_DELIM: "\0",
ID: 0,
MTIME: 1,
SIZE: 2,
VISITED: 3,
DEPENDENCIES: 4,
SHA1: 5,
SYMLINK: 6,
PATH: 0,
TYPE: 1,
MODULE: 0,
PACKAGE: 1,
GENERIC_PLATFORM: "g",
NATIVE_PLATFORM: "native",
};
module.exports = constants;

View File

@@ -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.
*
* @flow strict-local
* @noformat - Flow comment syntax
*/
/*
* This file exports a set of constants that are used for Jest's haste map
* serialization. On very large repositories, the haste map cache becomes very
* large to the point where it is the largest overhead in starting up Jest.
*
* This constant key map allows to keep the map smaller without having to build
* a custom serialization library.
*/
/*::
import type {HType} from './flow-types';
*/
'use strict';
const constants/*: HType */ = {
/* dependency serialization */
DEPENDENCY_DELIM: '\0',
/* file map attributes */
ID: 0,
MTIME: 1,
SIZE: 2,
VISITED: 3,
DEPENDENCIES: 4,
SHA1: 5,
SYMLINK: 6,
/* module map attributes */
PATH: 0,
TYPE: 1,
/* module types */
MODULE: 0,
PACKAGE: 1,
/* platforms */
GENERIC_PLATFORM: 'g',
NATIVE_PLATFORM: 'native',
};
module.exports = constants;

View File

@@ -0,0 +1 @@
"use strict";

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/

View File

@@ -0,0 +1 @@
"use strict";

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/

View File

@@ -0,0 +1 @@
"use strict";

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/

View File

@@ -0,0 +1 @@
"use strict";

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/

View File

@@ -0,0 +1 @@
"use strict";

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/

View File

@@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = hasNativeFindSupport;
var _child_process = require("child_process");
async function hasNativeFindSupport() {
try {
return await new Promise((resolve) => {
const args = [
".",
"-type",
"f",
"(",
"-iname",
"*.ts",
"-o",
"-iname",
"*.js",
")",
];
const child = (0, _child_process.spawn)("find", args, {
cwd: __dirname,
});
child.on("error", () => {
resolve(false);
});
child.on("exit", (code) => {
resolve(code === 0);
});
});
} catch {
return false;
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import {spawn} from 'child_process';
export default async function hasNativeFindSupport(): Promise<boolean> {
try {
return await new Promise(resolve => {
// Check the find binary supports the non-POSIX -iname parameter wrapped in parens.
const args = [
'.',
'-type',
'f',
'(',
'-iname',
'*.ts',
'-o',
'-iname',
'*.js',
')',
];
const child = spawn('find', args, {cwd: __dirname});
child.on('error', () => {
resolve(false);
});
child.on('exit', code => {
resolve(code === 0);
});
});
} catch {
return false;
}
}

View File

@@ -0,0 +1,242 @@
"use strict";
var _RootPathUtils = require("../../lib/RootPathUtils");
var _hasNativeFindSupport = _interopRequireDefault(
require("./hasNativeFindSupport")
);
var _child_process = require("child_process");
var fs = _interopRequireWildcard(require("graceful-fs"));
var _os = require("os");
var path = _interopRequireWildcard(require("path"));
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function (nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interopRequireWildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) {
return { default: obj };
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {};
var hasPropertyDescriptor =
Object.defineProperty && Object.getOwnPropertyDescriptor;
for (var key in obj) {
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor
? Object.getOwnPropertyDescriptor(obj, key)
: null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const debug = require("debug")("Metro:NodeCrawler");
function find(
roots,
extensions,
ignore,
includeSymlinks,
rootDir,
console,
callback
) {
const result = new Map();
let activeCalls = 0;
const pathUtils = new _RootPathUtils.RootPathUtils(rootDir);
function search(directory) {
activeCalls++;
fs.readdir(
directory,
{
withFileTypes: true,
},
(err, entries) => {
activeCalls--;
if (err) {
console.warn(
`Error "${
err.code ?? err.message
}" reading contents of "${directory}", skipping. Add this directory to your ignore list to exclude it.`
);
} else {
entries.forEach((entry) => {
const file = path.join(directory, entry.name.toString());
if (ignore(file)) {
return;
}
if (entry.isSymbolicLink() && !includeSymlinks) {
return;
}
if (entry.isDirectory()) {
search(file);
return;
}
activeCalls++;
fs.lstat(file, (err, stat) => {
activeCalls--;
if (!err && stat) {
const ext = path.extname(file).substr(1);
if (stat.isSymbolicLink() || extensions.includes(ext)) {
result.set(pathUtils.absoluteToNormal(file), [
"",
stat.mtime.getTime(),
stat.size,
0,
"",
null,
stat.isSymbolicLink() ? 1 : 0,
]);
}
}
if (activeCalls === 0) {
callback(result);
}
});
});
}
if (activeCalls === 0) {
callback(result);
}
}
);
}
if (roots.length > 0) {
roots.forEach(search);
} else {
callback(result);
}
}
function findNative(
roots,
extensions,
ignore,
includeSymlinks,
rootDir,
console,
callback
) {
const extensionClause = extensions.length
? `( ${extensions.map((ext) => `-iname *.${ext}`).join(" -o ")} )`
: "";
const expression = `( ( -type f ${extensionClause} ) ${
includeSymlinks ? "-o -type l " : ""
})`;
const pathUtils = new _RootPathUtils.RootPathUtils(rootDir);
const child = (0, _child_process.spawn)(
"find",
roots.concat(expression.split(" "))
);
let stdout = "";
if (child.stdout == null) {
throw new Error(
"stdout is null - this should never happen. Please open up an issue at https://github.com/facebook/metro"
);
}
child.stdout.setEncoding("utf-8");
child.stdout.on("data", (data) => (stdout += data));
child.stdout.on("close", () => {
const lines = stdout
.trim()
.split("\n")
.filter((x) => !ignore(x));
const result = new Map();
let count = lines.length;
if (!count) {
callback(new Map());
} else {
lines.forEach((path) => {
fs.lstat(path, (err, stat) => {
if (!err && stat) {
result.set(pathUtils.absoluteToNormal(path), [
"",
stat.mtime.getTime(),
stat.size,
0,
"",
null,
stat.isSymbolicLink() ? 1 : 0,
]);
}
if (--count === 0) {
callback(result);
}
});
});
}
});
}
module.exports = async function nodeCrawl(options) {
const {
console,
previousState,
extensions,
forceNodeFilesystemAPI,
ignore,
rootDir,
includeSymlinks,
perfLogger,
roots,
abortSignal,
} = options;
abortSignal?.throwIfAborted();
perfLogger?.point("nodeCrawl_start");
const useNativeFind =
!forceNodeFilesystemAPI &&
(0, _os.platform)() !== "win32" &&
(await (0, _hasNativeFindSupport.default)());
debug("Using system find: %s", useNativeFind);
return new Promise((resolve, reject) => {
const callback = (fileData) => {
const difference = previousState.fileSystem.getDifference(fileData);
perfLogger?.point("nodeCrawl_end");
try {
abortSignal?.throwIfAborted();
} catch (e) {
reject(e);
}
resolve(difference);
};
if (useNativeFind) {
findNative(
roots,
extensions,
ignore,
includeSymlinks,
rootDir,
console,
callback
);
} else {
find(
roots,
extensions,
ignore,
includeSymlinks,
rootDir,
console,
callback
);
}
});
};

View File

@@ -0,0 +1,238 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import type {
CanonicalPath,
Console,
CrawlerOptions,
FileData,
IgnoreMatcher,
} from '../../flow-types';
import {RootPathUtils} from '../../lib/RootPathUtils';
import hasNativeFindSupport from './hasNativeFindSupport';
import {spawn} from 'child_process';
import * as fs from 'graceful-fs';
import {platform} from 'os';
import * as path from 'path';
const debug = require('debug')('Metro:NodeCrawler');
type Callback = (result: FileData) => void;
function find(
roots: $ReadOnlyArray<string>,
extensions: $ReadOnlyArray<string>,
ignore: IgnoreMatcher,
includeSymlinks: boolean,
rootDir: string,
console: Console,
callback: Callback,
): void {
const result: FileData = new Map();
let activeCalls = 0;
const pathUtils = new RootPathUtils(rootDir);
function search(directory: string): void {
activeCalls++;
fs.readdir(directory, {withFileTypes: true}, (err, entries) => {
activeCalls--;
if (err) {
console.warn(
`Error "${err.code ?? err.message}" reading contents of "${directory}", skipping. Add this directory to your ignore list to exclude it.`,
);
} else {
entries.forEach((entry: fs.Dirent) => {
const file = path.join(directory, entry.name.toString());
if (ignore(file)) {
return;
}
if (entry.isSymbolicLink() && !includeSymlinks) {
return;
}
if (entry.isDirectory()) {
search(file);
return;
}
activeCalls++;
fs.lstat(file, (err, stat) => {
activeCalls--;
if (!err && stat) {
const ext = path.extname(file).substr(1);
if (stat.isSymbolicLink() || extensions.includes(ext)) {
result.set(pathUtils.absoluteToNormal(file), [
'',
stat.mtime.getTime(),
stat.size,
0,
'',
null,
stat.isSymbolicLink() ? 1 : 0,
]);
}
}
if (activeCalls === 0) {
callback(result);
}
});
});
}
if (activeCalls === 0) {
callback(result);
}
});
}
if (roots.length > 0) {
roots.forEach(search);
} else {
callback(result);
}
}
function findNative(
roots: $ReadOnlyArray<string>,
extensions: $ReadOnlyArray<string>,
ignore: IgnoreMatcher,
includeSymlinks: boolean,
rootDir: string,
console: Console,
callback: Callback,
): void {
// Examples:
// ( ( -type f ( -iname *.js ) ) )
// ( ( -type f ( -iname *.js -o -iname *.ts ) ) )
// ( ( -type f ( -iname *.js ) ) -o -type l )
// ( ( -type f ) -o -type l )
const extensionClause = extensions.length
? `( ${extensions.map(ext => `-iname *.${ext}`).join(' -o ')} )`
: ''; // Empty inner expressions eg "( )" are not allowed
const expression = `( ( -type f ${extensionClause} ) ${
includeSymlinks ? '-o -type l ' : ''
})`;
const pathUtils = new RootPathUtils(rootDir);
const child = spawn('find', roots.concat(expression.split(' ')));
let stdout = '';
if (child.stdout == null) {
throw new Error(
'stdout is null - this should never happen. Please open up an issue at https://github.com/facebook/metro',
);
}
child.stdout.setEncoding('utf-8');
child.stdout.on('data', data => (stdout += data));
child.stdout.on('close', () => {
const lines = stdout
.trim()
.split('\n')
.filter(x => !ignore(x));
const result: FileData = new Map();
let count = lines.length;
if (!count) {
callback(new Map());
} else {
lines.forEach(path => {
fs.lstat(path, (err, stat) => {
if (!err && stat) {
result.set(pathUtils.absoluteToNormal(path), [
'',
stat.mtime.getTime(),
stat.size,
0,
'',
null,
stat.isSymbolicLink() ? 1 : 0,
]);
}
if (--count === 0) {
callback(result);
}
});
});
}
});
}
module.exports = async function nodeCrawl(options: CrawlerOptions): Promise<{
removedFiles: Set<CanonicalPath>,
changedFiles: FileData,
}> {
const {
console,
previousState,
extensions,
forceNodeFilesystemAPI,
ignore,
rootDir,
includeSymlinks,
perfLogger,
roots,
abortSignal,
} = options;
abortSignal?.throwIfAborted();
perfLogger?.point('nodeCrawl_start');
const useNativeFind =
!forceNodeFilesystemAPI &&
platform() !== 'win32' &&
(await hasNativeFindSupport());
debug('Using system find: %s', useNativeFind);
return new Promise((resolve, reject) => {
const callback: Callback = fileData => {
const difference = previousState.fileSystem.getDifference(fileData);
perfLogger?.point('nodeCrawl_end');
try {
// TODO: Use AbortSignal.reason directly when Flow supports it
abortSignal?.throwIfAborted();
} catch (e) {
reject(e);
}
resolve(difference);
};
if (useNativeFind) {
findNative(
roots,
extensions,
ignore,
includeSymlinks,
rootDir,
console,
callback,
);
} else {
find(
roots,
extensions,
ignore,
includeSymlinks,
rootDir,
console,
callback,
);
}
});
};

View File

@@ -0,0 +1,307 @@
"use strict";
var _normalizePathSeparatorsToPosix = _interopRequireDefault(
require("../../lib/normalizePathSeparatorsToPosix")
);
var _normalizePathSeparatorsToSystem = _interopRequireDefault(
require("../../lib/normalizePathSeparatorsToSystem")
);
var _RootPathUtils = require("../../lib/RootPathUtils");
var _planQuery = require("./planQuery");
var _invariant = _interopRequireDefault(require("invariant"));
var path = _interopRequireWildcard(require("path"));
var _perf_hooks = require("perf_hooks");
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function (nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interopRequireWildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) {
return { default: obj };
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {};
var hasPropertyDescriptor =
Object.defineProperty && Object.getOwnPropertyDescriptor;
for (var key in obj) {
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor
? Object.getOwnPropertyDescriptor(obj, key)
: null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const watchman = require("fb-watchman");
const WATCHMAN_WARNING_INITIAL_DELAY_MILLISECONDS = 10000;
const WATCHMAN_WARNING_INTERVAL_MILLISECONDS = 20000;
const watchmanURL = "https://facebook.github.io/watchman/docs/troubleshooting";
function makeWatchmanError(error) {
error.message =
`Watchman error: ${error.message.trim()}. Make sure watchman ` +
`is running for this project. See ${watchmanURL}.`;
return error;
}
module.exports = async function watchmanCrawl({
abortSignal,
computeSha1,
extensions,
ignore,
includeSymlinks,
onStatus,
perfLogger,
previousState,
rootDir,
roots,
}) {
abortSignal?.throwIfAborted();
const client = new watchman.Client();
const pathUtils = new _RootPathUtils.RootPathUtils(rootDir);
abortSignal?.addEventListener("abort", () => client.end());
perfLogger?.point("watchmanCrawl_start");
const newClocks = new Map();
let clientError;
client.on("error", (error) => {
clientError = makeWatchmanError(error);
});
const cmd = async (command, ...args) => {
let didLogWatchmanWaitMessage = false;
const startTime = _perf_hooks.performance.now();
const logWatchmanWaitMessage = () => {
didLogWatchmanWaitMessage = true;
onStatus({
type: "watchman_slow_command",
timeElapsed: _perf_hooks.performance.now() - startTime,
command,
});
};
let intervalOrTimeoutId = setTimeout(() => {
logWatchmanWaitMessage();
intervalOrTimeoutId = setInterval(
logWatchmanWaitMessage,
WATCHMAN_WARNING_INTERVAL_MILLISECONDS
);
}, WATCHMAN_WARNING_INITIAL_DELAY_MILLISECONDS);
try {
const response = await new Promise((resolve, reject) =>
client.command([command, ...args], (error, result) =>
error ? reject(makeWatchmanError(error)) : resolve(result)
)
);
if ("warning" in response) {
onStatus({
type: "watchman_warning",
warning: response.warning,
command,
});
}
return response;
} finally {
clearInterval(intervalOrTimeoutId);
if (didLogWatchmanWaitMessage) {
onStatus({
type: "watchman_slow_command_complete",
timeElapsed: _perf_hooks.performance.now() - startTime,
command,
});
}
}
};
async function getWatchmanRoots(roots) {
perfLogger?.point("watchmanCrawl/getWatchmanRoots_start");
const watchmanRoots = new Map();
await Promise.all(
roots.map(async (root, index) => {
perfLogger?.point(`watchmanCrawl/watchProject_${index}_start`);
const response = await cmd("watch-project", root);
perfLogger?.point(`watchmanCrawl/watchProject_${index}_end`);
const existing = watchmanRoots.get(response.watch);
const canBeFiltered = !existing || existing.directoryFilters.length > 0;
if (canBeFiltered) {
if (response.relative_path) {
watchmanRoots.set(response.watch, {
watcher: response.watcher,
directoryFilters: (existing?.directoryFilters || []).concat(
response.relative_path
),
});
} else {
watchmanRoots.set(response.watch, {
watcher: response.watcher,
directoryFilters: [],
});
}
}
})
);
perfLogger?.point("watchmanCrawl/getWatchmanRoots_end");
return watchmanRoots;
}
async function queryWatchmanForDirs(rootProjectDirMappings) {
perfLogger?.point("watchmanCrawl/queryWatchmanForDirs_start");
const results = new Map();
let isFresh = false;
await Promise.all(
Array.from(rootProjectDirMappings).map(
async ([posixSeparatedRoot, { directoryFilters, watcher }], index) => {
const since = previousState.clocks.get(
(0, _normalizePathSeparatorsToPosix.default)(
pathUtils.absoluteToNormal(
(0, _normalizePathSeparatorsToSystem.default)(
posixSeparatedRoot
)
)
)
);
perfLogger?.annotate({
bool: {
[`watchmanCrawl/query_${index}_has_clock`]: since != null,
},
});
const { query, queryGenerator } = (0, _planQuery.planQuery)({
since,
extensions,
directoryFilters,
includeSha1: computeSha1,
includeSymlinks,
});
perfLogger?.annotate({
string: {
[`watchmanCrawl/query_${index}_watcher`]: watcher ?? "unknown",
[`watchmanCrawl/query_${index}_generator`]: queryGenerator,
},
});
perfLogger?.point(`watchmanCrawl/query_${index}_start`);
const response = await cmd("query", posixSeparatedRoot, query);
perfLogger?.point(`watchmanCrawl/query_${index}_end`);
const isSourceControlQuery =
typeof since !== "string" && since?.scm?.["mergebase-with"] != null;
if (!isSourceControlQuery) {
isFresh = isFresh || response.is_fresh_instance;
}
results.set(posixSeparatedRoot, response);
}
)
);
perfLogger?.point("watchmanCrawl/queryWatchmanForDirs_end");
return {
isFresh,
results,
};
}
let removedFiles = new Set();
let changedFiles = new Map();
let results;
let isFresh = false;
let queryError;
try {
const watchmanRoots = await getWatchmanRoots(roots);
const watchmanFileResults = await queryWatchmanForDirs(watchmanRoots);
results = watchmanFileResults.results;
isFresh = watchmanFileResults.isFresh;
} catch (e) {
queryError = e;
}
client.end();
if (results == null) {
if (clientError) {
perfLogger?.annotate({
string: {
"watchmanCrawl/client_error":
clientError.message ?? "[message missing]",
},
});
}
if (queryError) {
perfLogger?.annotate({
string: {
"watchmanCrawl/query_error":
queryError.message ?? "[message missing]",
},
});
}
perfLogger?.point("watchmanCrawl_end");
abortSignal?.throwIfAborted();
throw (
queryError ?? clientError ?? new Error("Watchman file results missing")
);
}
perfLogger?.point("watchmanCrawl/processResults_start");
const freshFileData = new Map();
for (const [watchRoot, response] of results) {
const fsRoot = (0, _normalizePathSeparatorsToSystem.default)(watchRoot);
const relativeFsRoot = pathUtils.absoluteToNormal(fsRoot);
newClocks.set(
(0, _normalizePathSeparatorsToPosix.default)(relativeFsRoot),
typeof response.clock === "string" ? response.clock : response.clock.clock
);
for (const fileData of response.files) {
const filePath =
fsRoot +
path.sep +
(0, _normalizePathSeparatorsToSystem.default)(fileData.name);
const relativeFilePath = pathUtils.absoluteToNormal(filePath);
if (!fileData.exists) {
if (!isFresh) {
removedFiles.add(relativeFilePath);
}
} else if (!ignore(filePath)) {
const { mtime_ms, size } = fileData;
(0, _invariant.default)(
mtime_ms != null && size != null,
"missing file data in watchman response"
);
const mtime =
typeof mtime_ms === "number" ? mtime_ms : mtime_ms.toNumber();
let sha1hex = fileData["content.sha1hex"];
if (typeof sha1hex !== "string" || sha1hex.length !== 40) {
sha1hex = undefined;
}
let symlinkInfo = 0;
if (fileData.type === "l") {
symlinkInfo = fileData["symlink_target"] ?? 1;
}
const nextData = ["", mtime, size, 0, "", sha1hex ?? null, symlinkInfo];
if (isFresh) {
freshFileData.set(relativeFilePath, nextData);
} else {
changedFiles.set(relativeFilePath, nextData);
}
}
}
}
if (isFresh) {
({ changedFiles, removedFiles } =
previousState.fileSystem.getDifference(freshFileData));
}
perfLogger?.point("watchmanCrawl/processResults_end");
perfLogger?.point("watchmanCrawl_end");
abortSignal?.throwIfAborted();
return {
changedFiles,
removedFiles,
clocks: newClocks,
};
};

View File

@@ -0,0 +1,370 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import type {WatchmanClockSpec} from '../../flow-types';
import type {
CanonicalPath,
CrawlerOptions,
FileData,
FileMetaData,
Path,
WatchmanClocks,
} from '../../flow-types';
import type {WatchmanQueryResponse, WatchmanWatchResponse} from 'fb-watchman';
import normalizePathSeparatorsToPosix from '../../lib/normalizePathSeparatorsToPosix';
import normalizePathSeparatorsToSystem from '../../lib/normalizePathSeparatorsToSystem';
import {RootPathUtils} from '../../lib/RootPathUtils';
import {planQuery} from './planQuery';
import invariant from 'invariant';
import * as path from 'path';
import {performance} from 'perf_hooks';
const watchman = require('fb-watchman');
type WatchmanRoots = Map<
string, // Posix-separated absolute path
$ReadOnly<{directoryFilters: Array<string>, watcher: string}>,
>;
const WATCHMAN_WARNING_INITIAL_DELAY_MILLISECONDS = 10000;
const WATCHMAN_WARNING_INTERVAL_MILLISECONDS = 20000;
const watchmanURL = 'https://facebook.github.io/watchman/docs/troubleshooting';
function makeWatchmanError(error: Error): Error {
error.message =
`Watchman error: ${error.message.trim()}. Make sure watchman ` +
`is running for this project. See ${watchmanURL}.`;
return error;
}
module.exports = async function watchmanCrawl({
abortSignal,
computeSha1,
extensions,
ignore,
includeSymlinks,
onStatus,
perfLogger,
previousState,
rootDir,
roots,
}: CrawlerOptions): Promise<{
changedFiles: FileData,
removedFiles: Set<CanonicalPath>,
clocks: WatchmanClocks,
}> {
abortSignal?.throwIfAborted();
const client = new watchman.Client();
const pathUtils = new RootPathUtils(rootDir);
abortSignal?.addEventListener('abort', () => client.end());
perfLogger?.point('watchmanCrawl_start');
const newClocks = new Map<Path, WatchmanClockSpec>();
let clientError;
client.on('error', error => {
clientError = makeWatchmanError(error);
});
const cmd = async <T>(
command: 'watch-project' | 'query',
// $FlowFixMe[unclear-type] - Fix to use fb-watchman types
...args: Array<any>
): Promise<T> => {
let didLogWatchmanWaitMessage = false;
const startTime = performance.now();
const logWatchmanWaitMessage = () => {
didLogWatchmanWaitMessage = true;
onStatus({
type: 'watchman_slow_command',
timeElapsed: performance.now() - startTime,
command,
});
};
let intervalOrTimeoutId: TimeoutID | IntervalID = setTimeout(() => {
logWatchmanWaitMessage();
intervalOrTimeoutId = setInterval(
logWatchmanWaitMessage,
WATCHMAN_WARNING_INTERVAL_MILLISECONDS,
);
}, WATCHMAN_WARNING_INITIAL_DELAY_MILLISECONDS);
try {
const response = await new Promise<WatchmanQueryResponse>(
(resolve, reject) =>
// $FlowFixMe[incompatible-call] - dynamic call of command
client.command(
[command, ...args],
(error: ?Error, result: WatchmanQueryResponse) =>
error ? reject(makeWatchmanError(error)) : resolve(result),
),
);
if ('warning' in response) {
onStatus({
type: 'watchman_warning',
warning: response.warning,
command,
});
}
// $FlowFixMe[incompatible-return]
return response;
} finally {
// $FlowFixMe[incompatible-call] clearInterval / clearTimeout are interchangeable
clearInterval(intervalOrTimeoutId);
if (didLogWatchmanWaitMessage) {
onStatus({
type: 'watchman_slow_command_complete',
timeElapsed: performance.now() - startTime,
command,
});
}
}
};
async function getWatchmanRoots(
roots: $ReadOnlyArray<Path>,
): Promise<WatchmanRoots> {
perfLogger?.point('watchmanCrawl/getWatchmanRoots_start');
const watchmanRoots: WatchmanRoots = new Map();
await Promise.all(
roots.map(async (root, index) => {
perfLogger?.point(`watchmanCrawl/watchProject_${index}_start`);
const response = await cmd<WatchmanWatchResponse>(
'watch-project',
root,
);
perfLogger?.point(`watchmanCrawl/watchProject_${index}_end`);
const existing = watchmanRoots.get(response.watch);
// A root can only be filtered if it was never seen with a
// relative_path before.
const canBeFiltered = !existing || existing.directoryFilters.length > 0;
if (canBeFiltered) {
if (response.relative_path) {
watchmanRoots.set(response.watch, {
watcher: response.watcher,
directoryFilters: (existing?.directoryFilters || []).concat(
response.relative_path,
),
});
} else {
// Make the filter directories an empty array to signal that this
// root was already seen and needs to be watched for all files or
// directories.
watchmanRoots.set(response.watch, {
watcher: response.watcher,
directoryFilters: [],
});
}
}
}),
);
perfLogger?.point('watchmanCrawl/getWatchmanRoots_end');
return watchmanRoots;
}
async function queryWatchmanForDirs(rootProjectDirMappings: WatchmanRoots) {
perfLogger?.point('watchmanCrawl/queryWatchmanForDirs_start');
const results = new Map<string, WatchmanQueryResponse>();
let isFresh = false;
await Promise.all(
Array.from(rootProjectDirMappings).map(
async ([posixSeparatedRoot, {directoryFilters, watcher}], index) => {
// Jest is only going to store one type of clock; a string that
// represents a local clock. However, the Watchman crawler supports
// a second type of clock that can be written by automation outside of
// Jest, called an "scm query", which fetches changed files based on
// source control mergebases. The reason this is necessary is because
// local clocks are not portable across systems, but scm queries are.
// By using scm queries, we can create the haste map on a different
// system and import it, transforming the clock into a local clock.
const since = previousState.clocks.get(
normalizePathSeparatorsToPosix(
pathUtils.absoluteToNormal(
normalizePathSeparatorsToSystem(posixSeparatedRoot),
),
),
);
perfLogger?.annotate({
bool: {
[`watchmanCrawl/query_${index}_has_clock`]: since != null,
},
});
const {query, queryGenerator} = planQuery({
since,
extensions,
directoryFilters,
includeSha1: computeSha1,
includeSymlinks,
});
perfLogger?.annotate({
string: {
[`watchmanCrawl/query_${index}_watcher`]: watcher ?? 'unknown',
[`watchmanCrawl/query_${index}_generator`]: queryGenerator,
},
});
perfLogger?.point(`watchmanCrawl/query_${index}_start`);
const response = await cmd<WatchmanQueryResponse>(
'query',
posixSeparatedRoot,
query,
);
perfLogger?.point(`watchmanCrawl/query_${index}_end`);
// When a source-control query is used, we ignore the "is fresh"
// response from Watchman because it will be true despite the query
// being incremental.
const isSourceControlQuery =
typeof since !== 'string' && since?.scm?.['mergebase-with'] != null;
if (!isSourceControlQuery) {
isFresh = isFresh || response.is_fresh_instance;
}
results.set(posixSeparatedRoot, response);
},
),
);
perfLogger?.point('watchmanCrawl/queryWatchmanForDirs_end');
return {
isFresh,
results,
};
}
let removedFiles: Set<CanonicalPath> = new Set();
let changedFiles: FileData = new Map();
let results: Map<string, WatchmanQueryResponse>;
let isFresh = false;
let queryError: ?Error;
try {
const watchmanRoots = await getWatchmanRoots(roots);
const watchmanFileResults = await queryWatchmanForDirs(watchmanRoots);
results = watchmanFileResults.results;
isFresh = watchmanFileResults.isFresh;
} catch (e) {
queryError = e;
}
client.end();
if (results == null) {
if (clientError) {
perfLogger?.annotate({
string: {
'watchmanCrawl/client_error':
clientError.message ?? '[message missing]',
},
});
}
if (queryError) {
perfLogger?.annotate({
string: {
'watchmanCrawl/query_error':
queryError.message ?? '[message missing]',
},
});
}
perfLogger?.point('watchmanCrawl_end');
abortSignal?.throwIfAborted();
throw (
queryError ?? clientError ?? new Error('Watchman file results missing')
);
}
perfLogger?.point('watchmanCrawl/processResults_start');
const freshFileData: FileData = new Map();
for (const [watchRoot, response] of results) {
const fsRoot = normalizePathSeparatorsToSystem(watchRoot);
const relativeFsRoot = pathUtils.absoluteToNormal(fsRoot);
newClocks.set(
normalizePathSeparatorsToPosix(relativeFsRoot),
// Ensure we persist only the local clock.
typeof response.clock === 'string'
? response.clock
: response.clock.clock,
);
for (const fileData of response.files) {
const filePath =
fsRoot + path.sep + normalizePathSeparatorsToSystem(fileData.name);
const relativeFilePath = pathUtils.absoluteToNormal(filePath);
if (!fileData.exists) {
if (!isFresh) {
removedFiles.add(relativeFilePath);
}
// Whether watchman can return exists: false in a fresh instance
// response is unknown, but there's nothing we need to do in that case.
} else if (!ignore(filePath)) {
const {mtime_ms, size} = fileData;
invariant(
mtime_ms != null && size != null,
'missing file data in watchman response',
);
const mtime =
typeof mtime_ms === 'number' ? mtime_ms : mtime_ms.toNumber();
let sha1hex = fileData['content.sha1hex'];
if (typeof sha1hex !== 'string' || sha1hex.length !== 40) {
sha1hex = undefined;
}
let symlinkInfo: 0 | 1 | string = 0;
if (fileData.type === 'l') {
symlinkInfo = fileData['symlink_target'] ?? 1;
}
const nextData: FileMetaData = [
'',
mtime,
size,
0,
'',
sha1hex ?? null,
symlinkInfo,
];
// If watchman is fresh, the removed files map starts with all files
// and we remove them as we verify they still exist.
if (isFresh) {
freshFileData.set(relativeFilePath, nextData);
} else {
changedFiles.set(relativeFilePath, nextData);
}
}
}
}
if (isFresh) {
({changedFiles, removedFiles} =
previousState.fileSystem.getDifference(freshFileData));
}
perfLogger?.point('watchmanCrawl/processResults_end');
perfLogger?.point('watchmanCrawl_end');
abortSignal?.throwIfAborted();
return {
changedFiles,
removedFiles,
clocks: newClocks,
};
};

View File

@@ -0,0 +1,62 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.planQuery = planQuery;
function planQuery({
since,
directoryFilters,
extensions,
includeSha1,
includeSymlinks,
}) {
const fields = ["name", "exists", "mtime_ms", "size"];
if (includeSha1) {
fields.push("content.sha1hex");
}
if (includeSymlinks) {
fields.push("type");
}
const allOfTerms = includeSymlinks
? [
[
"anyof",
["allof", ["type", "f"], ["suffix", extensions]],
["type", "l"],
],
]
: [["type", "f"]];
const query = {
fields,
};
let queryGenerator;
if (since != null) {
query.since = since;
queryGenerator = "since";
if (directoryFilters.length > 0) {
allOfTerms.push([
"anyof",
...directoryFilters.map((dir) => ["dirname", dir]),
]);
}
} else if (directoryFilters.length > 0) {
query.glob = directoryFilters.map((directory) => `${directory}/**`);
query.glob_includedotfiles = true;
queryGenerator = "glob";
} else if (!includeSymlinks) {
query.suffix = extensions;
queryGenerator = "suffix";
} else {
queryGenerator = "all";
}
if (!includeSymlinks && queryGenerator !== "suffix") {
allOfTerms.push(["suffix", extensions]);
}
query.expression =
allOfTerms.length === 1 ? allOfTerms[0] : ["allof", ...allOfTerms];
return {
query,
queryGenerator,
};
}

View File

@@ -0,0 +1,136 @@
/**
* 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
* @flow strict
*/
import type {
WatchmanDirnameExpression,
WatchmanExpression,
WatchmanQuery,
WatchmanQuerySince,
} from 'fb-watchman';
export function planQuery({
since,
directoryFilters,
extensions,
includeSha1,
includeSymlinks,
}: $ReadOnly<{
since: ?WatchmanQuerySince,
directoryFilters: $ReadOnlyArray<string>,
extensions: $ReadOnlyArray<string>,
includeSha1: boolean,
includeSymlinks: boolean,
}>): {
query: WatchmanQuery,
queryGenerator: string,
} {
const fields = ['name', 'exists', 'mtime_ms', 'size'];
if (includeSha1) {
fields.push('content.sha1hex');
}
/**
* Note on symlink_target:
*
* Watchman supports requesting the symlink_target field, which is
* *potentially* more efficient if targets can be read from metadata without
* reading/materialising files. However, at the time of writing, Watchman has
* issues reporting symlink_target on some backends[1]. Additionally, though
* the Eden watcher is known to work, it reads links serially[2] on demand[3]
* - less efficiently than we can do ourselves.
* [1] https://github.com/facebook/watchman/issues/1084
* [2] https://github.com/facebook/watchman/blob/v2023.01.02.00/watchman/watcher/eden.cpp#L476-L485
* [3] https://github.com/facebook/watchman/blob/v2023.01.02.00/watchman/watcher/eden.cpp#L433-L434
*/
if (includeSymlinks) {
fields.push('type');
}
const allOfTerms: Array<WatchmanExpression> = includeSymlinks
? [
[
'anyof',
['allof', ['type', 'f'], ['suffix', extensions]],
['type', 'l'],
],
]
: [['type', 'f']];
const query: WatchmanQuery = {fields};
/**
* Watchman "query planner".
*
* Watchman file queries consist of 1 or more generators that feed
* files through the expression evaluator.
*
* Strategy:
* 1. Select the narrowest possible generator so that the expression
* evaluator has fewer candidates to process.
* 2. Evaluate expressions from narrowest to broadest.
* 3. Don't use an expression to recheck a condition that the
* generator already guarantees.
* 4. Compose expressions to avoid combinatorial explosions in the
* number of terms.
*
* The ordering of generators/filters, from narrow to broad, is:
* - since = O(changes)
* - glob / dirname = O(files in a subtree of the repo)
* - suffix = O(files in the repo)
*
* We assume that file extensions are ~uniformly distributed in the
* repo but Haste map projects are focused on a handful of
* directories. Therefore `glob` < `suffix`.
*/
let queryGenerator: ?string;
if (since != null) {
// Prefer the since generator whenever we have a clock
query.since = since;
queryGenerator = 'since';
// Filter on directories using an anyof expression
if (directoryFilters.length > 0) {
allOfTerms.push([
'anyof',
...directoryFilters.map(
dir => (['dirname', dir]: WatchmanDirnameExpression),
),
]);
}
} else if (directoryFilters.length > 0) {
// Use the `glob` generator and filter only by extension.
query.glob = directoryFilters.map(directory => `${directory}/**`);
query.glob_includedotfiles = true;
queryGenerator = 'glob';
} else if (!includeSymlinks) {
// Use the `suffix` generator with no path/extension filtering, as long
// as we don't need (suffixless) directory symlinks.
query.suffix = extensions;
queryGenerator = 'suffix';
} else {
// Fall back to `all` if we need symlinks and don't have a clock or
// directory filters.
queryGenerator = 'all';
}
// `includeSymlinks` implies we need (suffixless) directory links. In the
// case of the `suffix` generator, a suffix expression would be redundant.
if (!includeSymlinks && queryGenerator !== 'suffix') {
allOfTerms.push(['suffix', extensions]);
}
// If we only have one "all of" expression we can use it directly, otherwise
// wrap in ['allof', ...expressions]. By construction we should never have
// length 0.
query.expression =
allOfTerms.length === 1 ? allOfTerms[0] : ['allof', ...allOfTerms];
return {query, queryGenerator};
}

View File

@@ -0,0 +1,327 @@
/**
* 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 {PerfLogger, PerfLoggerFactory, RootPerfLogger} from 'metro-config';
import type {AbortSignal} from 'node-abort-controller';
export type {PerfLoggerFactory, PerfLogger};
/**
* These inputs affect the internal data collected for a given filesystem
* state, and changes may invalidate a cache.
*/
export type BuildParameters = Readonly<{
computeDependencies: boolean;
computeSha1: boolean;
enableHastePackages: boolean;
enableSymlinks: boolean;
extensions: ReadonlyArray<string>;
forceNodeFilesystemAPI: boolean;
ignorePattern: RegExp;
mocksPattern: RegExp | null;
platforms: ReadonlyArray<string>;
retainAllFiles: boolean;
rootDir: string;
roots: ReadonlyArray<string>;
skipPackageJson: boolean;
dependencyExtractor: string | null;
hasteImplModulePath: string | null;
cacheBreaker: string;
}>;
export interface BuildResult {
fileSystem: FileSystem;
hasteMap: HasteMap;
}
export interface CacheData {
readonly clocks: WatchmanClocks;
readonly mocks: MockData;
readonly files: FileData;
}
export interface CacheManager {
read(): Promise<CacheData | null>;
write(
dataSnapshot: CacheData,
delta: Readonly<{changed: FileData; removed: FileData}>,
): Promise<void>;
}
export type CacheManagerFactory = (
buildParameters: BuildParameters,
) => CacheManager;
export interface ChangeEvent {
logger: RootPerfLogger | null;
eventsQueue: EventsQueue;
}
export interface ChangeEventMetadata {
/** Epoch ms */
modifiedTime: number | null;
/** Bytes */
size: number | null;
/** Regular file / Directory / Symlink */
type: 'f' | 'd' | 'l';
}
export type Console = typeof global.console;
export interface CrawlerOptions {
abortSignal: AbortSignal | null;
computeSha1: boolean;
extensions: ReadonlyArray<string>;
forceNodeFilesystemAPI: boolean;
ignore: IgnoreMatcher;
includeSymlinks: boolean;
perfLogger?: PerfLogger | null;
previousState: Readonly<{
clocks: ReadonlyMap<Path, WatchmanClockSpec>;
files: ReadonlyMap<Path, FileMetaData>;
}>;
rootDir: string;
roots: ReadonlyArray<string>;
onStatus: (status: WatcherStatus) => void;
}
export type WatcherStatus =
| {
type: 'watchman_slow_command';
timeElapsed: number;
command: 'watch-project' | 'query';
}
| {
type: 'watchman_slow_command_complete';
timeElapsed: number;
command: 'watch-project' | 'query';
}
| {
type: 'watchman_warning';
warning: unknown;
command: 'watch-project' | 'query';
};
export type DuplicatesSet = Map<string, /* type */ number>;
export type DuplicatesIndex = Map<string, Map<string, DuplicatesSet>>;
export type EventsQueue = Array<{
filePath: Path;
metadata?: ChangeEventMetadata | null;
type: string;
}>;
export interface HType {
ID: 0;
MTIME: 1;
SIZE: 2;
VISITED: 3;
DEPENDENCIES: 4;
SHA1: 5;
SYMLINK: 6;
PATH: 0;
TYPE: 1;
MODULE: 0;
PACKAGE: 1;
GENERIC_PLATFORM: 'g';
NATIVE_PLATFORM: 'native';
DEPENDENCY_DELIM: '\0';
}
type Values<T> = T[keyof T];
export type HTypeValue = Values<HType>;
export type IgnoreMatcher = (item: string) => boolean;
export type FileData = Map<Path, FileMetaData>;
export type FileMetaData = [
/* id */ string,
/* mtime */ number,
/* size */ number,
/* visited */ 0 | 1,
/* dependencies */ string,
/* sha1 */ string | null,
/* symlink */ 0 | 1 | string, // string specifies target, if known
];
export type FileStats = Readonly<{
fileType: 'f' | 'l';
modifiedTime: number;
}>;
export interface FileSystem {
exists(file: Path): boolean;
getAllFiles(): Path[];
getDependencies(file: Path): string[] | null;
getModuleName(file: Path): string | null;
getSerializableSnapshot(): FileData;
getSha1(file: Path): string | null;
/**
* Given a start path (which need not exist), a subpath and type, and
* optionally a 'breakOnSegment', performs the following:
*
* X = mixedStartPath
* do
* if basename(X) === opts.breakOnSegment
* return null
* if X + subpath exists and has type opts.subpathType
* return {
* absolutePath: realpath(X + subpath)
* containerRelativePath: relative(mixedStartPath, X)
* }
* X = dirname(X)
* while X !== dirname(X)
*
* If opts.invalidatedBy is given, collects all absolute, real paths that if
* added or removed may invalidate this result.
*
* Useful for finding the closest package scope (subpath: package.json,
* type f, breakOnSegment: node_modules) or closest potential package root
* (subpath: node_modules/pkg, type: d) in Node.js resolution.
*/
hierarchicalLookup(
mixedStartPath: string,
subpath: string,
opts: {
breakOnSegment: string | null | undefined;
invalidatedBy: Set<string> | null | undefined;
subpathType: 'f' | 'd';
},
): {
absolutePath: string;
containerRelativePath: string;
} | null;
/**
* Analogous to posix lstat. If the file at `file` is a symlink, return
* information about the symlink without following it.
*/
linkStats(file: Path): FileStats | null;
/**
* Return information about the given path, whether a directory or file.
* Always follow symlinks, and return a real path if it exists.
*/
lookup(mixedPath: Path): LookupResult;
matchFiles(opts: {
/* Filter relative paths against a pattern. */
filter?: RegExp | null;
/* `filter` is applied against absolute paths, vs rootDir-relative. (default: false) */
filterCompareAbsolute?: boolean;
/* `filter` is applied against posix-delimited paths, even on Windows. (default: false) */
filterComparePosix?: boolean;
/* Follow symlinks when enumerating paths. (default: false) */
follow?: boolean;
/* Should search for files recursively. (default: true) */
recursive?: boolean;
/* Match files under a given root, or null for all files */
rootDir?: Path | null;
}): Iterable<Path>;
}
export type Glob = string;
export type LookupResult =
| {
// The node is missing from the FileSystem implementation (note this
// could indicate an unwatched path, or a directory containing no watched
// files).
exists: false;
// The real, normal, absolute paths of any symlinks traversed.
links: ReadonlySet<string>;
// The real, normal, absolute path of the first path segment
// encountered that does not exist, or cannot be navigated through.
missing: string;
}
| {
exists: true;
// The real, normal, absolute paths of any symlinks traversed.
links: ReadonlySet<string>;
// The real, normal, absolute path of the file or directory.
realPath: string;
// Currently lookup always follows symlinks, so can only return
// directories or regular files, but this may be extended.
type: 'd' | 'f';
};
export interface HasteMap {
getModule(
name: string,
platform?: string | null,
supportsNativePlatform?: boolean | null,
type?: HTypeValue | null,
): Path | null;
getPackage(
name: string,
platform: string | null,
_supportsNativePlatform: boolean | null,
): Path | null;
getRawHasteMap(): ReadOnlyRawHasteMap;
}
export type MockData = Map<string, Path>;
export type HasteMapData = Map<string, HasteMapItem>;
export interface HasteMapItem {
[platform: string]: HasteMapItemMetaData;
}
export type HasteMapItemMetaData = [/* path */ string, /* type */ number];
export interface MutableFileSystem extends FileSystem {
remove(filePath: Path): void;
addOrModify(filePath: Path, fileMetadata: FileMetaData): void;
bulkAddOrModify(addedOrModifiedFiles: FileData): void;
}
export type Path = string;
export interface RawHasteMap {
rootDir: Path;
duplicates: DuplicatesIndex;
map: HasteMapData;
}
export type ReadOnlyRawHasteMap = Readonly<{
rootDir: Path;
duplicates: ReadonlyMap<
string,
ReadonlyMap<string, ReadonlyMap<string, number>>
>;
map: ReadonlyMap<string, HasteMapItem>;
}>;
export type WatchmanClockSpec =
| string
| Readonly<{scm: Readonly<{'mergebase-with': string}>}>;
export type WatchmanClocks = Map<Path, WatchmanClockSpec>;
export type WorkerMessage = Readonly<{
computeDependencies: boolean;
computeSha1: boolean;
dependencyExtractor?: string | null;
enableHastePackages: boolean;
readLink: boolean;
rootDir: string;
filePath: string;
hasteImplModulePath?: string | null;
}>;
export type WorkerMetadata = Readonly<{
dependencies?: ReadonlyArray<string>;
id?: string | null;
module?: HasteMapItemMetaData | null;
sha1?: string | null;
symlinkTarget?: string | null;
}>;

View File

@@ -0,0 +1 @@
"use strict";

View File

@@ -0,0 +1,347 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
'use strict';
import type {PerfLogger, PerfLoggerFactory, RootPerfLogger} from 'metro-config';
import type {AbortSignal} from 'node-abort-controller';
export type {PerfLoggerFactory, PerfLogger};
// These inputs affect the internal data collected for a given filesystem
// state, and changes may invalidate a cache.
export type BuildParameters = $ReadOnly<{
computeDependencies: boolean,
computeSha1: boolean,
enableHastePackages: boolean,
enableSymlinks: boolean,
extensions: $ReadOnlyArray<string>,
forceNodeFilesystemAPI: boolean,
ignorePattern: RegExp,
mocksPattern: ?RegExp,
platforms: $ReadOnlyArray<string>,
retainAllFiles: boolean,
rootDir: string,
roots: $ReadOnlyArray<string>,
skipPackageJson: boolean,
// Module paths that should export a 'getCacheKey' method
dependencyExtractor: ?string,
hasteImplModulePath: ?string,
cacheBreaker: string,
}>;
export type BuildResult = {
fileSystem: FileSystem,
hasteMap: HasteMap,
mockMap: MockMap,
};
export type CacheData = $ReadOnly<{
clocks: WatchmanClocks,
mocks: RawMockMap,
fileSystemData: mixed,
}>;
export type CacheDelta = $ReadOnly<{
changed: $ReadOnlyMap<CanonicalPath, FileMetaData>,
removed: $ReadOnlySet<CanonicalPath>,
}>;
export interface CacheManager {
read(): Promise<?CacheData>;
write(dataSnapshot: CacheData, delta: CacheDelta): Promise<void>;
}
export type CacheManagerFactory = (
buildParameters: BuildParameters,
) => CacheManager;
// A path that is
// - Relative to the contextual `rootDir`
// - Normalised (no extraneous '.' or '..')
// - Real (no symlinks in path, though the path itself may be a symlink)
export type CanonicalPath = string;
export type ChangeEvent = {
logger: ?RootPerfLogger,
eventsQueue: EventsQueue,
};
export type ChangeEventMetadata = {
modifiedTime: ?number, // Epoch ms
size: ?number, // Bytes
type: 'f' | 'd' | 'l', // Regular file / Directory / Symlink
};
export type Console = typeof global.console;
export type CrawlerOptions = {
abortSignal: ?AbortSignal,
computeSha1: boolean,
console: Console,
extensions: $ReadOnlyArray<string>,
forceNodeFilesystemAPI: boolean,
ignore: IgnoreMatcher,
includeSymlinks: boolean,
perfLogger?: ?PerfLogger,
previousState: $ReadOnly<{
clocks: $ReadOnlyMap<CanonicalPath, WatchmanClockSpec>,
fileSystem: FileSystem,
}>,
rootDir: string,
roots: $ReadOnlyArray<string>,
onStatus: (status: WatcherStatus) => void,
};
export type WatcherStatus =
| {
type: 'watchman_slow_command',
timeElapsed: number,
command: 'watch-project' | 'query',
}
| {
type: 'watchman_slow_command_complete',
timeElapsed: number,
command: 'watch-project' | 'query',
}
| {
type: 'watchman_warning',
warning: mixed,
command: 'watch-project' | 'query',
};
export type DuplicatesSet = Map<string, /* type */ number>;
export type DuplicatesIndex = Map<string, Map<string, DuplicatesSet>>;
export type EventsQueue = Array<{
filePath: Path,
metadata?: ?ChangeEventMetadata,
type: string,
}>;
export type HType = {
ID: 0,
MTIME: 1,
SIZE: 2,
VISITED: 3,
DEPENDENCIES: 4,
SHA1: 5,
SYMLINK: 6,
PATH: 0,
TYPE: 1,
MODULE: 0,
PACKAGE: 1,
GENERIC_PLATFORM: 'g',
NATIVE_PLATFORM: 'native',
DEPENDENCY_DELIM: '\0',
};
export type HTypeValue = $Values<HType>;
export type IgnoreMatcher = (item: string) => boolean;
export type FileData = Map<CanonicalPath, FileMetaData>;
export type FileMetaData = [
/* id */ string,
/* mtime */ ?number,
/* size */ number,
/* visited */ 0 | 1,
/* dependencies */ string,
/* sha1 */ ?string,
/* symlink */ 0 | 1 | string, // string specifies target, if known
];
export type FileStats = $ReadOnly<{
fileType: 'f' | 'l',
modifiedTime: ?number,
}>;
export interface FileSystem {
exists(file: Path): boolean;
getAllFiles(): Array<Path>;
getDependencies(file: Path): ?Array<string>;
getDifference(files: FileData): {
changedFiles: FileData,
removedFiles: Set<string>,
};
getModuleName(file: Path): ?string;
getSerializableSnapshot(): CacheData['fileSystemData'];
getSha1(file: Path): ?string;
/**
* Given a start path (which need not exist), a subpath and type, and
* optionally a 'breakOnSegment', performs the following:
*
* X = mixedStartPath
* do
* if basename(X) === opts.breakOnSegment
* return null
* if X + subpath exists and has type opts.subpathType
* return {
* absolutePath: realpath(X + subpath)
* containerRelativePath: relative(mixedStartPath, X)
* }
* X = dirname(X)
* while X !== dirname(X)
*
* If opts.invalidatedBy is given, collects all absolute, real paths that if
* added or removed may invalidate this result.
*
* Useful for finding the closest package scope (subpath: package.json,
* type f, breakOnSegment: node_modules) or closest potential package root
* (subpath: node_modules/pkg, type: d) in Node.js resolution.
*/
hierarchicalLookup(
mixedStartPath: string,
subpath: string,
opts: {
breakOnSegment: ?string,
invalidatedBy: ?Set<string>,
subpathType: 'f' | 'd',
},
): ?{
absolutePath: string,
containerRelativePath: string,
};
/**
* Analogous to posix lstat. If the file at `file` is a symlink, return
* information about the symlink without following it.
*/
linkStats(file: Path): ?FileStats;
/**
* Return information about the given path, whether a directory or file.
* Always follow symlinks, and return a real path if it exists.
*/
lookup(mixedPath: Path): LookupResult;
matchFiles(opts: {
/* Filter relative paths against a pattern. */
filter?: RegExp | null,
/* `filter` is applied against absolute paths, vs rootDir-relative. (default: false) */
filterCompareAbsolute?: boolean,
/* `filter` is applied against posix-delimited paths, even on Windows. (default: false) */
filterComparePosix?: boolean,
/* Follow symlinks when enumerating paths. (default: false) */
follow?: boolean,
/* Should search for files recursively. (default: true) */
recursive?: boolean,
/* Match files under a given root, or null for all files */
rootDir?: Path | null,
}): Iterable<Path>;
}
export type Glob = string;
export type LookupResult =
| {
// The node is missing from the FileSystem implementation (note this
// could indicate an unwatched path, or a directory containing no watched
// files).
exists: false,
// The real, normal, absolute paths of any symlinks traversed.
links: $ReadOnlySet<string>,
// The real, normal, absolute path of the first path segment
// encountered that does not exist, or cannot be navigated through.
missing: string,
}
| {
exists: true,
// The real, normal, absolute paths of any symlinks traversed.
links: $ReadOnlySet<string>,
// The real, normal, absolute path of the file or directory.
realPath: string,
// Currently lookup always follows symlinks, so can only return
// directories or regular files, but this may be extended.
type: 'd' | 'f',
};
export interface MockMap {
getMockModule(name: string): ?Path;
}
export interface HasteMap {
getModule(
name: string,
platform?: ?string,
supportsNativePlatform?: ?boolean,
type?: ?HTypeValue,
): ?Path;
getPackage(
name: string,
platform: ?string,
_supportsNativePlatform: ?boolean,
): ?Path;
getRawHasteMap(): ReadOnlyRawHasteMap;
}
export type HasteMapData = Map<string, HasteMapItem>;
export type HasteMapItem = {
[platform: string]: HasteMapItemMetaData,
__proto__: null,
};
export type HasteMapItemMetaData = [/* path */ string, /* type */ number];
export interface MutableFileSystem extends FileSystem {
remove(filePath: Path): ?FileMetaData;
addOrModify(filePath: Path, fileMetadata: FileMetaData): void;
bulkAddOrModify(addedOrModifiedFiles: FileData): void;
}
export type Path = string;
export type RawMockMap = Map<string, Path>;
export type RawHasteMap = {
duplicates: DuplicatesIndex,
map: HasteMapData,
};
export type ReadOnlyRawHasteMap = $ReadOnly<{
duplicates: $ReadOnlyMap<
string,
$ReadOnlyMap<string, $ReadOnlyMap<string, number>>,
>,
map: $ReadOnlyMap<string, HasteMapItem>,
}>;
export type ReadOnlyRawMockMap = $ReadOnlyMap<string, Path>;
export type WatchmanClockSpec =
| string
| $ReadOnly<{scm: $ReadOnly<{'mergebase-with': string}>}>;
export type WatchmanClocks = Map<Path, WatchmanClockSpec>;
export type WorkerMessage = $ReadOnly<{
computeDependencies: boolean,
computeSha1: boolean,
dependencyExtractor?: ?string,
enableHastePackages: boolean,
readLink: boolean,
rootDir: string,
filePath: string,
hasteImplModulePath?: ?string,
}>;
export type WorkerMetadata = $ReadOnly<{
dependencies?: ?$ReadOnlyArray<string>,
id?: ?string,
module?: ?HasteMapItemMetaData,
sha1?: ?string,
symlinkTarget?: ?string,
}>;

View File

@@ -0,0 +1,56 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
var path = _interopRequireWildcard(require("path"));
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function (nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interopRequireWildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) {
return { default: obj };
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {};
var hasPropertyDescriptor =
Object.defineProperty && Object.getOwnPropertyDescriptor;
for (var key in obj) {
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor
? Object.getOwnPropertyDescriptor(obj, key)
: null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
const MOCKS_PATTERN = path.sep + "__mocks__" + path.sep;
const getMockName = (filePath) => {
const mockPath = filePath.split(MOCKS_PATTERN)[1];
return mockPath
.substring(0, mockPath.lastIndexOf(path.extname(mockPath)))
.replaceAll("\\", "/");
};
var _default = getMockName;
exports.default = _default;

View File

@@ -0,0 +1,22 @@
/**
* 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
* @flow strict
*/
import * as path from 'path';
const MOCKS_PATTERN = path.sep + '__mocks__' + path.sep;
const getMockName = (filePath: string): string => {
const mockPath = filePath.split(MOCKS_PATTERN)[1];
return mockPath
.substring(0, mockPath.lastIndexOf(path.extname(mockPath)))
.replaceAll('\\', '/');
};
export default getMockName;

View File

@@ -0,0 +1,93 @@
/**
* 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 {
BuildParameters,
BuildResult,
CacheData,
CacheManagerFactory,
ChangeEventMetadata,
Console,
FileData,
FileSystem,
HasteMapData,
HasteMapItem,
PerfLoggerFactory,
} from './flow-types';
import type {EventEmitter} from 'events';
export type {
BuildParameters,
CacheData,
ChangeEventMetadata,
FileData,
FileMap,
FileSystem,
HasteMapData,
HasteMapItem,
};
export type InputOptions = Readonly<{
computeDependencies?: boolean | null;
computeSha1?: boolean | null;
enableSymlinks?: boolean | null;
extensions: ReadonlyArray<string>;
forceNodeFilesystemAPI?: boolean | null;
ignorePattern?: RegExp | null;
mocksPattern?: string | null;
platforms: ReadonlyArray<string>;
retainAllFiles: boolean;
rootDir: string;
roots: ReadonlyArray<string>;
skipPackageJson?: boolean | null;
/** Module paths that should export a 'getCacheKey' method */
dependencyExtractor?: string | null;
hasteImplModulePath?: string | null;
perfLoggerFactory?: PerfLoggerFactory | null;
resetCache?: boolean | null;
maxWorkers: number;
throwOnModuleCollision?: boolean | null;
useWatchman?: boolean | null;
watchmanDeferStates?: ReadonlyArray<string>;
watch?: boolean | null;
console?: Console;
cacheManagerFactory?: CacheManagerFactory | null;
healthCheck: HealthCheckOptions;
}>;
type HealthCheckOptions = Readonly<{
enabled: boolean;
interval: number;
timeout: number;
filePrefix: string;
}>;
export {DiskCacheManager} from './cache/DiskCacheManager';
export {DuplicateHasteCandidatesError} from './lib/DuplicateHasteCandidatesError';
export type {HasteMap} from './flow-types';
export type {HealthCheckResult} from './Watcher';
export type {
CacheManager,
CacheManagerFactory,
ChangeEvent,
WatcherStatus,
} from './flow-types';
export default class FileMap extends EventEmitter {
static create(options: InputOptions): FileMap;
constructor(options: InputOptions);
build(): Promise<BuildResult>;
read(): Promise<CacheData | null>;
}
export class DuplicateError extends Error {}

View File

@@ -0,0 +1,850 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
Object.defineProperty(exports, "DiskCacheManager", {
enumerable: true,
get: function () {
return _DiskCacheManager.DiskCacheManager;
},
});
Object.defineProperty(exports, "DuplicateHasteCandidatesError", {
enumerable: true,
get: function () {
return _DuplicateHasteCandidatesError.DuplicateHasteCandidatesError;
},
});
Object.defineProperty(exports, "MutableHasteMap", {
enumerable: true,
get: function () {
return _MutableHasteMap.default;
},
});
exports.default = void 0;
var _DiskCacheManager = require("./cache/DiskCacheManager");
var _constants = _interopRequireDefault(require("./constants"));
var _getMockName = _interopRequireDefault(require("./getMockName"));
var _checkWatchmanCapabilities = _interopRequireDefault(
require("./lib/checkWatchmanCapabilities")
);
var _DuplicateError = require("./lib/DuplicateError");
var _MockMap = _interopRequireDefault(require("./lib/MockMap"));
var _MutableHasteMap = _interopRequireDefault(require("./lib/MutableHasteMap"));
var _normalizePathSeparatorsToSystem = _interopRequireDefault(
require("./lib/normalizePathSeparatorsToSystem")
);
var _RootPathUtils = require("./lib/RootPathUtils");
var _TreeFS = _interopRequireDefault(require("./lib/TreeFS"));
var _Watcher = require("./Watcher");
var _worker = require("./worker");
var _events = _interopRequireDefault(require("events"));
var _invariant = _interopRequireDefault(require("invariant"));
var _jestWorker = require("jest-worker");
var _nodeAbortController = require("node-abort-controller");
var _nullthrows = _interopRequireDefault(require("nullthrows"));
var path = _interopRequireWildcard(require("path"));
var _perf_hooks = require("perf_hooks");
var _DuplicateHasteCandidatesError = require("./lib/DuplicateHasteCandidatesError");
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function (nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interopRequireWildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) {
return { default: obj };
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {};
var hasPropertyDescriptor =
Object.defineProperty && Object.getOwnPropertyDescriptor;
for (var key in obj) {
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor
? Object.getOwnPropertyDescriptor(obj, key)
: null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const debug = require("debug")("Metro:FileMap");
const CACHE_BREAKER = "7";
const CHANGE_INTERVAL = 30;
const YIELD_EVERY_NUM_HASTE_FILES = 10000;
const NODE_MODULES = path.sep + "node_modules" + path.sep;
const PACKAGE_JSON = path.sep + "package.json";
const VCS_DIRECTORIES = /[/\\]\.(git|hg)[/\\]/.source;
const WATCHMAN_REQUIRED_CAPABILITIES = [
"field-content.sha1hex",
"relative_root",
"suffix-set",
"wildmatch",
];
class FileMap extends _events.default {
static create(options) {
return new FileMap(options);
}
constructor(options) {
super();
if (options.perfLoggerFactory) {
this._startupPerfLogger =
options.perfLoggerFactory?.("START_UP").subSpan("fileMap") ?? null;
this._startupPerfLogger?.point("constructor_start");
}
let ignorePattern;
if (options.ignorePattern) {
const inputIgnorePattern = options.ignorePattern;
if (inputIgnorePattern instanceof RegExp) {
ignorePattern = new RegExp(
inputIgnorePattern.source.concat("|" + VCS_DIRECTORIES),
inputIgnorePattern.flags
);
} else {
throw new Error(
"metro-file-map: the `ignorePattern` option must be a RegExp"
);
}
} else {
ignorePattern = new RegExp(VCS_DIRECTORIES);
}
const buildParameters = {
computeDependencies:
options.computeDependencies == null
? true
: options.computeDependencies,
computeSha1: options.computeSha1 || false,
dependencyExtractor: options.dependencyExtractor ?? null,
enableHastePackages: options.enableHastePackages ?? true,
enableSymlinks: options.enableSymlinks || false,
extensions: options.extensions,
forceNodeFilesystemAPI: !!options.forceNodeFilesystemAPI,
hasteImplModulePath: options.hasteImplModulePath,
ignorePattern,
mocksPattern:
options.mocksPattern != null && options.mocksPattern !== ""
? new RegExp(options.mocksPattern)
: null,
platforms: options.platforms,
retainAllFiles: options.retainAllFiles,
rootDir: options.rootDir,
roots: Array.from(new Set(options.roots)),
skipPackageJson: !!options.skipPackageJson,
cacheBreaker: CACHE_BREAKER,
};
this._options = {
...buildParameters,
enableWorkerThreads: options.enableWorkerThreads ?? false,
healthCheck: options.healthCheck,
maxWorkers: options.maxWorkers,
perfLoggerFactory: options.perfLoggerFactory,
resetCache: options.resetCache,
throwOnModuleCollision: !!options.throwOnModuleCollision,
useWatchman: options.useWatchman == null ? true : options.useWatchman,
watch: !!options.watch,
watchmanDeferStates: options.watchmanDeferStates ?? [],
};
this._console = options.console || global.console;
this._cacheManager = options.cacheManagerFactory
? options.cacheManagerFactory.call(null, buildParameters)
: new _DiskCacheManager.DiskCacheManager({
buildParameters,
});
this._buildPromise = null;
this._pathUtils = new _RootPathUtils.RootPathUtils(options.rootDir);
this._worker = null;
this._startupPerfLogger?.point("constructor_end");
this._crawlerAbortController = new _nodeAbortController.AbortController();
this._changeID = 0;
}
build() {
this._startupPerfLogger?.point("build_start");
if (!this._buildPromise) {
this._buildPromise = (async () => {
let initialData;
if (this._options.resetCache !== true) {
initialData = await this.read();
}
if (!initialData) {
debug("Not using a cache");
} else {
debug("Cache loaded (%d clock(s))", initialData.clocks.size);
}
const rootDir = this._options.rootDir;
this._startupPerfLogger?.point("constructFileSystem_start");
const fileSystem =
initialData != null
? _TreeFS.default.fromDeserializedSnapshot({
rootDir,
fileSystemData: initialData.fileSystemData,
})
: new _TreeFS.default({
rootDir,
});
this._startupPerfLogger?.point("constructFileSystem_end");
const mocks = initialData?.mocks ?? new Map();
const [fileDelta, hasteMap] = await Promise.all([
this._buildFileDelta({
fileSystem,
clocks: initialData?.clocks ?? new Map(),
}),
this._constructHasteMap(fileSystem),
]);
await this._applyFileDelta(fileSystem, hasteMap, mocks, fileDelta);
await this._takeSnapshotAndPersist(
fileSystem,
fileDelta.clocks ?? new Map(),
hasteMap,
mocks,
fileDelta.changedFiles,
fileDelta.removedFiles
);
debug(
"Finished mapping files (%d changes, %d removed).",
fileDelta.changedFiles.size,
fileDelta.removedFiles.size
);
await this._watch(fileSystem, hasteMap, mocks);
return {
fileSystem,
hasteMap,
mockMap: new _MockMap.default({
rootDir,
rawMockMap: mocks,
}),
};
})();
}
return this._buildPromise.then((result) => {
this._startupPerfLogger?.point("build_end");
return result;
});
}
async _constructHasteMap(fileSystem) {
this._startupPerfLogger?.point("constructHasteMap_start");
const hasteMap = new _MutableHasteMap.default({
console: this._console,
platforms: new Set(this._options.platforms),
rootDir: this._options.rootDir,
throwOnModuleCollision: this._options.throwOnModuleCollision,
});
let hasteFiles = 0;
for (const {
baseName,
canonicalPath,
metadata,
} of fileSystem.metadataIterator({
includeNodeModules: false,
includeSymlinks: false,
})) {
if (metadata[_constants.default.ID]) {
hasteMap.setModule(metadata[_constants.default.ID], [
canonicalPath,
baseName === "package.json"
? _constants.default.PACKAGE
: _constants.default.MODULE,
]);
if (++hasteFiles % YIELD_EVERY_NUM_HASTE_FILES === 0) {
await new Promise(setImmediate);
}
}
}
this._startupPerfLogger?.annotate({
int: {
hasteFiles,
},
});
this._startupPerfLogger?.point("constructHasteMap_end");
return hasteMap;
}
async read() {
let data;
this._startupPerfLogger?.point("read_start");
try {
data = await this._cacheManager.read();
} catch (e) {
this._console.warn(
"Error while reading cache, falling back to a full crawl:\n",
e
);
this._startupPerfLogger?.annotate({
string: {
cacheReadError: e.toString(),
},
});
}
this._startupPerfLogger?.point("read_end");
return data;
}
async _buildFileDelta(previousState) {
this._startupPerfLogger?.point("buildFileDelta_start");
const {
computeSha1,
enableSymlinks,
extensions,
forceNodeFilesystemAPI,
ignorePattern,
roots,
rootDir,
watch,
watchmanDeferStates,
} = this._options;
this._watcher = new _Watcher.Watcher({
abortSignal: this._crawlerAbortController.signal,
computeSha1,
console: this._console,
enableSymlinks,
extensions,
forceNodeFilesystemAPI,
healthCheckFilePrefix: this._options.healthCheck.filePrefix,
ignore: (path) => this._ignore(path),
ignorePattern,
perfLogger: this._startupPerfLogger,
previousState,
roots,
rootDir,
useWatchman: await this._shouldUseWatchman(),
watch,
watchmanDeferStates,
});
const watcher = this._watcher;
watcher.on("status", (status) => this.emit("status", status));
return watcher.crawl().then((result) => {
this._startupPerfLogger?.point("buildFileDelta_end");
return result;
});
}
_processFile(hasteMap, mockMap, filePath, fileMetadata, workerOptions) {
const rootDir = this._options.rootDir;
const relativeFilePath = this._pathUtils.absoluteToNormal(filePath);
const isSymlink = fileMetadata[_constants.default.SYMLINK] !== 0;
const computeSha1 =
this._options.computeSha1 &&
!isSymlink &&
fileMetadata[_constants.default.SHA1] == null;
const readLink =
this._options.enableSymlinks &&
isSymlink &&
typeof fileMetadata[_constants.default.SYMLINK] !== "string";
const workerReply = (metadata) => {
fileMetadata[_constants.default.VISITED] = 1;
const metadataId = metadata.id;
const metadataModule = metadata.module;
if (metadataId != null && metadataModule) {
fileMetadata[_constants.default.ID] = metadataId;
hasteMap.setModule(metadataId, metadataModule);
}
fileMetadata[_constants.default.DEPENDENCIES] = metadata.dependencies
? metadata.dependencies.join(_constants.default.DEPENDENCY_DELIM)
: "";
if (computeSha1) {
fileMetadata[_constants.default.SHA1] = metadata.sha1;
}
if (metadata.symlinkTarget != null) {
fileMetadata[_constants.default.SYMLINK] = metadata.symlinkTarget;
}
};
const workerError = (error) => {
if (
error == null ||
typeof error !== "object" ||
error.message == null ||
error.stack == null
) {
error = new Error(error);
error.stack = "";
}
throw error;
};
if (this._options.retainAllFiles && filePath.includes(NODE_MODULES)) {
if (computeSha1 || readLink) {
return this._getWorker(workerOptions)
.worker({
computeDependencies: false,
computeSha1,
dependencyExtractor: null,
enableHastePackages: false,
filePath,
hasteImplModulePath: null,
readLink,
rootDir,
})
.then(workerReply, workerError);
}
return null;
}
if (isSymlink) {
if (readLink) {
return this._getWorker({
forceInBand: true,
})
.worker({
computeDependencies: false,
computeSha1: false,
dependencyExtractor: null,
enableHastePackages: false,
filePath,
hasteImplModulePath: null,
readLink,
rootDir,
})
.then(workerReply, workerError);
}
return null;
}
if (
this._options.mocksPattern &&
this._options.mocksPattern.test(filePath)
) {
const mockPath = (0, _getMockName.default)(filePath);
const existingMockPath = mockMap.get(mockPath);
if (existingMockPath != null) {
const secondMockPath = this._pathUtils.absoluteToNormal(filePath);
if (existingMockPath !== secondMockPath) {
const method = this._options.throwOnModuleCollision
? "error"
: "warn";
this._console[method](
[
"metro-file-map: duplicate manual mock found: " + mockPath,
" The following files share their name; please delete one of them:",
" * <rootDir>" + path.sep + existingMockPath,
" * <rootDir>" + path.sep + secondMockPath,
"",
].join("\n")
);
if (this._options.throwOnModuleCollision) {
throw new _DuplicateError.DuplicateError(
existingMockPath,
secondMockPath
);
}
}
}
mockMap.set(mockPath, relativeFilePath);
}
return this._getWorker(workerOptions)
.worker({
computeDependencies: this._options.computeDependencies,
computeSha1,
dependencyExtractor: this._options.dependencyExtractor,
enableHastePackages: this._options.enableHastePackages,
filePath,
hasteImplModulePath: this._options.hasteImplModulePath,
readLink: false,
rootDir,
})
.then(workerReply, workerError);
}
async _applyFileDelta(fileSystem, hasteMap, mockMap, delta) {
this._startupPerfLogger?.point("applyFileDelta_start");
const { changedFiles, removedFiles } = delta;
this._startupPerfLogger?.point("applyFileDelta_preprocess_start");
const promises = [];
const missingFiles = new Set();
this._startupPerfLogger?.point("applyFileDelta_remove_start");
for (const relativeFilePath of removedFiles) {
this._removeIfExists(fileSystem, hasteMap, mockMap, relativeFilePath);
}
this._startupPerfLogger?.point("applyFileDelta_remove_end");
for (const [relativeFilePath, fileData] of changedFiles) {
if (fileData[_constants.default.VISITED] === 1) {
continue;
}
if (
this._options.skipPackageJson &&
relativeFilePath.endsWith(PACKAGE_JSON)
) {
continue;
}
const filePath = this._pathUtils.normalToAbsolute(relativeFilePath);
const maybePromise = this._processFile(
hasteMap,
mockMap,
filePath,
fileData,
{
perfLogger: this._startupPerfLogger,
}
);
if (maybePromise) {
promises.push(
maybePromise.catch((e) => {
if (["ENOENT", "EACCESS"].includes(e.code)) {
missingFiles.add(relativeFilePath);
} else {
throw e;
}
})
);
}
}
this._startupPerfLogger?.point("applyFileDelta_preprocess_end");
debug("Visiting %d added/modified files.", promises.length);
this._startupPerfLogger?.point("applyFileDelta_process_start");
try {
await Promise.all(promises);
} finally {
this._cleanup();
}
this._startupPerfLogger?.point("applyFileDelta_process_end");
this._startupPerfLogger?.point("applyFileDelta_add_start");
for (const relativeFilePath of missingFiles) {
changedFiles.delete(relativeFilePath);
this._removeIfExists(fileSystem, hasteMap, mockMap, relativeFilePath);
}
fileSystem.bulkAddOrModify(changedFiles);
this._startupPerfLogger?.point("applyFileDelta_add_end");
this._startupPerfLogger?.point("applyFileDelta_end");
}
_cleanup() {
const worker = this._worker;
if (worker && typeof worker.end === "function") {
worker.end();
}
this._worker = null;
}
async _takeSnapshotAndPersist(
fileSystem,
clocks,
hasteMap,
mockMap,
changed,
removed
) {
this._startupPerfLogger?.point("persist_start");
await this._cacheManager.write(
{
fileSystemData: fileSystem.getSerializableSnapshot(),
clocks: new Map(clocks),
mocks: new Map(mockMap),
},
{
changed,
removed,
}
);
this._startupPerfLogger?.point("persist_end");
}
_getWorker(options) {
if (!this._worker) {
const { forceInBand, perfLogger } = options ?? {};
if (forceInBand === true || this._options.maxWorkers <= 1) {
this._worker = {
worker: _worker.worker,
};
} else {
const workerPath = require.resolve("./worker");
perfLogger?.point("initWorkers_start");
this._worker = new _jestWorker.Worker(workerPath, {
exposedMethods: ["worker"],
maxRetries: 3,
numWorkers: this._options.maxWorkers,
enableWorkerThreads: this._options.enableWorkerThreads,
forkOptions: {
execArgv: [],
},
});
perfLogger?.point("initWorkers_end");
}
}
return (0, _nullthrows.default)(this._worker);
}
_removeIfExists(fileSystem, hasteMap, mockMap, relativeFilePath) {
const fileMetadata = fileSystem.remove(relativeFilePath);
if (fileMetadata == null) {
return;
}
const moduleName = fileMetadata[_constants.default.ID] || null;
if (moduleName == null) {
return;
}
hasteMap.removeModule(moduleName, relativeFilePath);
if (this._options.mocksPattern) {
const absoluteFilePath = path.join(
this._options.rootDir,
(0, _normalizePathSeparatorsToSystem.default)(relativeFilePath)
);
if (
this._options.mocksPattern &&
this._options.mocksPattern.test(absoluteFilePath)
) {
const mockName = (0, _getMockName.default)(absoluteFilePath);
mockMap.delete(mockName);
}
}
}
async _watch(fileSystem, hasteMap, mockMap) {
this._startupPerfLogger?.point("watch_start");
if (!this._options.watch) {
this._startupPerfLogger?.point("watch_end");
return;
}
this._options.throwOnModuleCollision = false;
this._options.retainAllFiles = true;
const hasWatchedExtension = (filePath) =>
this._options.extensions.some((ext) => filePath.endsWith(ext));
let changeQueue = Promise.resolve();
let nextEmit = null;
const emitChange = () => {
if (nextEmit == null || nextEmit.eventsQueue.length === 0) {
return;
}
const { eventsQueue, firstEventTimestamp, firstEnqueuedTimestamp } =
nextEmit;
const hmrPerfLogger = this._options.perfLoggerFactory?.("HMR", {
key: this._getNextChangeID(),
});
if (hmrPerfLogger != null) {
hmrPerfLogger.start({
timestamp: firstEventTimestamp,
});
hmrPerfLogger.point("waitingForChangeInterval_start", {
timestamp: firstEnqueuedTimestamp,
});
hmrPerfLogger.point("waitingForChangeInterval_end");
hmrPerfLogger.annotate({
int: {
eventsQueueLength: eventsQueue.length,
},
});
hmrPerfLogger.point("fileChange_start");
}
const changeEvent = {
logger: hmrPerfLogger,
eventsQueue,
};
this.emit("change", changeEvent);
nextEmit = null;
};
const onChange = (type, filePath, root, metadata) => {
if (
metadata &&
(metadata.type === "d" ||
(metadata.type === "f" && !hasWatchedExtension(filePath)) ||
(!this._options.enableSymlinks && metadata?.type === "l"))
) {
return;
}
const absoluteFilePath = path.join(
root,
(0, _normalizePathSeparatorsToSystem.default)(filePath)
);
if (this._options.ignorePattern.test(absoluteFilePath)) {
return;
}
const relativeFilePath =
this._pathUtils.absoluteToNormal(absoluteFilePath);
const linkStats = fileSystem.linkStats(relativeFilePath);
if (
type === "change" &&
linkStats != null &&
metadata &&
metadata.modifiedTime != null &&
linkStats.modifiedTime === metadata.modifiedTime
) {
return;
}
const onChangeStartTime =
_perf_hooks.performance.timeOrigin + _perf_hooks.performance.now();
changeQueue = changeQueue
.then(async () => {
if (
nextEmit != null &&
nextEmit.eventsQueue.find(
(event) =>
event.type === type &&
event.filePath === absoluteFilePath &&
((!event.metadata && !metadata) ||
(event.metadata &&
metadata &&
event.metadata.modifiedTime != null &&
metadata.modifiedTime != null &&
event.metadata.modifiedTime === metadata.modifiedTime))
)
) {
return null;
}
const linkStats = fileSystem.linkStats(relativeFilePath);
const enqueueEvent = (metadata) => {
const event = {
filePath: absoluteFilePath,
metadata,
type,
};
if (nextEmit == null) {
nextEmit = {
eventsQueue: [event],
firstEventTimestamp: onChangeStartTime,
firstEnqueuedTimestamp:
_perf_hooks.performance.timeOrigin +
_perf_hooks.performance.now(),
};
} else {
nextEmit.eventsQueue.push(event);
}
return null;
};
if (linkStats != null) {
this._removeIfExists(
fileSystem,
hasteMap,
mockMap,
relativeFilePath
);
}
if (type === "add" || type === "change") {
(0, _invariant.default)(
metadata != null && metadata.size != null,
"since the file exists or changed, it should have metadata"
);
const fileMetadata = [
"",
metadata.modifiedTime,
metadata.size,
0,
"",
null,
metadata.type === "l" ? 1 : 0,
];
try {
await this._processFile(
hasteMap,
mockMap,
absoluteFilePath,
fileMetadata,
{
forceInBand: true,
}
);
fileSystem.addOrModify(relativeFilePath, fileMetadata);
enqueueEvent(metadata);
} catch (e) {
if (!["ENOENT", "EACCESS"].includes(e.code)) {
throw e;
}
}
} else if (type === "delete") {
if (linkStats == null) {
return null;
}
enqueueEvent({
modifiedTime: null,
size: null,
type: linkStats.fileType,
});
} else {
throw new Error(
`metro-file-map: Unrecognized event type from watcher: ${type}`
);
}
return null;
})
.catch((error) => {
this._console.error(
`metro-file-map: watch error:\n ${error.stack}\n`
);
});
};
this._changeInterval = setInterval(emitChange, CHANGE_INTERVAL);
(0, _invariant.default)(
this._watcher != null,
"Expected _watcher to have been initialised by build()"
);
await this._watcher.watch(onChange);
if (this._options.healthCheck.enabled) {
const performHealthCheck = () => {
if (!this._watcher) {
return;
}
this._watcher
.checkHealth(this._options.healthCheck.timeout)
.then((result) => {
this.emit("healthCheck", result);
});
};
performHealthCheck();
this._healthCheckInterval = setInterval(
performHealthCheck,
this._options.healthCheck.interval
);
}
this._startupPerfLogger?.point("watch_end");
}
async end() {
if (this._changeInterval) {
clearInterval(this._changeInterval);
}
if (this._healthCheckInterval) {
clearInterval(this._healthCheckInterval);
}
this._crawlerAbortController.abort();
if (!this._watcher) {
return;
}
await this._watcher.close();
}
_ignore(filePath) {
const ignoreMatched = this._options.ignorePattern.test(filePath);
return (
ignoreMatched ||
(!this._options.retainAllFiles && filePath.includes(NODE_MODULES))
);
}
async _shouldUseWatchman() {
if (!this._options.useWatchman) {
return false;
}
if (!this._canUseWatchmanPromise) {
this._canUseWatchmanPromise = (0, _checkWatchmanCapabilities.default)(
WATCHMAN_REQUIRED_CAPABILITIES
)
.then(({ version }) => {
this._startupPerfLogger?.annotate({
string: {
watchmanVersion: version,
},
});
return true;
})
.catch((e) => {
this._startupPerfLogger?.annotate({
string: {
watchmanFailedCapabilityCheck: e?.message ?? "[missing]",
},
});
return false;
});
}
return this._canUseWatchmanPromise;
}
_getNextChangeID() {
if (this._changeID >= Number.MAX_SAFE_INTEGER) {
this._changeID = 0;
}
return ++this._changeID;
}
static H = _constants.default;
}
exports.default = FileMap;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.DuplicateError = void 0;
class DuplicateError extends Error {
constructor(mockPath1, mockPath2) {
super("Duplicated files or mocks. Please check the console for more info");
this.mockPath1 = mockPath1;
this.mockPath2 = mockPath2;
}
}
exports.DuplicateError = DuplicateError;

View File

@@ -0,0 +1,22 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
export class DuplicateError extends Error {
mockPath1: string;
mockPath2: string;
constructor(mockPath1: string, mockPath2: string) {
super('Duplicated files or mocks. Please check the console for more info');
this.mockPath1 = mockPath1;
this.mockPath2 = mockPath2;
}
}

View File

@@ -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 {DuplicatesSet} from '../flow-types';
export class DuplicateHasteCandidatesError extends Error {
hasteName: string;
platform: string | null;
supportsNativePlatform: boolean;
duplicatesSet: DuplicatesSet;
constructor(
name: string,
platform: string,
supportsNativePlatform: boolean,
duplicatesSet: DuplicatesSet,
);
}

View File

@@ -0,0 +1,49 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.DuplicateHasteCandidatesError = void 0;
var _constants = _interopRequireDefault(require("../constants"));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
class DuplicateHasteCandidatesError extends Error {
constructor(name, platform, supportsNativePlatform, duplicatesSet) {
const platformMessage = getPlatformMessage(platform);
super(
`The name \`${name}\` was looked up in the Haste module map. It ` +
"cannot be resolved, because there exists several different " +
"files, or packages, that provide a module for " +
`that particular name and platform. ${platformMessage} You must ` +
"delete or exclude files until there remains only one of these:\n\n" +
Array.from(duplicatesSet)
.map(
([dupFilePath, dupFileType]) =>
` * \`${dupFilePath}\` (${getTypeMessage(dupFileType)})\n`
)
.sort()
.join("")
);
this.hasteName = name;
this.platform = platform;
this.supportsNativePlatform = supportsNativePlatform;
this.duplicatesSet = duplicatesSet;
}
}
exports.DuplicateHasteCandidatesError = DuplicateHasteCandidatesError;
function getPlatformMessage(platform) {
if (platform === _constants.default.GENERIC_PLATFORM) {
return "The platform is generic (no extension).";
}
return `The platform extension is \`${platform}\`.`;
}
function getTypeMessage(type) {
switch (type) {
case _constants.default.MODULE:
return "module";
case _constants.default.PACKAGE:
return "package";
}
return "unknown";
}

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import type {DuplicatesSet} from '../flow-types';
import H from '../constants';
export class DuplicateHasteCandidatesError extends Error {
hasteName: string;
platform: string | null;
supportsNativePlatform: boolean;
duplicatesSet: DuplicatesSet;
constructor(
name: string,
platform: string,
supportsNativePlatform: boolean,
duplicatesSet: DuplicatesSet,
) {
const platformMessage = getPlatformMessage(platform);
super(
`The name \`${name}\` was looked up in the Haste module map. It ` +
'cannot be resolved, because there exists several different ' +
'files, or packages, that provide a module for ' +
`that particular name and platform. ${platformMessage} You must ` +
'delete or exclude files until there remains only one of these:\n\n' +
Array.from(duplicatesSet)
.map(
([dupFilePath, dupFileType]) =>
` * \`${dupFilePath}\` (${getTypeMessage(dupFileType)})\n`,
)
.sort()
.join(''),
);
this.hasteName = name;
this.platform = platform;
this.supportsNativePlatform = supportsNativePlatform;
this.duplicatesSet = duplicatesSet;
}
}
function getPlatformMessage(platform: string) {
if (platform === H.GENERIC_PLATFORM) {
return 'The platform is generic (no extension).';
}
return `The platform extension is \`${platform}\`.`;
}
function getTypeMessage(type: number) {
switch (type) {
case H.MODULE:
return 'module';
case H.PACKAGE:
return 'package';
}
return 'unknown';
}

View File

@@ -0,0 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
var _RootPathUtils = require("./RootPathUtils");
class MockMap {
#raw;
#rootDir;
#pathUtils;
constructor({ rawMockMap, rootDir }) {
this.#raw = rawMockMap;
this.#rootDir = rootDir;
this.#pathUtils = new _RootPathUtils.RootPathUtils(rootDir);
}
getMockModule(name) {
const mockPath = this.#raw.get(name) || this.#raw.get(name + "/index");
return mockPath != null ? this.#pathUtils.normalToAbsolute(mockPath) : null;
}
}
exports.default = MockMap;

View File

@@ -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.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import type {MockMap as IMockMap, Path, RawMockMap} from '../flow-types';
import {RootPathUtils} from './RootPathUtils';
export default class MockMap implements IMockMap {
+#raw: RawMockMap;
+#rootDir: Path;
+#pathUtils: RootPathUtils;
constructor({rawMockMap, rootDir}: {rawMockMap: RawMockMap, rootDir: Path}) {
this.#raw = rawMockMap;
this.#rootDir = rootDir;
this.#pathUtils = new RootPathUtils(rootDir);
}
getMockModule(name: string): ?Path {
const mockPath = this.#raw.get(name) || this.#raw.get(name + '/index');
return mockPath != null ? this.#pathUtils.normalToAbsolute(mockPath) : null;
}
}

View File

@@ -0,0 +1,262 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
var _constants = _interopRequireDefault(require("../constants"));
var _DuplicateError = require("./DuplicateError");
var _DuplicateHasteCandidatesError = require("./DuplicateHasteCandidatesError");
var _getPlatformExtension = _interopRequireDefault(
require("./getPlatformExtension")
);
var _RootPathUtils = require("./RootPathUtils");
var _path = _interopRequireDefault(require("path"));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const EMPTY_OBJ = {};
const EMPTY_MAP = new Map();
class MutableHasteMap {
#rootDir;
#map = new Map();
#duplicates = new Map();
#console;
#pathUtils;
#platforms;
#throwOnModuleCollision;
constructor(options) {
this.#console = options.console ?? null;
this.#platforms = options.platforms;
this.#rootDir = options.rootDir;
this.#pathUtils = new _RootPathUtils.RootPathUtils(options.rootDir);
this.#throwOnModuleCollision = options.throwOnModuleCollision;
}
static fromDeserializedSnapshot(deserializedData, options) {
const hasteMap = new MutableHasteMap(options);
hasteMap.#map = deserializedData.map;
hasteMap.#duplicates = deserializedData.duplicates;
return hasteMap;
}
getSerializableSnapshot() {
const mapMap = (map, mapFn) => {
return new Map(
Array.from(map.entries(), ([key, val]) => [key, mapFn(val)])
);
};
return {
duplicates: mapMap(this.#duplicates, (v) =>
mapMap(v, (v2) => new Map(v2.entries()))
),
map: mapMap(this.#map, (v) =>
Object.assign(
Object.create(null),
Object.fromEntries(
Array.from(Object.entries(v), ([key, val]) => [key, [...val]])
)
)
),
};
}
getModule(name, platform, supportsNativePlatform, type) {
const module = this._getModuleMetadata(
name,
platform,
!!supportsNativePlatform
);
if (
module &&
module[_constants.default.TYPE] === (type ?? _constants.default.MODULE)
) {
const modulePath = module[_constants.default.PATH];
return modulePath && this.#pathUtils.normalToAbsolute(modulePath);
}
return null;
}
getPackage(name, platform, _supportsNativePlatform) {
return this.getModule(name, platform, null, _constants.default.PACKAGE);
}
getRawHasteMap() {
return {
duplicates: this.#duplicates,
map: this.#map,
};
}
_getModuleMetadata(name, platform, supportsNativePlatform) {
const map = this.#map.get(name) || EMPTY_OBJ;
const dupMap = this.#duplicates.get(name) || EMPTY_MAP;
if (platform != null) {
this._assertNoDuplicates(
name,
platform,
supportsNativePlatform,
dupMap.get(platform)
);
if (map[platform] != null) {
return map[platform];
}
}
if (supportsNativePlatform) {
this._assertNoDuplicates(
name,
_constants.default.NATIVE_PLATFORM,
supportsNativePlatform,
dupMap.get(_constants.default.NATIVE_PLATFORM)
);
if (map[_constants.default.NATIVE_PLATFORM]) {
return map[_constants.default.NATIVE_PLATFORM];
}
}
this._assertNoDuplicates(
name,
_constants.default.GENERIC_PLATFORM,
supportsNativePlatform,
dupMap.get(_constants.default.GENERIC_PLATFORM)
);
if (map[_constants.default.GENERIC_PLATFORM]) {
return map[_constants.default.GENERIC_PLATFORM];
}
return null;
}
_assertNoDuplicates(name, platform, supportsNativePlatform, relativePathSet) {
if (relativePathSet == null) {
return;
}
const duplicates = new Map();
for (const [relativePath, type] of relativePathSet) {
const duplicatePath = this.#pathUtils.normalToAbsolute(relativePath);
duplicates.set(duplicatePath, type);
}
throw new _DuplicateHasteCandidatesError.DuplicateHasteCandidatesError(
name,
platform,
supportsNativePlatform,
duplicates
);
}
setModule(id, module) {
let hasteMapItem = this.#map.get(id);
if (!hasteMapItem) {
hasteMapItem = Object.create(null);
this.#map.set(id, hasteMapItem);
}
const platform =
(0, _getPlatformExtension.default)(
module[_constants.default.PATH],
this.#platforms
) || _constants.default.GENERIC_PLATFORM;
const existingModule = hasteMapItem[platform];
if (
existingModule &&
existingModule[_constants.default.PATH] !==
module[_constants.default.PATH]
) {
if (this.#console) {
const method = this.#throwOnModuleCollision ? "error" : "warn";
this.#console[method](
[
"metro-file-map: Haste module naming collision: " + id,
" The following files share their name; please adjust your hasteImpl:",
" * <rootDir>" +
_path.default.sep +
existingModule[_constants.default.PATH],
" * <rootDir>" +
_path.default.sep +
module[_constants.default.PATH],
"",
].join("\n")
);
}
if (this.#throwOnModuleCollision) {
throw new _DuplicateError.DuplicateError(
existingModule[_constants.default.PATH],
module[_constants.default.PATH]
);
}
delete hasteMapItem[platform];
if (Object.keys(hasteMapItem).length === 0) {
this.#map.delete(id);
}
let dupsByPlatform = this.#duplicates.get(id);
if (dupsByPlatform == null) {
dupsByPlatform = new Map();
this.#duplicates.set(id, dupsByPlatform);
}
const dups = new Map([
[module[_constants.default.PATH], module[_constants.default.TYPE]],
[
existingModule[_constants.default.PATH],
existingModule[_constants.default.TYPE],
],
]);
dupsByPlatform.set(platform, dups);
return;
}
const dupsByPlatform = this.#duplicates.get(id);
if (dupsByPlatform != null) {
const dups = dupsByPlatform.get(platform);
if (dups != null) {
dups.set(
module[_constants.default.PATH],
module[_constants.default.TYPE]
);
}
return;
}
hasteMapItem[platform] = module;
}
removeModule(moduleName, relativeFilePath) {
const platform =
(0, _getPlatformExtension.default)(relativeFilePath, this.#platforms) ||
_constants.default.GENERIC_PLATFORM;
const hasteMapItem = this.#map.get(moduleName);
if (hasteMapItem != null) {
delete hasteMapItem[platform];
if (Object.keys(hasteMapItem).length === 0) {
this.#map.delete(moduleName);
} else {
this.#map.set(moduleName, hasteMapItem);
}
}
this._recoverDuplicates(moduleName, relativeFilePath);
}
setThrowOnModuleCollision(shouldThrow) {
this.#throwOnModuleCollision = shouldThrow;
}
_recoverDuplicates(moduleName, relativeFilePath) {
let dupsByPlatform = this.#duplicates.get(moduleName);
if (dupsByPlatform == null) {
return;
}
const platform =
(0, _getPlatformExtension.default)(relativeFilePath, this.#platforms) ||
_constants.default.GENERIC_PLATFORM;
let dups = dupsByPlatform.get(platform);
if (dups == null) {
return;
}
dupsByPlatform = new Map(dupsByPlatform);
this.#duplicates.set(moduleName, dupsByPlatform);
dups = new Map(dups);
dupsByPlatform.set(platform, dups);
dups.delete(relativeFilePath);
if (dups.size !== 1) {
return;
}
const uniqueModule = dups.entries().next().value;
if (!uniqueModule) {
return;
}
let dedupMap = this.#map.get(moduleName);
if (dedupMap == null) {
dedupMap = Object.create(null);
this.#map.set(moduleName, dedupMap);
}
dedupMap[platform] = uniqueModule;
dupsByPlatform.delete(platform);
if (dupsByPlatform.size === 0) {
this.#duplicates.delete(moduleName);
}
}
}
exports.default = MutableHasteMap;

View File

@@ -0,0 +1,342 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import type {
Console,
DuplicatesIndex,
DuplicatesSet,
HasteMap,
HasteMapItem,
HasteMapItemMetaData,
HTypeValue,
Path,
RawHasteMap,
ReadOnlyRawHasteMap,
} from '../flow-types';
import H from '../constants';
import {DuplicateError} from './DuplicateError';
import {DuplicateHasteCandidatesError} from './DuplicateHasteCandidatesError';
import getPlatformExtension from './getPlatformExtension';
import {RootPathUtils} from './RootPathUtils';
import path from 'path';
const EMPTY_OBJ: $ReadOnly<{[string]: HasteMapItemMetaData}> = {};
const EMPTY_MAP: $ReadOnlyMap<string, DuplicatesSet> = new Map();
type HasteMapOptions = $ReadOnly<{
console?: ?Console,
platforms: $ReadOnlySet<string>,
rootDir: Path,
throwOnModuleCollision: boolean,
}>;
export default class MutableHasteMap implements HasteMap {
+#rootDir: Path;
#map: Map<string, HasteMapItem> = new Map();
#duplicates: DuplicatesIndex = new Map();
+#console: ?Console;
+#pathUtils: RootPathUtils;
+#platforms: $ReadOnlySet<string>;
#throwOnModuleCollision: boolean;
constructor(options: HasteMapOptions) {
this.#console = options.console ?? null;
this.#platforms = options.platforms;
this.#rootDir = options.rootDir;
this.#pathUtils = new RootPathUtils(options.rootDir);
this.#throwOnModuleCollision = options.throwOnModuleCollision;
}
static fromDeserializedSnapshot(
deserializedData: RawHasteMap,
options: HasteMapOptions,
): MutableHasteMap {
const hasteMap = new MutableHasteMap(options);
hasteMap.#map = deserializedData.map;
hasteMap.#duplicates = deserializedData.duplicates;
return hasteMap;
}
getSerializableSnapshot(): RawHasteMap {
const mapMap = <K, V1, V2>(
map: $ReadOnlyMap<K, V1>,
mapFn: (v: V1) => V2,
): Map<K, V2> => {
return new Map(
Array.from(map.entries(), ([key, val]): [K, V2] => [key, mapFn(val)]),
);
};
return {
duplicates: mapMap(this.#duplicates, v =>
mapMap(v, v2 => new Map(v2.entries())),
),
map: mapMap(this.#map, v =>
Object.assign(
Object.create(null),
Object.fromEntries(
Array.from(Object.entries(v), ([key, val]) => [key, [...val]]),
),
),
),
};
}
getModule(
name: string,
platform?: ?string,
supportsNativePlatform?: ?boolean,
type?: ?HTypeValue,
): ?Path {
const module = this._getModuleMetadata(
name,
platform,
!!supportsNativePlatform,
);
if (module && module[H.TYPE] === (type ?? H.MODULE)) {
const modulePath = module[H.PATH];
return modulePath && this.#pathUtils.normalToAbsolute(modulePath);
}
return null;
}
getPackage(
name: string,
platform: ?string,
_supportsNativePlatform?: ?boolean,
): ?Path {
return this.getModule(name, platform, null, H.PACKAGE);
}
// FIXME: This is only used by Meta-internal validation and should be
// removed or replaced with a less leaky API.
getRawHasteMap(): ReadOnlyRawHasteMap {
return {
duplicates: this.#duplicates,
map: this.#map,
};
}
/**
* When looking up a module's data, we walk through each eligible platform for
* the query. For each platform, we want to check if there are known
* duplicates for that name+platform pair. The duplication logic normally
* removes elements from the `map` object, but we want to check upfront to be
* extra sure. If metadata exists both in the `duplicates` object and the
* `map`, this would be a bug.
*/
_getModuleMetadata(
name: string,
platform: ?string,
supportsNativePlatform: boolean,
): HasteMapItemMetaData | null {
const map = this.#map.get(name) || EMPTY_OBJ;
const dupMap = this.#duplicates.get(name) || EMPTY_MAP;
if (platform != null) {
this._assertNoDuplicates(
name,
platform,
supportsNativePlatform,
dupMap.get(platform),
);
if (map[platform] != null) {
return map[platform];
}
}
if (supportsNativePlatform) {
this._assertNoDuplicates(
name,
H.NATIVE_PLATFORM,
supportsNativePlatform,
dupMap.get(H.NATIVE_PLATFORM),
);
if (map[H.NATIVE_PLATFORM]) {
return map[H.NATIVE_PLATFORM];
}
}
this._assertNoDuplicates(
name,
H.GENERIC_PLATFORM,
supportsNativePlatform,
dupMap.get(H.GENERIC_PLATFORM),
);
if (map[H.GENERIC_PLATFORM]) {
return map[H.GENERIC_PLATFORM];
}
return null;
}
_assertNoDuplicates(
name: string,
platform: string,
supportsNativePlatform: boolean,
relativePathSet: ?DuplicatesSet,
): void {
if (relativePathSet == null) {
return;
}
const duplicates = new Map<string, number>();
for (const [relativePath, type] of relativePathSet) {
const duplicatePath = this.#pathUtils.normalToAbsolute(relativePath);
duplicates.set(duplicatePath, type);
}
throw new DuplicateHasteCandidatesError(
name,
platform,
supportsNativePlatform,
duplicates,
);
}
setModule(id: string, module: HasteMapItemMetaData): void {
let hasteMapItem = this.#map.get(id);
if (!hasteMapItem) {
// $FlowFixMe[unclear-type] - Add type coverage
hasteMapItem = (Object.create(null): any);
this.#map.set(id, hasteMapItem);
}
const platform =
getPlatformExtension(module[H.PATH], this.#platforms) ||
H.GENERIC_PLATFORM;
const existingModule = hasteMapItem[platform];
if (existingModule && existingModule[H.PATH] !== module[H.PATH]) {
if (this.#console) {
const method = this.#throwOnModuleCollision ? 'error' : 'warn';
this.#console[method](
[
'metro-file-map: Haste module naming collision: ' + id,
' The following files share their name; please adjust your hasteImpl:',
' * <rootDir>' + path.sep + existingModule[H.PATH],
' * <rootDir>' + path.sep + module[H.PATH],
'',
].join('\n'),
);
}
if (this.#throwOnModuleCollision) {
throw new DuplicateError(existingModule[H.PATH], module[H.PATH]);
}
// We do NOT want consumers to use a module that is ambiguous.
delete hasteMapItem[platform];
if (Object.keys(hasteMapItem).length === 0) {
this.#map.delete(id);
}
let dupsByPlatform = this.#duplicates.get(id);
if (dupsByPlatform == null) {
dupsByPlatform = new Map();
this.#duplicates.set(id, dupsByPlatform);
}
const dups = new Map([
[module[H.PATH], module[H.TYPE]],
[existingModule[H.PATH], existingModule[H.TYPE]],
]);
dupsByPlatform.set(platform, dups);
return;
}
const dupsByPlatform = this.#duplicates.get(id);
if (dupsByPlatform != null) {
const dups = dupsByPlatform.get(platform);
if (dups != null) {
dups.set(module[H.PATH], module[H.TYPE]);
}
return;
}
hasteMapItem[platform] = module;
}
removeModule(moduleName: string, relativeFilePath: string) {
const platform =
getPlatformExtension(relativeFilePath, this.#platforms) ||
H.GENERIC_PLATFORM;
const hasteMapItem = this.#map.get(moduleName);
if (hasteMapItem != null) {
delete hasteMapItem[platform];
if (Object.keys(hasteMapItem).length === 0) {
this.#map.delete(moduleName);
} else {
this.#map.set(moduleName, hasteMapItem);
}
}
this._recoverDuplicates(moduleName, relativeFilePath);
}
setThrowOnModuleCollision(shouldThrow: boolean) {
this.#throwOnModuleCollision = shouldThrow;
}
/**
* This function should be called when the file under `filePath` is removed
* or changed. When that happens, we want to figure out if that file was
* part of a group of files that had the same ID. If it was, we want to
* remove it from the group. Furthermore, if there is only one file
* remaining in the group, then we want to restore that single file as the
* correct resolution for its ID, and cleanup the duplicates index.
*/
_recoverDuplicates(moduleName: string, relativeFilePath: string) {
let dupsByPlatform = this.#duplicates.get(moduleName);
if (dupsByPlatform == null) {
return;
}
const platform =
getPlatformExtension(relativeFilePath, this.#platforms) ||
H.GENERIC_PLATFORM;
let dups = dupsByPlatform.get(platform);
if (dups == null) {
return;
}
dupsByPlatform = new Map(dupsByPlatform);
this.#duplicates.set(moduleName, dupsByPlatform);
dups = new Map(dups);
dupsByPlatform.set(platform, dups);
dups.delete(relativeFilePath);
if (dups.size !== 1) {
return;
}
const uniqueModule = dups.entries().next().value;
if (!uniqueModule) {
return;
}
let dedupMap: ?HasteMapItem = this.#map.get(moduleName);
if (dedupMap == null) {
dedupMap = (Object.create(null): HasteMapItem);
this.#map.set(moduleName, dedupMap);
}
dedupMap[platform] = uniqueModule;
dupsByPlatform.delete(platform);
if (dupsByPlatform.size === 0) {
this.#duplicates.delete(moduleName);
}
}
}

View File

@@ -0,0 +1,255 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.RootPathUtils = void 0;
var _invariant = _interopRequireDefault(require("invariant"));
var path = _interopRequireWildcard(require("path"));
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function (nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interopRequireWildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) {
return { default: obj };
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {};
var hasPropertyDescriptor =
Object.defineProperty && Object.getOwnPropertyDescriptor;
for (var key in obj) {
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor
? Object.getOwnPropertyDescriptor(obj, key)
: null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const UP_FRAGMENT_SEP = ".." + path.sep;
const SEP_UP_FRAGMENT = path.sep + "..";
const UP_FRAGMENT_SEP_LENGTH = UP_FRAGMENT_SEP.length;
const CURRENT_FRAGMENT = "." + path.sep;
class RootPathUtils {
#rootDir;
#rootDirnames;
#rootParts;
#rootDepth;
constructor(rootDir) {
this.#rootDir = rootDir;
const rootDirnames = [];
for (
let next = rootDir, previous = null;
previous !== next;
previous = next, next = path.dirname(next)
) {
rootDirnames.push(next);
}
this.#rootDirnames = rootDirnames;
this.#rootParts = rootDir.split(path.sep);
this.#rootDepth = rootDirnames.length - 1;
if (this.#rootDepth === 0) {
this.#rootParts.pop();
}
}
getBasenameOfNthAncestor(n) {
return this.#rootParts[this.#rootParts.length - 1 - n];
}
getParts() {
return this.#rootParts;
}
absoluteToNormal(absolutePath) {
let endOfMatchingPrefix = 0;
let lastMatchingPartIdx = 0;
for (
let nextPart = this.#rootParts[0], nextLength = nextPart.length;
nextPart != null &&
absolutePath.startsWith(nextPart, endOfMatchingPrefix) &&
(absolutePath.length === endOfMatchingPrefix + nextLength ||
absolutePath[endOfMatchingPrefix + nextLength] === path.sep);
) {
endOfMatchingPrefix += nextLength + 1;
nextPart = this.#rootParts[++lastMatchingPartIdx];
nextLength = nextPart?.length;
}
const upIndirectionsToPrepend =
this.#rootParts.length - lastMatchingPartIdx;
return (
this.#tryCollapseIndirectionsInSuffix(
absolutePath,
endOfMatchingPrefix,
upIndirectionsToPrepend
)?.collapsedPath ?? this.#slowAbsoluteToNormal(absolutePath)
);
}
#slowAbsoluteToNormal(absolutePath) {
const endsWithSep = absolutePath.endsWith(path.sep);
const result = path.relative(this.#rootDir, absolutePath);
return endsWithSep && !result.endsWith(path.sep)
? result + path.sep
: result;
}
normalToAbsolute(normalPath) {
let left = this.#rootDir;
let i = 0;
let pos = 0;
while (
normalPath.startsWith(UP_FRAGMENT_SEP, pos) ||
(normalPath.endsWith("..") && normalPath.length === 2 + pos)
) {
left = this.#rootDirnames[i === this.#rootDepth ? this.#rootDepth : ++i];
pos += UP_FRAGMENT_SEP_LENGTH;
}
const right = pos === 0 ? normalPath : normalPath.slice(pos);
if (right.length === 0) {
return left;
}
if (i === this.#rootDepth) {
return left + right;
}
return left + path.sep + right;
}
relativeToNormal(relativePath) {
return (
this.#tryCollapseIndirectionsInSuffix(relativePath, 0, 0)
?.collapsedPath ??
path.relative(this.#rootDir, path.join(this.#rootDir, relativePath))
);
}
getAncestorOfRootIdx(normalPath) {
if (normalPath === "") {
return 0;
}
if (normalPath === "..") {
return 1;
}
if (normalPath.endsWith(SEP_UP_FRAGMENT)) {
return (normalPath.length + 1) / 3;
}
return null;
}
joinNormalToRelative(normalPath, relativePath) {
if (normalPath === "") {
return {
collapsedSegments: 0,
normalPath: relativePath,
};
}
if (relativePath === "") {
return {
collapsedSegments: 0,
normalPath,
};
}
const left = normalPath + path.sep;
const rawPath = left + relativePath;
if (normalPath === ".." || normalPath.endsWith(SEP_UP_FRAGMENT)) {
const collapsed = this.#tryCollapseIndirectionsInSuffix(rawPath, 0, 0);
(0, _invariant.default)(collapsed != null, "Failed to collapse");
return {
collapsedSegments: collapsed.collapsedSegments,
normalPath: collapsed.collapsedPath,
};
}
return {
collapsedSegments: 0,
normalPath: rawPath,
};
}
relative(from, to) {
return path.relative(from, to);
}
#tryCollapseIndirectionsInSuffix(
fullPath,
startOfRelativePart,
implicitUpIndirections
) {
let totalUpIndirections = implicitUpIndirections;
let collapsedSegments = 0;
for (let pos = startOfRelativePart; ; pos += UP_FRAGMENT_SEP_LENGTH) {
const nextIndirection = fullPath.indexOf(CURRENT_FRAGMENT, pos);
if (nextIndirection === -1) {
while (totalUpIndirections > 0) {
const segmentToMaybeCollapse =
this.#rootParts[this.#rootParts.length - totalUpIndirections];
if (
fullPath.startsWith(segmentToMaybeCollapse, pos) &&
(fullPath.length === segmentToMaybeCollapse.length + pos ||
fullPath[segmentToMaybeCollapse.length + pos] === path.sep)
) {
pos += segmentToMaybeCollapse.length + 1;
collapsedSegments++;
totalUpIndirections--;
} else {
break;
}
}
if (pos >= fullPath.length) {
return {
collapsedPath:
totalUpIndirections > 0
? UP_FRAGMENT_SEP.repeat(totalUpIndirections - 1) +
".." +
fullPath.slice(pos - 1)
: "",
collapsedSegments,
};
}
const right = pos > 0 ? fullPath.slice(pos) : fullPath;
if (
right === ".." &&
totalUpIndirections >= this.#rootParts.length - 1
) {
return {
collapsedPath: UP_FRAGMENT_SEP.repeat(totalUpIndirections).slice(
0,
-1
),
collapsedSegments,
};
}
if (totalUpIndirections === 0) {
return {
collapsedPath: right,
collapsedSegments,
};
}
return {
collapsedPath: UP_FRAGMENT_SEP.repeat(totalUpIndirections) + right,
collapsedSegments,
};
}
if (totalUpIndirections < this.#rootParts.length - 1) {
totalUpIndirections++;
}
if (nextIndirection !== pos + 1 || fullPath[pos] !== ".") {
return null;
}
}
}
}
exports.RootPathUtils = RootPathUtils;

View File

@@ -0,0 +1,314 @@
/**
* 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
* @flow strict
*/
import invariant from 'invariant';
import * as path from 'path';
/**
* This module provides path utility functions - similar to `node:path` -
* optimised for Metro's use case (many paths, few roots) under assumptions
* typically safe to make within Metro - namely:
*
* - All input path separators must be system-native.
* - Double/redundant separators like '/foo//bar' are not supported.
* - All characters except separators are assumed to be valid in path segments.
*
* - A "well-formed" path is any path following the rules above.
* - A "normal" path is a root-relative well-formed path with no redundant
* indirections. Normal paths have no leading './`, and the normal path of
* the root is the empty string.
*
* Output and input paths are at least well-formed (normal where indicated by
* naming).
*
* Trailing path separators are preserved, except for fs roots in
* normalToAbsolute (fs roots always have a trailing separator), and the
* project root in absoluteToNormal and relativeToNormal (the project root is
* always the empty string, and is always a directory, so a trailing separator
* is redundant).
*
* As of Node 20, absoluteToNormal is ~8x faster than `path.relative` and
* `normalToAbsolute` is ~20x faster than `path.resolve`, benchmarked on the
* real inputs from building FB's product graph. Some well-formed inputs
* (e.g., /project/./foo/../bar), are handled but not optimised, and we fall
* back to `node:path` equivalents in those cases.
*/
const UP_FRAGMENT_SEP = '..' + path.sep;
const SEP_UP_FRAGMENT = path.sep + '..';
const UP_FRAGMENT_SEP_LENGTH = UP_FRAGMENT_SEP.length;
const CURRENT_FRAGMENT = '.' + path.sep;
export class RootPathUtils {
#rootDir: string;
#rootDirnames: $ReadOnlyArray<string>;
#rootParts: $ReadOnlyArray<string>;
#rootDepth: number;
constructor(rootDir: string) {
this.#rootDir = rootDir;
const rootDirnames = [];
for (
let next = rootDir, previous = null;
previous !== next;
previous = next, next = path.dirname(next)
) {
rootDirnames.push(next);
}
this.#rootDirnames = rootDirnames;
this.#rootParts = rootDir.split(path.sep);
this.#rootDepth = rootDirnames.length - 1;
// If rootDir is a filesystem root (C:\ or /), it will end in a separator and
// give a spurious empty entry at the end of rootParts.
if (this.#rootDepth === 0) {
this.#rootParts.pop();
}
}
getBasenameOfNthAncestor(n: number): string {
return this.#rootParts[this.#rootParts.length - 1 - n];
}
getParts(): $ReadOnlyArray<string> {
return this.#rootParts;
}
// absolutePath may be any well-formed absolute path.
absoluteToNormal(absolutePath: string): string {
let endOfMatchingPrefix = 0;
let lastMatchingPartIdx = 0;
for (
let nextPart = this.#rootParts[0], nextLength = nextPart.length;
nextPart != null &&
// Check that absolutePath is equal to nextPart + '/' or ends with
// nextPart, starting from endOfMatchingPrefix.
absolutePath.startsWith(nextPart, endOfMatchingPrefix) &&
(absolutePath.length === endOfMatchingPrefix + nextLength ||
absolutePath[endOfMatchingPrefix + nextLength] === path.sep);
) {
// Move our matching pointer forward and load the next part.
endOfMatchingPrefix += nextLength + 1;
nextPart = this.#rootParts[++lastMatchingPartIdx];
nextLength = nextPart?.length;
}
// If our root is /project/root and we're given /project/bar/foo.js, we
// have matched up to '/project', and will need to return a path
// beginning '../' (one prepended indirection, to go up from 'root').
//
// If we're given /project/../project2/otherroot, we have one level of
// indirection up to prepend in the same way as above. There's another
// explicit indirection already present in the input - we'll account for
// that in tryCollapseIndirectionsInSuffix.
const upIndirectionsToPrepend =
this.#rootParts.length - lastMatchingPartIdx;
return (
this.#tryCollapseIndirectionsInSuffix(
absolutePath,
endOfMatchingPrefix,
upIndirectionsToPrepend,
)?.collapsedPath ?? this.#slowAbsoluteToNormal(absolutePath)
);
}
#slowAbsoluteToNormal(absolutePath: string): string {
const endsWithSep = absolutePath.endsWith(path.sep);
const result = path.relative(this.#rootDir, absolutePath);
return endsWithSep && !result.endsWith(path.sep)
? result + path.sep
: result;
}
// `normalPath` is assumed to be normal (root-relative, no redundant
// indirection), per the definition above.
normalToAbsolute(normalPath: string): string {
let left = this.#rootDir;
let i = 0;
let pos = 0;
while (
normalPath.startsWith(UP_FRAGMENT_SEP, pos) ||
(normalPath.endsWith('..') && normalPath.length === 2 + pos)
) {
left = this.#rootDirnames[i === this.#rootDepth ? this.#rootDepth : ++i];
pos += UP_FRAGMENT_SEP_LENGTH;
}
const right = pos === 0 ? normalPath : normalPath.slice(pos);
if (right.length === 0) {
return left;
}
// left may already end in a path separator only if it is a filesystem root,
// '/' or 'X:\'.
if (i === this.#rootDepth) {
return left + right;
}
return left + path.sep + right;
}
relativeToNormal(relativePath: string): string {
return (
this.#tryCollapseIndirectionsInSuffix(relativePath, 0, 0)
?.collapsedPath ??
path.relative(this.#rootDir, path.join(this.#rootDir, relativePath))
);
}
// If a path is a direct ancestor of the project root (or the root itself),
// return a number with the degrees of separation, e.g. root=0, parent=1,..
// or null otherwise.
getAncestorOfRootIdx(normalPath: string): ?number {
if (normalPath === '') {
return 0;
}
if (normalPath === '..') {
return 1;
}
// Otherwise a *normal* path is only a root ancestor if it is a sequence of
// '../' segments followed by '..', so the length tells us the number of
// up fragments.
if (normalPath.endsWith(SEP_UP_FRAGMENT)) {
return (normalPath.length + 1) / 3;
}
return null;
}
// Takes a normal and relative path, and joins them efficiently into a normal
// path, including collapsing trailing '..' in the first part with leading
// project root segments in the relative part.
joinNormalToRelative(
normalPath: string,
relativePath: string,
): {normalPath: string, collapsedSegments: number} {
if (normalPath === '') {
return {collapsedSegments: 0, normalPath: relativePath};
}
if (relativePath === '') {
return {collapsedSegments: 0, normalPath};
}
const left = normalPath + path.sep;
const rawPath = left + relativePath;
if (normalPath === '..' || normalPath.endsWith(SEP_UP_FRAGMENT)) {
const collapsed = this.#tryCollapseIndirectionsInSuffix(rawPath, 0, 0);
invariant(collapsed != null, 'Failed to collapse');
return {
collapsedSegments: collapsed.collapsedSegments,
normalPath: collapsed.collapsedPath,
};
}
return {
collapsedSegments: 0,
normalPath: rawPath,
};
}
relative(from: string, to: string): string {
return path.relative(from, to);
}
// Internal: Tries to collapse sequences like `../root/foo` for root
// `/project/root` down to the normal 'foo'.
#tryCollapseIndirectionsInSuffix(
fullPath: string, // A string ending with the relative path to process
startOfRelativePart: number, // Index of the start of part to process
implicitUpIndirections: number, // 0=root-relative, 1=dirname(root)-relative...
): ?{collapsedPath: string, collapsedSegments: number} {
let totalUpIndirections = implicitUpIndirections;
let collapsedSegments = 0;
// Allow any sequence of indirection fragments at the start of the
// unmatched suffix e.g /project/[../../foo], but bail out to Node's
// path.relative if we find a possible indirection after any later segment,
// or on any "./" that isn't a "../".
for (let pos = startOfRelativePart; ; pos += UP_FRAGMENT_SEP_LENGTH) {
const nextIndirection = fullPath.indexOf(CURRENT_FRAGMENT, pos);
if (nextIndirection === -1) {
// If we have any indirections, they may "collapse" if a subsequent
// segment re-enters a directory we had previously exited, e.g:
// /project/root/../root/foo should collapse to /project/root/foo' and
// return foo, not ../root/foo.
//
// We match each segment following redirections, in turn, against the
// part of the root path they may collapse into, and break on the first
// mismatch.
while (totalUpIndirections > 0) {
const segmentToMaybeCollapse =
this.#rootParts[this.#rootParts.length - totalUpIndirections];
if (
fullPath.startsWith(segmentToMaybeCollapse, pos) &&
// The following character should be either a separator or end of
// string
(fullPath.length === segmentToMaybeCollapse.length + pos ||
fullPath[segmentToMaybeCollapse.length + pos] === path.sep)
) {
pos += segmentToMaybeCollapse.length + 1;
collapsedSegments++;
totalUpIndirections--;
} else {
break;
}
}
// After collapsing we may have no more segments remaining (following
// '..' indirections). Ensure that we don't drop or add a trailing
// separator in this case by taking .slice(pos-1). In any other case,
// we know that fullPath[pos] is a separator.
if (pos >= fullPath.length) {
return {
collapsedPath:
totalUpIndirections > 0
? UP_FRAGMENT_SEP.repeat(totalUpIndirections - 1) +
'..' +
fullPath.slice(pos - 1)
: '',
collapsedSegments,
};
}
const right = pos > 0 ? fullPath.slice(pos) : fullPath;
if (
right === '..' &&
totalUpIndirections >= this.#rootParts.length - 1
) {
// If we have no right side (or an indirection that would take us
// below the root), just ensure we don't include a trailing separtor.
return {
collapsedPath: UP_FRAGMENT_SEP.repeat(totalUpIndirections).slice(
0,
-1,
),
collapsedSegments,
};
}
// Optimisation for the common case, saves a concatenation.
if (totalUpIndirections === 0) {
return {collapsedPath: right, collapsedSegments};
}
return {
collapsedPath: UP_FRAGMENT_SEP.repeat(totalUpIndirections) + right,
collapsedSegments,
};
}
// Cap the number of indirections at the total number of root segments.
// File systems treat '..' at the root as '.'.
if (totalUpIndirections < this.#rootParts.length - 1) {
totalUpIndirections++;
}
if (
nextIndirection !== pos + 1 || // Fallback when ./ later in the path, or leading
fullPath[pos] !== '.' // and for anything other than a leading ../
) {
return null;
}
}
}
}

View File

@@ -0,0 +1,798 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
var _constants = _interopRequireDefault(require("../constants"));
var _RootPathUtils = require("./RootPathUtils");
var _invariant = _interopRequireDefault(require("invariant"));
var _path = _interopRequireDefault(require("path"));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
function isDirectory(node) {
return node instanceof Map;
}
function isRegularFile(node) {
return node[_constants.default.SYMLINK] === 0;
}
class TreeFS {
#cachedNormalSymlinkTargets = new WeakMap();
#rootDir;
#rootNode = new Map();
#pathUtils;
constructor({ rootDir, files }) {
this.#rootDir = rootDir;
this.#pathUtils = new _RootPathUtils.RootPathUtils(rootDir);
if (files != null) {
this.bulkAddOrModify(files);
}
}
getSerializableSnapshot() {
return this._cloneTree(this.#rootNode);
}
static fromDeserializedSnapshot({ rootDir, fileSystemData }) {
const tfs = new TreeFS({
rootDir,
});
tfs.#rootNode = fileSystemData;
return tfs;
}
getModuleName(mixedPath) {
const fileMetadata = this._getFileData(mixedPath);
return (fileMetadata && fileMetadata[_constants.default.ID]) ?? null;
}
getSize(mixedPath) {
const fileMetadata = this._getFileData(mixedPath);
return (fileMetadata && fileMetadata[_constants.default.SIZE]) ?? null;
}
getDependencies(mixedPath) {
const fileMetadata = this._getFileData(mixedPath);
if (fileMetadata) {
return fileMetadata[_constants.default.DEPENDENCIES]
? fileMetadata[_constants.default.DEPENDENCIES].split(
_constants.default.DEPENDENCY_DELIM
)
: [];
} else {
return null;
}
}
getDifference(files) {
const changedFiles = new Map(files);
const removedFiles = new Set();
for (const { canonicalPath, metadata } of this.metadataIterator({
includeSymlinks: true,
includeNodeModules: true,
})) {
const newMetadata = files.get(canonicalPath);
if (newMetadata) {
if (isRegularFile(newMetadata) !== isRegularFile(metadata)) {
continue;
}
if (
newMetadata[_constants.default.MTIME] != null &&
newMetadata[_constants.default.MTIME] != 0 &&
newMetadata[_constants.default.MTIME] ===
metadata[_constants.default.MTIME]
) {
changedFiles.delete(canonicalPath);
} else if (
newMetadata[_constants.default.SHA1] != null &&
newMetadata[_constants.default.SHA1] ===
metadata[_constants.default.SHA1] &&
metadata[_constants.default.VISITED] === 1
) {
const updatedMetadata = [...metadata];
updatedMetadata[_constants.default.MTIME] =
newMetadata[_constants.default.MTIME];
changedFiles.set(canonicalPath, updatedMetadata);
}
} else {
removedFiles.add(canonicalPath);
}
}
return {
changedFiles,
removedFiles,
};
}
getSha1(mixedPath) {
const fileMetadata = this._getFileData(mixedPath);
return (fileMetadata && fileMetadata[_constants.default.SHA1]) ?? null;
}
exists(mixedPath) {
const result = this._getFileData(mixedPath);
return result != null;
}
lookup(mixedPath) {
const normalPath = this._normalizePath(mixedPath);
const links = new Set();
const result = this._lookupByNormalPath(normalPath, {
collectLinkPaths: links,
followLeaf: true,
});
if (!result.exists) {
const { canonicalMissingPath } = result;
return {
exists: false,
links,
missing: this.#pathUtils.normalToAbsolute(canonicalMissingPath),
};
}
const { canonicalPath, node } = result;
const type = isDirectory(node) ? "d" : isRegularFile(node) ? "f" : "l";
(0, _invariant.default)(
type !== "l",
"lookup follows symlinks, so should never return one (%s -> %s)",
mixedPath,
canonicalPath
);
return {
exists: true,
links,
realPath: this.#pathUtils.normalToAbsolute(canonicalPath),
type,
};
}
getAllFiles() {
return Array.from(
this.metadataIterator({
includeSymlinks: false,
includeNodeModules: true,
}),
({ canonicalPath }) => this.#pathUtils.normalToAbsolute(canonicalPath)
);
}
linkStats(mixedPath) {
const fileMetadata = this._getFileData(mixedPath, {
followLeaf: false,
});
if (fileMetadata == null) {
return null;
}
const fileType = isRegularFile(fileMetadata) ? "f" : "l";
const modifiedTime = fileMetadata[_constants.default.MTIME];
return {
fileType,
modifiedTime,
};
}
*matchFiles({
filter = null,
filterCompareAbsolute = false,
filterComparePosix = false,
follow = false,
recursive = true,
rootDir = null,
}) {
const normalRoot = rootDir == null ? "" : this._normalizePath(rootDir);
const contextRootResult = this._lookupByNormalPath(normalRoot);
if (!contextRootResult.exists) {
return;
}
const {
ancestorOfRootIdx,
canonicalPath: rootRealPath,
node: contextRoot,
parentNode: contextRootParent,
} = contextRootResult;
if (!isDirectory(contextRoot)) {
return;
}
const contextRootAbsolutePath =
rootRealPath === ""
? this.#rootDir
: _path.default.join(this.#rootDir, rootRealPath);
const prefix = filterComparePosix ? "./" : "." + _path.default.sep;
const contextRootAbsolutePathForComparison =
filterComparePosix && _path.default.sep !== "/"
? contextRootAbsolutePath.replaceAll(_path.default.sep, "/")
: contextRootAbsolutePath;
for (const relativePathForComparison of this._pathIterator(
contextRoot,
contextRootParent,
ancestorOfRootIdx,
{
alwaysYieldPosix: filterComparePosix,
canonicalPathOfRoot: rootRealPath,
follow,
recursive,
subtreeOnly: rootDir != null,
}
)) {
if (
filter == null ||
filter.test(
filterCompareAbsolute === true
? _path.default.join(
contextRootAbsolutePathForComparison,
relativePathForComparison
)
: prefix + relativePathForComparison
)
) {
const relativePath =
filterComparePosix === true && _path.default.sep !== "/"
? relativePathForComparison.replaceAll("/", _path.default.sep)
: relativePathForComparison;
yield _path.default.join(contextRootAbsolutePath, relativePath);
}
}
}
addOrModify(mixedPath, metadata) {
const normalPath = this._normalizePath(mixedPath);
const parentDirNode = this._lookupByNormalPath(
_path.default.dirname(normalPath),
{
makeDirectories: true,
}
);
if (!parentDirNode.exists) {
throw new Error(
`TreeFS: Failed to make parent directory entry for ${mixedPath}`
);
}
const canonicalPath = this._normalizePath(
parentDirNode.canonicalPath +
_path.default.sep +
_path.default.basename(normalPath)
);
this.bulkAddOrModify(new Map([[canonicalPath, metadata]]));
}
bulkAddOrModify(addedOrModifiedFiles) {
let lastDir;
let directoryNode;
for (const [normalPath, metadata] of addedOrModifiedFiles) {
const lastSepIdx = normalPath.lastIndexOf(_path.default.sep);
const dirname = lastSepIdx === -1 ? "" : normalPath.slice(0, lastSepIdx);
const basename =
lastSepIdx === -1 ? normalPath : normalPath.slice(lastSepIdx + 1);
if (directoryNode == null || dirname !== lastDir) {
const lookup = this._lookupByNormalPath(dirname, {
followLeaf: false,
makeDirectories: true,
});
if (!lookup.exists) {
throw new Error(
`TreeFS: Unexpected error adding ${normalPath}.\nMissing: ` +
lookup.canonicalMissingPath
);
}
if (!isDirectory(lookup.node)) {
throw new Error(
`TreeFS: Could not add directory ${dirname}, adding ${normalPath}. ` +
`${dirname} already exists in the file map as a file.`
);
}
lastDir = dirname;
directoryNode = lookup.node;
}
directoryNode.set(basename, metadata);
}
}
remove(mixedPath) {
const normalPath = this._normalizePath(mixedPath);
const result = this._lookupByNormalPath(normalPath, {
followLeaf: false,
});
if (!result.exists) {
return null;
}
const { parentNode, canonicalPath, node } = result;
if (isDirectory(node) && node.size > 0) {
throw new Error(
`TreeFS: remove called on a non-empty directory: ${mixedPath}`
);
}
if (parentNode != null) {
parentNode.delete(_path.default.basename(canonicalPath));
if (parentNode.size === 0 && parentNode !== this.#rootNode) {
this.remove(_path.default.dirname(canonicalPath));
}
}
return isDirectory(node) ? null : node;
}
_lookupByNormalPath(
requestedNormalPath,
opts = {
followLeaf: true,
makeDirectories: false,
}
) {
let targetNormalPath = requestedNormalPath;
let seen;
let fromIdx = opts.start?.pathIdx ?? 0;
let parentNode = opts.start?.node ?? this.#rootNode;
let ancestorOfRootIdx = opts.start?.ancestorOfRootIdx ?? 0;
const collectAncestors = opts.collectAncestors;
let unseenPathFromIdx = 0;
while (targetNormalPath.length > fromIdx) {
const nextSepIdx = targetNormalPath.indexOf(_path.default.sep, fromIdx);
const isLastSegment = nextSepIdx === -1;
const segmentName = isLastSegment
? targetNormalPath.slice(fromIdx)
: targetNormalPath.slice(fromIdx, nextSepIdx);
const isUnseen = fromIdx >= unseenPathFromIdx;
fromIdx = !isLastSegment ? nextSepIdx + 1 : targetNormalPath.length;
if (segmentName === ".") {
continue;
}
let segmentNode = parentNode.get(segmentName);
if (segmentName === ".." && ancestorOfRootIdx != null) {
ancestorOfRootIdx++;
} else if (segmentNode != null) {
ancestorOfRootIdx = null;
}
if (segmentNode == null) {
if (opts.makeDirectories !== true && segmentName !== "..") {
return {
canonicalMissingPath: isLastSegment
? targetNormalPath
: targetNormalPath.slice(0, fromIdx - 1),
exists: false,
missingSegmentName: segmentName,
};
}
segmentNode = new Map();
if (opts.makeDirectories === true) {
parentNode.set(segmentName, segmentNode);
}
}
if (
(nextSepIdx === targetNormalPath.length - 1 &&
isDirectory(segmentNode)) ||
(isLastSegment &&
(isDirectory(segmentNode) ||
isRegularFile(segmentNode) ||
opts.followLeaf === false))
) {
return {
ancestorOfRootIdx,
canonicalPath: isLastSegment
? targetNormalPath
: targetNormalPath.slice(0, -1),
exists: true,
node: segmentNode,
parentNode,
};
}
if (isDirectory(segmentNode)) {
parentNode = segmentNode;
if (collectAncestors && isUnseen) {
const currentPath = isLastSegment
? targetNormalPath
: targetNormalPath.slice(0, fromIdx - 1);
collectAncestors.push({
ancestorOfRootIdx,
node: segmentNode,
normalPath: currentPath,
segmentName,
});
}
} else {
const currentPath = isLastSegment
? targetNormalPath
: targetNormalPath.slice(0, fromIdx - 1);
if (isRegularFile(segmentNode)) {
return {
canonicalMissingPath: currentPath,
exists: false,
missingSegmentName: segmentName,
};
}
const normalSymlinkTarget = this._resolveSymlinkTargetToNormalPath(
segmentNode,
currentPath
);
if (opts.collectLinkPaths) {
opts.collectLinkPaths.add(
this.#pathUtils.normalToAbsolute(currentPath)
);
}
const remainingTargetPath = isLastSegment
? ""
: targetNormalPath.slice(fromIdx);
const joinedResult = this.#pathUtils.joinNormalToRelative(
normalSymlinkTarget.normalPath,
remainingTargetPath
);
targetNormalPath = joinedResult.normalPath;
if (
collectAncestors &&
!isLastSegment &&
(normalSymlinkTarget.ancestorOfRootIdx === 0 ||
joinedResult.collapsedSegments > 0)
) {
let node = this.#rootNode;
let collapsedPath = "";
const reverseAncestors = [];
for (
let i = 0;
i <= joinedResult.collapsedSegments && isDirectory(node);
i++
) {
if (
i > 0 ||
normalSymlinkTarget.ancestorOfRootIdx === 0 ||
joinedResult.collapsedSegments > 0
) {
reverseAncestors.push({
ancestorOfRootIdx: i,
node,
normalPath: collapsedPath,
segmentName: this.#pathUtils.getBasenameOfNthAncestor(i),
});
}
node = node.get("..") ?? new Map();
collapsedPath =
collapsedPath === ""
? ".."
: collapsedPath + _path.default.sep + "..";
}
collectAncestors.push(...reverseAncestors.reverse());
}
unseenPathFromIdx = normalSymlinkTarget.startOfBasenameIdx;
if (seen == null) {
seen = new Set([requestedNormalPath]);
}
if (seen.has(targetNormalPath)) {
return {
canonicalMissingPath: targetNormalPath,
exists: false,
missingSegmentName: segmentName,
};
}
seen.add(targetNormalPath);
fromIdx = 0;
parentNode = this.#rootNode;
ancestorOfRootIdx = 0;
}
}
(0, _invariant.default)(
parentNode === this.#rootNode,
"Unexpectedly escaped traversal"
);
return {
ancestorOfRootIdx: 0,
canonicalPath: targetNormalPath,
exists: true,
node: this.#rootNode,
parentNode: null,
};
}
hierarchicalLookup(mixedStartPath, subpath, opts) {
const ancestorsOfInput = [];
const normalPath = this._normalizePath(mixedStartPath);
const invalidatedBy = opts.invalidatedBy;
const closestLookup = this._lookupByNormalPath(normalPath, {
collectAncestors: ancestorsOfInput,
collectLinkPaths: invalidatedBy,
});
if (closestLookup.exists && isDirectory(closestLookup.node)) {
const maybeAbsolutePathMatch = this.#checkCandidateHasSubpath(
closestLookup.canonicalPath,
subpath,
opts.subpathType,
invalidatedBy,
null
);
if (maybeAbsolutePathMatch != null) {
return {
absolutePath: maybeAbsolutePathMatch,
containerRelativePath: "",
};
}
} else {
if (
invalidatedBy &&
(!closestLookup.exists || !isDirectory(closestLookup.node))
) {
invalidatedBy.add(
this.#pathUtils.normalToAbsolute(
closestLookup.exists
? closestLookup.canonicalPath
: closestLookup.canonicalMissingPath
)
);
}
if (
opts.breakOnSegment != null &&
!closestLookup.exists &&
closestLookup.missingSegmentName === opts.breakOnSegment
) {
return null;
}
}
let commonRoot = this.#rootNode;
let commonRootDepth = 0;
if (closestLookup.exists && closestLookup.ancestorOfRootIdx != null) {
commonRootDepth = closestLookup.ancestorOfRootIdx;
(0, _invariant.default)(
isDirectory(closestLookup.node),
"ancestors of the root must be directories"
);
commonRoot = closestLookup.node;
} else {
for (const ancestor of ancestorsOfInput) {
if (ancestor.ancestorOfRootIdx == null) {
break;
}
commonRootDepth = ancestor.ancestorOfRootIdx;
commonRoot = ancestor.node;
}
}
for (
let candidateIdx = ancestorsOfInput.length - 1;
candidateIdx >= commonRootDepth;
--candidateIdx
) {
const candidate = ancestorsOfInput[candidateIdx];
if (candidate.segmentName === opts.breakOnSegment) {
return null;
}
const maybeAbsolutePathMatch = this.#checkCandidateHasSubpath(
candidate.normalPath,
subpath,
opts.subpathType,
invalidatedBy,
{
ancestorOfRootIdx: candidate.ancestorOfRootIdx,
node: candidate.node,
pathIdx:
candidate.normalPath.length > 0
? candidate.normalPath.length + 1
: 0,
}
);
if (maybeAbsolutePathMatch != null) {
let prefixLength = commonRootDepth * 3;
for (let i = commonRootDepth; i <= candidateIdx; i++) {
prefixLength = normalPath.indexOf(
_path.default.sep,
prefixLength + 1
);
}
const containerRelativePath = normalPath.slice(prefixLength + 1);
return {
absolutePath: maybeAbsolutePathMatch,
containerRelativePath,
};
}
}
let candidateNormalPath =
commonRootDepth > 0 ? normalPath.slice(0, 3 * commonRootDepth - 1) : "";
const remainingNormalPath = normalPath.slice(commonRootDepth * 3);
let nextNode = commonRoot;
let depthBelowCommonRoot = 0;
while (isDirectory(nextNode)) {
const maybeAbsolutePathMatch = this.#checkCandidateHasSubpath(
candidateNormalPath,
subpath,
opts.subpathType,
invalidatedBy,
null
);
if (maybeAbsolutePathMatch != null) {
const rootDirParts = this.#pathUtils.getParts();
const relativeParts =
depthBelowCommonRoot > 0
? rootDirParts.slice(
-(depthBelowCommonRoot + commonRootDepth),
commonRootDepth > 0 ? -commonRootDepth : undefined
)
: [];
if (remainingNormalPath !== "") {
relativeParts.push(remainingNormalPath);
}
return {
absolutePath: maybeAbsolutePathMatch,
containerRelativePath: relativeParts.join(_path.default.sep),
};
}
depthBelowCommonRoot++;
candidateNormalPath =
candidateNormalPath === ""
? ".."
: candidateNormalPath + _path.default.sep + "..";
nextNode = nextNode.get("..");
}
return null;
}
#checkCandidateHasSubpath(
normalCandidatePath,
subpath,
subpathType,
invalidatedBy,
start
) {
const lookupResult = this._lookupByNormalPath(
this.#pathUtils.joinNormalToRelative(normalCandidatePath, subpath)
.normalPath,
{
collectLinkPaths: invalidatedBy,
}
);
if (
lookupResult.exists &&
isDirectory(lookupResult.node) === (subpathType === "d")
) {
return this.#pathUtils.normalToAbsolute(lookupResult.canonicalPath);
} else if (invalidatedBy) {
invalidatedBy.add(
this.#pathUtils.normalToAbsolute(
lookupResult.exists
? lookupResult.canonicalPath
: lookupResult.canonicalMissingPath
)
);
}
return null;
}
*metadataIterator(opts) {
yield* this._metadataIterator(this.#rootNode, opts);
}
*_metadataIterator(rootNode, opts, prefix = "") {
for (const [name, node] of rootNode) {
if (
!opts.includeNodeModules &&
isDirectory(node) &&
name === "node_modules"
) {
continue;
}
const prefixedName =
prefix === "" ? name : prefix + _path.default.sep + name;
if (isDirectory(node)) {
yield* this._metadataIterator(node, opts, prefixedName);
} else if (isRegularFile(node) || opts.includeSymlinks) {
yield {
canonicalPath: prefixedName,
metadata: node,
baseName: name,
};
}
}
}
_normalizePath(relativeOrAbsolutePath) {
return _path.default.isAbsolute(relativeOrAbsolutePath)
? this.#pathUtils.absoluteToNormal(relativeOrAbsolutePath)
: this.#pathUtils.relativeToNormal(relativeOrAbsolutePath);
}
*#directoryNodeIterator(node, parent, ancestorOfRootIdx) {
if (ancestorOfRootIdx != null && ancestorOfRootIdx > 0 && parent) {
yield [
this.#pathUtils.getBasenameOfNthAncestor(ancestorOfRootIdx - 1),
parent,
];
}
yield* node.entries();
}
*_pathIterator(
iterationRootNode,
iterationRootParentNode,
ancestorOfRootIdx,
opts,
pathPrefix = "",
followedLinks = new Set()
) {
const pathSep = opts.alwaysYieldPosix ? "/" : _path.default.sep;
const prefixWithSep = pathPrefix === "" ? pathPrefix : pathPrefix + pathSep;
for (const [name, node] of this.#directoryNodeIterator(
iterationRootNode,
iterationRootParentNode,
ancestorOfRootIdx
)) {
if (opts.subtreeOnly && name === "..") {
continue;
}
const nodePath = prefixWithSep + name;
if (!isDirectory(node)) {
if (isRegularFile(node)) {
yield nodePath;
} else {
const nodePathWithSystemSeparators =
pathSep === _path.default.sep
? nodePath
: nodePath.replaceAll(pathSep, _path.default.sep);
const normalPathOfSymlink = _path.default.join(
opts.canonicalPathOfRoot,
nodePathWithSystemSeparators
);
const resolved = this._lookupByNormalPath(normalPathOfSymlink, {
followLeaf: true,
});
if (!resolved.exists) {
continue;
}
const target = resolved.node;
if (!isDirectory(target)) {
yield nodePath;
} else if (
opts.recursive &&
opts.follow &&
!followedLinks.has(node)
) {
yield* this._pathIterator(
target,
resolved.parentNode,
resolved.ancestorOfRootIdx,
opts,
nodePath,
new Set([...followedLinks, node])
);
}
}
} else if (opts.recursive) {
yield* this._pathIterator(
node,
iterationRootParentNode,
ancestorOfRootIdx != null && ancestorOfRootIdx > 0
? ancestorOfRootIdx - 1
: null,
opts,
nodePath,
followedLinks
);
}
}
}
_resolveSymlinkTargetToNormalPath(symlinkNode, canonicalPathOfSymlink) {
const cachedResult = this.#cachedNormalSymlinkTargets.get(symlinkNode);
if (cachedResult != null) {
return cachedResult;
}
const literalSymlinkTarget = symlinkNode[_constants.default.SYMLINK];
(0, _invariant.default)(
typeof literalSymlinkTarget === "string",
"Expected symlink target to be populated."
);
const absoluteSymlinkTarget = _path.default.resolve(
this.#rootDir,
canonicalPathOfSymlink,
"..",
literalSymlinkTarget
);
const normalSymlinkTarget = _path.default.relative(
this.#rootDir,
absoluteSymlinkTarget
);
const result = {
ancestorOfRootIdx:
this.#pathUtils.getAncestorOfRootIdx(normalSymlinkTarget),
normalPath: normalSymlinkTarget,
startOfBasenameIdx:
normalSymlinkTarget.lastIndexOf(_path.default.sep) + 1,
};
this.#cachedNormalSymlinkTargets.set(symlinkNode, result);
return result;
}
_getFileData(
filePath,
opts = {
followLeaf: true,
}
) {
const normalPath = this._normalizePath(filePath);
const result = this._lookupByNormalPath(normalPath, {
followLeaf: opts.followLeaf,
});
if (!result.exists || isDirectory(result.node)) {
return null;
}
return result.node;
}
_cloneTree(root) {
const clone = new Map();
for (const [name, node] of root) {
if (isDirectory(node)) {
clone.set(name, this._cloneTree(node));
} else {
clone.set(name, [...node]);
}
}
return clone;
}
}
exports.default = TreeFS;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = checkWatchmanCapabilities;
var _child_process = require("child_process");
var _util = require("util");
async function checkWatchmanCapabilities(requiredCapabilities) {
const execFilePromise = (0, _util.promisify)(_child_process.execFile);
let rawResponse;
try {
const result = await execFilePromise("watchman", [
"list-capabilities",
"--output-encoding=json",
"--no-pretty",
"--no-spawn",
]);
rawResponse = result.stdout;
} catch (e) {
if (e?.code === "ENOENT") {
throw new Error("Watchman is not installed or not available on PATH");
}
throw e;
}
let parsedResponse;
try {
parsedResponse = JSON.parse(rawResponse);
} catch {
throw new Error(
"Failed to parse response from `watchman list-capabilities`"
);
}
if (
parsedResponse == null ||
typeof parsedResponse !== "object" ||
typeof parsedResponse.version !== "string" ||
!Array.isArray(parsedResponse.capabilities)
) {
throw new Error("Unexpected response from `watchman list-capabilities`");
}
const version = parsedResponse.version;
const capabilities = new Set(parsedResponse.capabilities);
const missingCapabilities = requiredCapabilities.filter(
(requiredCapability) => !capabilities.has(requiredCapability)
);
if (missingCapabilities.length > 0) {
throw new Error(
`The installed version of Watchman (${version}) is missing required capabilities: ${missingCapabilities.join(
", "
)}`
);
}
return {
version,
};
}

View File

@@ -0,0 +1,68 @@
/**
* 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
* @flow strict
*/
import {execFile} from 'child_process';
import {promisify} from 'util';
export default async function checkWatchmanCapabilities(
requiredCapabilities: $ReadOnlyArray<string>,
): Promise<{version: string}> {
const execFilePromise: (
cmd: string,
args: $ReadOnlyArray<string>,
) => Promise<{stdout: string}> = promisify(execFile);
let rawResponse;
try {
const result = await execFilePromise('watchman', [
'list-capabilities',
'--output-encoding=json',
'--no-pretty',
'--no-spawn', // The client can answer this, so don't spawn a server
]);
rawResponse = result.stdout;
} catch (e) {
if (e?.code === 'ENOENT') {
throw new Error('Watchman is not installed or not available on PATH');
}
throw e;
}
let parsedResponse;
try {
parsedResponse = (JSON.parse(rawResponse): mixed);
} catch {
throw new Error(
'Failed to parse response from `watchman list-capabilities`',
);
}
if (
parsedResponse == null ||
typeof parsedResponse !== 'object' ||
typeof parsedResponse.version !== 'string' ||
!Array.isArray(parsedResponse.capabilities)
) {
throw new Error('Unexpected response from `watchman list-capabilities`');
}
const version = parsedResponse.version;
const capabilities = new Set(parsedResponse.capabilities);
const missingCapabilities = requiredCapabilities.filter(
requiredCapability => !capabilities.has(requiredCapability),
);
if (missingCapabilities.length > 0) {
throw new Error(
`The installed version of Watchman (${version}) is missing required capabilities: ${missingCapabilities.join(
', ',
)}`,
);
}
return {version};
}

View File

@@ -0,0 +1,73 @@
"use strict";
const NOT_A_DOT = "(?<!\\.\\s*)";
const CAPTURE_STRING_LITERAL = (pos) => `([\`'"])([^'"\`]*?)(?:\\${pos})`;
const WORD_SEPARATOR = "\\b";
const LEFT_PARENTHESIS = "\\(";
const RIGHT_PARENTHESIS = "\\)";
const WHITESPACE = "\\s*";
const OPTIONAL_COMMA = "(:?,\\s*)?";
function createRegExp(parts, flags) {
return new RegExp(parts.join(""), flags);
}
function alternatives(...parts) {
return `(?:${parts.join("|")})`;
}
function functionCallStart(...names) {
return [
NOT_A_DOT,
WORD_SEPARATOR,
alternatives(...names),
WHITESPACE,
LEFT_PARENTHESIS,
WHITESPACE,
];
}
const BLOCK_COMMENT_RE = /\/\*[^]*?\*\//g;
const LINE_COMMENT_RE = /\/\/.*/g;
const REQUIRE_OR_DYNAMIC_IMPORT_RE = createRegExp(
[
...functionCallStart("require", "import"),
CAPTURE_STRING_LITERAL(1),
WHITESPACE,
OPTIONAL_COMMA,
RIGHT_PARENTHESIS,
],
"g"
);
const IMPORT_OR_EXPORT_RE = createRegExp(
[
"\\b(?:import|export)\\s+(?!type(?:of)?\\s+)(?:[^'\"]+\\s+from\\s+)?",
CAPTURE_STRING_LITERAL(1),
],
"g"
);
const JEST_EXTENSIONS_RE = createRegExp(
[
...functionCallStart(
"jest\\s*\\.\\s*(?:requireActual|requireMock|genMockFromModule|createMockFromModule)"
),
CAPTURE_STRING_LITERAL(1),
WHITESPACE,
OPTIONAL_COMMA,
RIGHT_PARENTHESIS,
],
"g"
);
function extract(code) {
const dependencies = new Set();
const addDependency = (match, _, dep) => {
dependencies.add(dep);
return match;
};
code
.replace(BLOCK_COMMENT_RE, "")
.replace(LINE_COMMENT_RE, "")
.replace(IMPORT_OR_EXPORT_RE, addDependency)
.replace(REQUIRE_OR_DYNAMIC_IMPORT_RE, addDependency)
.replace(JEST_EXTENSIONS_RE, addDependency);
return dependencies;
}
module.exports = {
extract,
};

View File

@@ -0,0 +1,101 @@
/**
* 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
* @flow strict
*/
'use strict';
const NOT_A_DOT = '(?<!\\.\\s*)';
const CAPTURE_STRING_LITERAL = (pos /*: number */) =>
`([\`'"])([^'"\`]*?)(?:\\${pos})`;
const WORD_SEPARATOR = '\\b';
const LEFT_PARENTHESIS = '\\(';
const RIGHT_PARENTHESIS = '\\)';
const WHITESPACE = '\\s*';
const OPTIONAL_COMMA = '(:?,\\s*)?';
function createRegExp(
parts /*: $ReadOnlyArray<string> */,
flags /*: string */,
) {
return new RegExp(parts.join(''), flags);
}
function alternatives(...parts /*: $ReadOnlyArray<string> */) {
return `(?:${parts.join('|')})`;
}
function functionCallStart(...names /*: $ReadOnlyArray<string> */) {
return [
NOT_A_DOT,
WORD_SEPARATOR,
alternatives(...names),
WHITESPACE,
LEFT_PARENTHESIS,
WHITESPACE,
];
}
const BLOCK_COMMENT_RE = /\/\*[^]*?\*\//g;
const LINE_COMMENT_RE = /\/\/.*/g;
const REQUIRE_OR_DYNAMIC_IMPORT_RE = createRegExp(
[
...functionCallStart('require', 'import'),
CAPTURE_STRING_LITERAL(1),
WHITESPACE,
OPTIONAL_COMMA,
RIGHT_PARENTHESIS,
],
'g',
);
const IMPORT_OR_EXPORT_RE = createRegExp(
[
'\\b(?:import|export)\\s+(?!type(?:of)?\\s+)(?:[^\'"]+\\s+from\\s+)?',
CAPTURE_STRING_LITERAL(1),
],
'g',
);
const JEST_EXTENSIONS_RE = createRegExp(
[
...functionCallStart(
'jest\\s*\\.\\s*(?:requireActual|requireMock|genMockFromModule|createMockFromModule)',
),
CAPTURE_STRING_LITERAL(1),
WHITESPACE,
OPTIONAL_COMMA,
RIGHT_PARENTHESIS,
],
'g',
);
function extract(code /*: string */) /*: $ReadOnlySet<string> */ {
const dependencies /*: Set<string> */ = new Set();
const addDependency = (
match /*: string */,
_ /*: string */,
dep /*: string */,
) => {
dependencies.add(dep);
return match;
};
code
.replace(BLOCK_COMMENT_RE, '')
.replace(LINE_COMMENT_RE, '')
.replace(IMPORT_OR_EXPORT_RE, addDependency)
.replace(REQUIRE_OR_DYNAMIC_IMPORT_RE, addDependency)
.replace(JEST_EXTENSIONS_RE, addDependency);
return dependencies;
}
module.exports = {extract};

View File

@@ -0,0 +1,95 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.relative = relative;
exports.resolve = resolve;
var path = _interopRequireWildcard(require("path"));
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function (nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interopRequireWildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) {
return { default: obj };
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {};
var hasPropertyDescriptor =
Object.defineProperty && Object.getOwnPropertyDescriptor;
for (var key in obj) {
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor
? Object.getOwnPropertyDescriptor(obj, key)
: null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
function relative(rootDir, filename) {
if (filename.indexOf(rootDir + path.sep) === 0) {
const relativePath = filename.substr(rootDir.length + 1);
for (let i = 0; ; i += UP_FRAGMENT_LENGTH) {
const nextIndirection = relativePath.indexOf(CURRENT_FRAGMENT, i);
if (nextIndirection === -1) {
return relativePath;
}
if (nextIndirection !== i + 1 || relativePath[i] !== ".") {
return path.relative(rootDir, filename);
}
}
}
return path.relative(rootDir, filename);
}
const UP_FRAGMENT = ".." + path.sep;
const UP_FRAGMENT_LENGTH = UP_FRAGMENT.length;
const CURRENT_FRAGMENT = "." + path.sep;
let cachedDirName = null;
let dirnameCache = [];
function resolve(rootDir, normalPath) {
let left = rootDir;
let i = 0;
let pos = 0;
while (
normalPath.startsWith(UP_FRAGMENT, pos) ||
(normalPath.endsWith("..") && normalPath.length === 2 + pos)
) {
if (i === 0 && cachedDirName !== rootDir) {
dirnameCache = [];
cachedDirName = rootDir;
}
if (dirnameCache.length === i) {
dirnameCache.push(path.dirname(left));
}
left = dirnameCache[i++];
pos += UP_FRAGMENT_LENGTH;
}
const right = pos === 0 ? normalPath : normalPath.slice(pos);
if (right.length === 0) {
return left;
}
if (left.endsWith(path.sep)) {
return left + right;
}
return left + path.sep + right;
}

View File

@@ -0,0 +1,78 @@
/**
* 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
* @flow strict
*/
import * as path from 'path';
// rootDir must be normalized and absolute, filename may be any absolute path.
// (but will optimally start with rootDir)
export function relative(rootDir: string, filename: string): string {
if (filename.indexOf(rootDir + path.sep) === 0) {
const relativePath = filename.substr(rootDir.length + 1);
// Allow any sequence of indirection fragments at the start of the path,
// e.g ../../foo, but bail out to Node's path.relative if we find a
// possible indirection after any other segment, or a leading "./".
for (let i = 0; ; i += UP_FRAGMENT_LENGTH) {
const nextIndirection = relativePath.indexOf(CURRENT_FRAGMENT, i);
if (nextIndirection === -1) {
return relativePath;
}
if (
nextIndirection !== i + 1 || // Fallback when ./ later in the path, or leading
relativePath[i] !== '.' // and for anything other than a leading ../
) {
return path.relative(rootDir, filename);
}
}
}
return path.relative(rootDir, filename);
}
const UP_FRAGMENT = '..' + path.sep;
const UP_FRAGMENT_LENGTH = UP_FRAGMENT.length;
const CURRENT_FRAGMENT = '.' + path.sep;
// Optimise for the case where we're often repeatedly dealing with the same
// root by caching just the most recent.
let cachedDirName = null;
let dirnameCache = [];
// rootDir must be an absolute path and normalPath must be a normal relative
// path (e.g.: foo/bar or ../foo/bar, but never ./foo or foo/../bar)
// As of Node 18 this is several times faster than path.resolve, over
// thousands of real calls with 1-3 levels of indirection.
export function resolve(rootDir: string, normalPath: string): string {
let left = rootDir;
let i = 0;
let pos = 0;
while (
normalPath.startsWith(UP_FRAGMENT, pos) ||
(normalPath.endsWith('..') && normalPath.length === 2 + pos)
) {
if (i === 0 && cachedDirName !== rootDir) {
dirnameCache = [];
cachedDirName = rootDir;
}
if (dirnameCache.length === i) {
dirnameCache.push(path.dirname(left));
}
left = dirnameCache[i++];
pos += UP_FRAGMENT_LENGTH;
}
const right = pos === 0 ? normalPath : normalPath.slice(pos);
if (right.length === 0) {
return left;
}
// left may already end in a path separator only if it is a filesystem root,
// '/' or 'X:\'.
if (left.endsWith(path.sep)) {
return left + right;
}
return left + path.sep + right;
}

View File

@@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = getPlatformExtension;
function getPlatformExtension(file, platforms) {
const last = file.lastIndexOf(".");
const secondToLast = file.lastIndexOf(".", last - 1);
if (secondToLast === -1) {
return null;
}
const platform = file.substring(secondToLast + 1, last);
return platforms.has(platform) ? platform : null;
}

View File

@@ -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
* @flow strict
*/
// Extract platform extension: index.ios.js -> ios
export default function getPlatformExtension(
file: string,
platforms: $ReadOnlySet<string>,
): ?string {
const last = file.lastIndexOf('.');
const secondToLast = file.lastIndexOf('.', last - 1);
if (secondToLast === -1) {
return null;
}
const platform = file.substring(secondToLast + 1, last);
return platforms.has(platform) ? platform : null;
}

View File

@@ -0,0 +1,55 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
var path = _interopRequireWildcard(require("path"));
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function (nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interopRequireWildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) {
return { default: obj };
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {};
var hasPropertyDescriptor =
Object.defineProperty && Object.getOwnPropertyDescriptor;
for (var key in obj) {
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor
? Object.getOwnPropertyDescriptor(obj, key)
: null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
let normalizePathSeparatorsToPosix;
if (path.sep === "/") {
normalizePathSeparatorsToPosix = (filePath) => filePath;
} else {
normalizePathSeparatorsToPosix = (filePath) => filePath.replace(/\\/g, "/");
}
var _default = normalizePathSeparatorsToPosix;
exports.default = _default;

View File

@@ -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
* @flow strict
*/
import * as path from 'path';
let normalizePathSeparatorsToPosix: (string: string) => string;
if (path.sep === '/') {
normalizePathSeparatorsToPosix = (filePath: string): string => filePath;
} else {
normalizePathSeparatorsToPosix = (filePath: string): string =>
filePath.replace(/\\/g, '/');
}
export default normalizePathSeparatorsToPosix;

View File

@@ -0,0 +1,56 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
var path = _interopRequireWildcard(require("path"));
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function (nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interopRequireWildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) {
return { default: obj };
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {};
var hasPropertyDescriptor =
Object.defineProperty && Object.getOwnPropertyDescriptor;
for (var key in obj) {
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor
? Object.getOwnPropertyDescriptor(obj, key)
: null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
let normalizePathSeparatorsToSystem;
if (path.sep === "/") {
normalizePathSeparatorsToSystem = (filePath) => filePath;
} else {
normalizePathSeparatorsToSystem = (filePath) =>
filePath.replace(/\//g, path.sep);
}
var _default = normalizePathSeparatorsToSystem;
exports.default = _default;

View File

@@ -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
* @flow strict
*/
import * as path from 'path';
let normalizePathSeparatorsToSystem: (string: string) => string;
if (path.sep === '/') {
normalizePathSeparatorsToSystem = (filePath: string): string => filePath;
} else {
normalizePathSeparatorsToSystem = (filePath: string): string =>
filePath.replace(/\//g, path.sep);
}
export default normalizePathSeparatorsToSystem;

View File

@@ -0,0 +1,75 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = rootRelativeCacheKeys;
var _normalizePathSeparatorsToPosix = _interopRequireDefault(
require("./normalizePathSeparatorsToPosix")
);
var _RootPathUtils = require("./RootPathUtils");
var _crypto = require("crypto");
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
function moduleCacheKey(modulePath) {
if (modulePath == null) {
return null;
}
const moduleExports = require(modulePath);
if (typeof moduleExports?.getCacheKey !== "function") {
console.warn(
`metro-file-map: Expected \`${modulePath}\` to export ` +
"`getCacheKey: () => string`"
);
return null;
}
return moduleExports.getCacheKey();
}
function rootRelativeCacheKeys(buildParameters) {
const { rootDir, ...otherParameters } = buildParameters;
const rootDirHash = (0, _crypto.createHash)("md5")
.update((0, _normalizePathSeparatorsToPosix.default)(rootDir))
.digest("hex");
const pathUtils = new _RootPathUtils.RootPathUtils(rootDir);
const cacheComponents = Object.keys(otherParameters)
.sort()
.map((key) => {
switch (key) {
case "roots":
return buildParameters[key].map((root) =>
(0, _normalizePathSeparatorsToPosix.default)(
pathUtils.absoluteToNormal(root)
)
);
case "cacheBreaker":
case "extensions":
case "computeDependencies":
case "computeSha1":
case "enableHastePackages":
case "enableSymlinks":
case "forceNodeFilesystemAPI":
case "platforms":
case "retainAllFiles":
case "skipPackageJson":
return buildParameters[key] ?? null;
case "mocksPattern":
return buildParameters[key]?.toString() ?? null;
case "ignorePattern":
return buildParameters[key].toString();
case "hasteImplModulePath":
case "dependencyExtractor":
return moduleCacheKey(buildParameters[key]);
default:
key;
throw new Error("Unrecognised key in build parameters: " + key);
}
});
const relativeConfigHash = (0, _crypto.createHash)("md5")
.update(JSON.stringify(cacheComponents))
.digest("hex");
return {
rootDirHash,
relativeConfigHash,
};
}

View File

@@ -0,0 +1,89 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import type {BuildParameters} from '../flow-types';
import normalizePathSeparatorsToPosix from './normalizePathSeparatorsToPosix';
import {RootPathUtils} from './RootPathUtils';
import {createHash} from 'crypto';
function moduleCacheKey(modulePath: ?string) {
if (modulePath == null) {
return null;
}
// $FlowFixMe[unsupported-syntax] - Dynamic require
const moduleExports = require(modulePath);
if (typeof moduleExports?.getCacheKey !== 'function') {
console.warn(
`metro-file-map: Expected \`${modulePath}\` to export ` +
'`getCacheKey: () => string`',
);
return null;
}
return moduleExports.getCacheKey();
}
export default function rootRelativeCacheKeys(
buildParameters: BuildParameters,
): {
rootDirHash: string,
relativeConfigHash: string,
} {
const {rootDir, ...otherParameters} = buildParameters;
const rootDirHash = createHash('md5')
.update(normalizePathSeparatorsToPosix(rootDir))
.digest('hex');
const pathUtils = new RootPathUtils(rootDir);
const cacheComponents = Object.keys(otherParameters)
.sort()
.map(key => {
switch (key) {
case 'roots':
return buildParameters[key].map(root =>
normalizePathSeparatorsToPosix(pathUtils.absoluteToNormal(root)),
);
case 'cacheBreaker':
case 'extensions':
case 'computeDependencies':
case 'computeSha1':
case 'enableHastePackages':
case 'enableSymlinks':
case 'forceNodeFilesystemAPI':
case 'platforms':
case 'retainAllFiles':
case 'skipPackageJson':
return buildParameters[key] ?? null;
case 'mocksPattern':
return buildParameters[key]?.toString() ?? null;
case 'ignorePattern':
return buildParameters[key].toString();
case 'hasteImplModulePath':
case 'dependencyExtractor':
return moduleCacheKey(buildParameters[key]);
default:
(key: empty);
throw new Error('Unrecognised key in build parameters: ' + key);
}
});
// JSON.stringify is stable here because we only deal in (nested) arrays of
// primitives. Use a different approach if this is expanded to include
// objects/Sets/Maps, etc.
const relativeConfigHash = createHash('md5')
.update(JSON.stringify(cacheComponents))
.digest('hex');
return {
rootDirHash,
relativeConfigHash,
};
}

View File

@@ -0,0 +1,184 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
var _common = require("./common");
var _anymatch = _interopRequireDefault(require("anymatch"));
var _events = _interopRequireDefault(require("events"));
var _fs = require("fs");
var path = _interopRequireWildcard(require("path"));
var _walker = _interopRequireDefault(require("walker"));
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function (nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interopRequireWildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) {
return { default: obj };
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {};
var hasPropertyDescriptor =
Object.defineProperty && Object.getOwnPropertyDescriptor;
for (var key in obj) {
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor
? Object.getOwnPropertyDescriptor(obj, key)
: null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const debug = require("debug")("Metro:FSEventsWatcher");
let fsevents = null;
try {
fsevents = require("fsevents");
} catch {}
const CHANGE_EVENT = "change";
const DELETE_EVENT = "delete";
const ADD_EVENT = "add";
const ALL_EVENT = "all";
class FSEventsWatcher extends _events.default {
static isSupported() {
return fsevents != null;
}
static _normalizeProxy(callback) {
return (filepath, stats) => callback(path.normalize(filepath), stats);
}
static _recReaddir(
dir,
dirCallback,
fileCallback,
symlinkCallback,
endCallback,
errorCallback,
ignored
) {
(0, _walker.default)(dir)
.filterDir(
(currentDir) => !ignored || !(0, _anymatch.default)(ignored, currentDir)
)
.on("dir", FSEventsWatcher._normalizeProxy(dirCallback))
.on("file", FSEventsWatcher._normalizeProxy(fileCallback))
.on("symlink", FSEventsWatcher._normalizeProxy(symlinkCallback))
.on("error", errorCallback)
.on("end", () => {
endCallback();
});
}
constructor(dir, opts) {
if (!fsevents) {
throw new Error(
"`fsevents` unavailable (this watcher can only be used on Darwin)"
);
}
super();
this.dot = opts.dot || false;
this.ignored = opts.ignored;
this.glob = Array.isArray(opts.glob) ? opts.glob : [opts.glob];
this.doIgnore = opts.ignored
? (0, _anymatch.default)(opts.ignored)
: () => false;
this.root = path.resolve(dir);
this.fsEventsWatchStopper = fsevents.watch(this.root, (path) => {
this._handleEvent(path).catch((error) => {
this.emit("error", error);
});
});
debug(`Watching ${this.root}`);
this._tracked = new Set();
const trackPath = (filePath) => {
this._tracked.add(filePath);
};
FSEventsWatcher._recReaddir(
this.root,
trackPath,
trackPath,
trackPath,
this.emit.bind(this, "ready"),
this.emit.bind(this, "error"),
this.ignored
);
}
async close(callback) {
await this.fsEventsWatchStopper();
this.removeAllListeners();
if (typeof callback === "function") {
process.nextTick(callback.bind(null, null, true));
}
}
async _handleEvent(filepath) {
const relativePath = path.relative(this.root, filepath);
try {
const stat = await _fs.promises.lstat(filepath);
const type = (0, _common.typeFromStat)(stat);
if (!type) {
return;
}
if (
!(0, _common.isIncluded)(
type,
this.glob,
this.dot,
this.doIgnore,
relativePath
)
) {
return;
}
const metadata = {
type,
modifiedTime: stat.mtime.getTime(),
size: stat.size,
};
if (this._tracked.has(filepath)) {
this._emit(CHANGE_EVENT, relativePath, metadata);
} else {
this._tracked.add(filepath);
this._emit(ADD_EVENT, relativePath, metadata);
}
} catch (error) {
if (error?.code !== "ENOENT") {
this.emit("error", error);
return;
}
if (!this._tracked.has(filepath)) {
return;
}
this._emit(DELETE_EVENT, relativePath);
this._tracked.delete(filepath);
}
}
_emit(type, file, metadata) {
this.emit(type, file, this.root, metadata);
this.emit(ALL_EVENT, type, file, this.root, metadata);
}
getPauseReason() {
return null;
}
}
exports.default = FSEventsWatcher;

View File

@@ -0,0 +1,216 @@
/**
* 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
* @flow strict-local
*/
import type {ChangeEventMetadata} from '../flow-types';
import type {Stats} from 'fs';
// $FlowFixMe[cannot-resolve-module] - Optional, Darwin only
import type {FSEvents} from 'fsevents';
import {isIncluded, typeFromStat} from './common';
// $FlowFixMe[untyped-import] - anymatch
import anymatch from 'anymatch';
import EventEmitter from 'events';
import {promises as fsPromises} from 'fs';
import * as path from 'path';
// $FlowFixMe[untyped-import] - walker
import walker from 'walker';
const debug = require('debug')('Metro:FSEventsWatcher');
type Matcher = typeof anymatch.Matcher;
let fsevents: ?FSEvents = null;
try {
// $FlowFixMe[cannot-resolve-module] - Optional, Darwin only
fsevents = require('fsevents');
} catch {
// Optional dependency, only supported on Darwin.
}
const CHANGE_EVENT = 'change';
const DELETE_EVENT = 'delete';
const ADD_EVENT = 'add';
const ALL_EVENT = 'all';
type FsEventsWatcherEvent =
| typeof CHANGE_EVENT
| typeof DELETE_EVENT
| typeof ADD_EVENT
| typeof ALL_EVENT;
/**
* Export `FSEventsWatcher` class.
* Watches `dir`.
*/
export default class FSEventsWatcher extends EventEmitter {
+root: string;
+ignored: ?Matcher;
+glob: $ReadOnlyArray<string>;
+dot: boolean;
+doIgnore: (path: string) => boolean;
+fsEventsWatchStopper: () => Promise<void>;
_tracked: Set<string>;
static isSupported(): boolean {
return fsevents != null;
}
static _normalizeProxy(
callback: (normalizedPath: string, stats: Stats) => void,
): (filepath: string, stats: Stats) => void {
return (filepath: string, stats: Stats): void =>
callback(path.normalize(filepath), stats);
}
static _recReaddir(
dir: string,
dirCallback: (normalizedPath: string, stats: Stats) => void,
fileCallback: (normalizedPath: string, stats: Stats) => void,
symlinkCallback: (normalizedPath: string, stats: Stats) => void,
// $FlowFixMe[unclear-type] Add types for callback
endCallback: Function,
// $FlowFixMe[unclear-type] Add types for callback
errorCallback: Function,
ignored?: Matcher,
) {
walker(dir)
.filterDir(
(currentDir: string) => !ignored || !anymatch(ignored, currentDir),
)
.on('dir', FSEventsWatcher._normalizeProxy(dirCallback))
.on('file', FSEventsWatcher._normalizeProxy(fileCallback))
.on('symlink', FSEventsWatcher._normalizeProxy(symlinkCallback))
.on('error', errorCallback)
.on('end', () => {
endCallback();
});
}
constructor(
dir: string,
opts: $ReadOnly<{
ignored?: Matcher,
glob: string | $ReadOnlyArray<string>,
dot: boolean,
...
}>,
) {
if (!fsevents) {
throw new Error(
'`fsevents` unavailable (this watcher can only be used on Darwin)',
);
}
super();
this.dot = opts.dot || false;
this.ignored = opts.ignored;
this.glob = Array.isArray(opts.glob) ? opts.glob : [opts.glob];
this.doIgnore = opts.ignored ? anymatch(opts.ignored) : () => false;
this.root = path.resolve(dir);
this.fsEventsWatchStopper = fsevents.watch(this.root, path => {
this._handleEvent(path).catch(error => {
this.emit('error', error);
});
});
debug(`Watching ${this.root}`);
this._tracked = new Set();
const trackPath = (filePath: string) => {
this._tracked.add(filePath);
};
FSEventsWatcher._recReaddir(
this.root,
trackPath,
trackPath,
trackPath,
// $FlowFixMe[method-unbinding] - Refactor
this.emit.bind(this, 'ready'),
// $FlowFixMe[method-unbinding] - Refactor
this.emit.bind(this, 'error'),
this.ignored,
);
}
/**
* End watching.
*/
async close(callback?: () => void): Promise<void> {
await this.fsEventsWatchStopper();
this.removeAllListeners();
if (typeof callback === 'function') {
// $FlowFixMe[extra-arg] - Is this a Node-style callback or as typed?
process.nextTick(callback.bind(null, null, true));
}
}
async _handleEvent(filepath: string) {
const relativePath = path.relative(this.root, filepath);
try {
const stat = await fsPromises.lstat(filepath);
const type = typeFromStat(stat);
// Ignore files of an unrecognized type
if (!type) {
return;
}
if (!isIncluded(type, this.glob, this.dot, this.doIgnore, relativePath)) {
return;
}
const metadata: ChangeEventMetadata = {
type,
modifiedTime: stat.mtime.getTime(),
size: stat.size,
};
if (this._tracked.has(filepath)) {
this._emit(CHANGE_EVENT, relativePath, metadata);
} else {
this._tracked.add(filepath);
this._emit(ADD_EVENT, relativePath, metadata);
}
} catch (error) {
if (error?.code !== 'ENOENT') {
this.emit('error', error);
return;
}
// Ignore files that aren't tracked and don't exist.
if (!this._tracked.has(filepath)) {
return;
}
this._emit(DELETE_EVENT, relativePath);
this._tracked.delete(filepath);
}
}
/**
* Emit events.
*/
_emit(
type: FsEventsWatcherEvent,
file: string,
metadata?: ChangeEventMetadata,
) {
this.emit(type, file, this.root, metadata);
this.emit(ALL_EVENT, type, file, this.root, metadata);
}
getPauseReason(): ?string {
return null;
}
}

View File

@@ -0,0 +1,276 @@
"use strict";
const common = require("./common");
const { EventEmitter } = require("events");
const fs = require("fs");
const platform = require("os").platform();
const path = require("path");
const fsPromises = fs.promises;
const CHANGE_EVENT = common.CHANGE_EVENT;
const DELETE_EVENT = common.DELETE_EVENT;
const ADD_EVENT = common.ADD_EVENT;
const ALL_EVENT = common.ALL_EVENT;
const DEBOUNCE_MS = 100;
module.exports = class NodeWatcher extends EventEmitter {
_changeTimers = new Map();
constructor(dir, opts) {
super();
common.assignOptions(this, opts);
this.watched = Object.create(null);
this._dirRegistry = Object.create(null);
this.root = path.resolve(dir);
this._watchdir(this.root);
common.recReaddir(
this.root,
(dir) => {
this._watchdir(dir);
},
(filename) => {
this._register(filename, "f");
},
(symlink) => {
this._register(symlink, "l");
},
() => {
this.emit("ready");
},
this._checkedEmitError,
this.ignored
);
}
_register(filepath, type) {
const dir = path.dirname(filepath);
const filename = path.basename(filepath);
if (this._dirRegistry[dir] && this._dirRegistry[dir][filename]) {
return false;
}
const relativePath = path.relative(this.root, filepath);
if (
type === "f" &&
!common.isIncluded("f", this.globs, this.dot, this.doIgnore, relativePath)
) {
return false;
}
if (!this._dirRegistry[dir]) {
this._dirRegistry[dir] = Object.create(null);
}
this._dirRegistry[dir][filename] = true;
return true;
}
_unregister(filepath) {
const dir = path.dirname(filepath);
if (this._dirRegistry[dir]) {
const filename = path.basename(filepath);
delete this._dirRegistry[dir][filename];
}
}
_unregisterDir(dirpath) {
if (this._dirRegistry[dirpath]) {
delete this._dirRegistry[dirpath];
}
}
_registered(fullpath) {
const dir = path.dirname(fullpath);
return !!(
this._dirRegistry[fullpath] ||
(this._dirRegistry[dir] &&
this._dirRegistry[dir][path.basename(fullpath)])
);
}
_checkedEmitError = (error) => {
if (!isIgnorableFileError(error)) {
this.emit("error", error);
}
};
_watchdir = (dir) => {
if (this.watched[dir]) {
return false;
}
const watcher = fs.watch(
dir,
{
persistent: true,
},
(event, filename) => this._normalizeChange(dir, event, filename)
);
this.watched[dir] = watcher;
watcher.on("error", this._checkedEmitError);
if (this.root !== dir) {
this._register(dir, "d");
}
return true;
};
_stopWatching(dir) {
if (this.watched[dir]) {
this.watched[dir].close();
delete this.watched[dir];
}
}
async close() {
Object.keys(this.watched).forEach((dir) => this._stopWatching(dir));
this.removeAllListeners();
}
_detectChangedFile(dir, event, callback) {
if (!this._dirRegistry[dir]) {
return;
}
let found = false;
let closest = null;
let c = 0;
Object.keys(this._dirRegistry[dir]).forEach((file, i, arr) => {
fs.lstat(path.join(dir, file), (error, stat) => {
if (found) {
return;
}
if (error) {
if (isIgnorableFileError(error)) {
found = true;
callback(file);
} else {
this.emit("error", error);
}
} else {
if (closest == null || stat.mtime > closest.mtime) {
closest = {
file,
mtime: stat.mtime,
};
}
if (arr.length === ++c) {
callback(closest.file);
}
}
});
});
}
_normalizeChange(dir, event, file) {
if (!file) {
this._detectChangedFile(dir, event, (actualFile) => {
if (actualFile) {
this._processChange(dir, event, actualFile).catch((error) =>
this.emit("error", error)
);
}
});
} else {
this._processChange(dir, event, path.normalize(file)).catch((error) =>
this.emit("error", error)
);
}
}
async _processChange(dir, event, file) {
const fullPath = path.join(dir, file);
const relativePath = path.join(path.relative(this.root, dir), file);
const registered = this._registered(fullPath);
try {
const stat = await fsPromises.lstat(fullPath);
if (stat.isDirectory()) {
if (event === "change") {
return;
}
if (
!common.isIncluded(
"d",
this.globs,
this.dot,
this.doIgnore,
relativePath
)
) {
return;
}
common.recReaddir(
path.resolve(this.root, relativePath),
(dir, stats) => {
if (this._watchdir(dir)) {
this._emitEvent(ADD_EVENT, path.relative(this.root, dir), {
modifiedTime: stats.mtime.getTime(),
size: stats.size,
type: "d",
});
}
},
(file, stats) => {
if (this._register(file, "f")) {
this._emitEvent(ADD_EVENT, path.relative(this.root, file), {
modifiedTime: stats.mtime.getTime(),
size: stats.size,
type: "f",
});
}
},
(symlink, stats) => {
if (this._register(symlink, "l")) {
this._rawEmitEvent(ADD_EVENT, path.relative(this.root, symlink), {
modifiedTime: stats.mtime.getTime(),
size: stats.size,
type: "l",
});
}
},
function endCallback() {},
this._checkedEmitError,
this.ignored
);
} else {
const type = common.typeFromStat(stat);
if (type == null) {
return;
}
const metadata = {
modifiedTime: stat.mtime.getTime(),
size: stat.size,
type,
};
if (registered) {
this._emitEvent(CHANGE_EVENT, relativePath, metadata);
} else {
if (this._register(fullPath, type)) {
this._emitEvent(ADD_EVENT, relativePath, metadata);
}
}
}
} catch (error) {
if (!isIgnorableFileError(error)) {
this.emit("error", error);
return;
}
this._unregister(fullPath);
this._stopWatching(fullPath);
this._unregisterDir(fullPath);
if (registered) {
this._emitEvent(DELETE_EVENT, relativePath);
}
}
}
_emitEvent(type, file, metadata) {
const key = type + "-" + file;
const addKey = ADD_EVENT + "-" + file;
if (type === CHANGE_EVENT && this._changeTimers.has(addKey)) {
return;
}
const existingTimer = this._changeTimers.get(key);
if (existingTimer) {
clearTimeout(existingTimer);
}
this._changeTimers.set(
key,
setTimeout(() => {
this._changeTimers.delete(key);
this._rawEmitEvent(type, file, metadata);
}, DEBOUNCE_MS)
);
}
_rawEmitEvent(eventType, file, metadata) {
this.emit(eventType, file, this.root, metadata);
this.emit(ALL_EVENT, eventType, file, this.root, metadata);
}
getPauseReason() {
return null;
}
};
function isIgnorableFileError(error) {
return (
error.code === "ENOENT" || (error.code === "EPERM" && platform === "win32")
);
}

View File

@@ -0,0 +1,414 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
/**
* Originally vendored from https://github.com/amasad/sane/blob/64ff3a870c42e84f744086884bf55a4f9c22d376/src/node_watcher.js
*/
'use strict';
import type {ChangeEventMetadata} from '../flow-types';
import type {WatcherOptions} from './common';
import type {FSWatcher, Stats} from 'fs';
const common = require('./common');
const {EventEmitter} = require('events');
const fs = require('fs');
const platform = require('os').platform();
const path = require('path');
const fsPromises = fs.promises;
const CHANGE_EVENT = common.CHANGE_EVENT;
const DELETE_EVENT = common.DELETE_EVENT;
const ADD_EVENT = common.ADD_EVENT;
const ALL_EVENT = common.ALL_EVENT;
/**
* This setting delays all events. It suppresses 'change' events that
* immediately follow an 'add', and debounces successive 'change' events to
* only emit the latest.
*/
const DEBOUNCE_MS = 100;
module.exports = class NodeWatcher extends EventEmitter {
_changeTimers: Map<string, TimeoutID> = new Map();
_dirRegistry: {
[directory: string]: {[file: string]: true, __proto__: null},
__proto__: null,
};
doIgnore: string => boolean;
dot: boolean;
globs: $ReadOnlyArray<string>;
ignored: ?(boolean | RegExp);
root: string;
watched: {[key: string]: FSWatcher, __proto__: null};
watchmanDeferStates: $ReadOnlyArray<string>;
constructor(dir: string, opts: WatcherOptions) {
super();
common.assignOptions(this, opts);
this.watched = Object.create(null);
this._dirRegistry = Object.create(null);
this.root = path.resolve(dir);
this._watchdir(this.root);
common.recReaddir(
this.root,
dir => {
this._watchdir(dir);
},
filename => {
this._register(filename, 'f');
},
symlink => {
this._register(symlink, 'l');
},
() => {
this.emit('ready');
},
this._checkedEmitError,
this.ignored,
);
}
/**
* Register files that matches our globs to know what to type of event to
* emit in the future.
*
* Registry looks like the following:
*
* dirRegister => Map {
* dirpath => Map {
* filename => true
* }
* }
*
* Return false if ignored or already registered.
*/
_register(filepath: string, type: ChangeEventMetadata['type']): boolean {
const dir = path.dirname(filepath);
const filename = path.basename(filepath);
if (this._dirRegistry[dir] && this._dirRegistry[dir][filename]) {
return false;
}
const relativePath = path.relative(this.root, filepath);
if (
type === 'f' &&
!common.isIncluded('f', this.globs, this.dot, this.doIgnore, relativePath)
) {
return false;
}
if (!this._dirRegistry[dir]) {
this._dirRegistry[dir] = Object.create(null);
}
this._dirRegistry[dir][filename] = true;
return true;
}
/**
* Removes a file from the registry.
*/
_unregister(filepath: string) {
const dir = path.dirname(filepath);
if (this._dirRegistry[dir]) {
const filename = path.basename(filepath);
delete this._dirRegistry[dir][filename];
}
}
/**
* Removes a dir from the registry.
*/
_unregisterDir(dirpath: string): void {
if (this._dirRegistry[dirpath]) {
delete this._dirRegistry[dirpath];
}
}
/**
* Checks if a file or directory exists in the registry.
*/
_registered(fullpath: string): boolean {
const dir = path.dirname(fullpath);
return !!(
this._dirRegistry[fullpath] ||
(this._dirRegistry[dir] &&
this._dirRegistry[dir][path.basename(fullpath)])
);
}
/**
* Emit "error" event if it's not an ignorable event
*/
_checkedEmitError: (error: Error) => void = error => {
if (!isIgnorableFileError(error)) {
this.emit('error', error);
}
};
/**
* Watch a directory.
*/
_watchdir: string => boolean = (dir: string) => {
if (this.watched[dir]) {
return false;
}
const watcher = fs.watch(dir, {persistent: true}, (event, filename) =>
this._normalizeChange(dir, event, filename),
);
this.watched[dir] = watcher;
watcher.on('error', this._checkedEmitError);
if (this.root !== dir) {
this._register(dir, 'd');
}
return true;
};
/**
* Stop watching a directory.
*/
_stopWatching(dir: string) {
if (this.watched[dir]) {
this.watched[dir].close();
delete this.watched[dir];
}
}
/**
* End watching.
*/
async close(): Promise<void> {
Object.keys(this.watched).forEach(dir => this._stopWatching(dir));
this.removeAllListeners();
}
/**
* On some platforms, as pointed out on the fs docs (most likely just win32)
* the file argument might be missing from the fs event. Try to detect what
* change by detecting if something was deleted or the most recent file change.
*/
_detectChangedFile(
dir: string,
event: string,
callback: (file: string) => void,
) {
if (!this._dirRegistry[dir]) {
return;
}
let found = false;
let closest: ?$ReadOnly<{file: string, mtime: Stats['mtime']}> = null;
let c = 0;
Object.keys(this._dirRegistry[dir]).forEach((file, i, arr) => {
fs.lstat(path.join(dir, file), (error, stat) => {
if (found) {
return;
}
if (error) {
if (isIgnorableFileError(error)) {
found = true;
callback(file);
} else {
this.emit('error', error);
}
} else {
if (closest == null || stat.mtime > closest.mtime) {
closest = {file, mtime: stat.mtime};
}
if (arr.length === ++c) {
callback(closest.file);
}
}
});
});
}
/**
* Normalize fs events and pass it on to be processed.
*/
_normalizeChange(dir: string, event: string, file: string) {
if (!file) {
this._detectChangedFile(dir, event, actualFile => {
if (actualFile) {
this._processChange(dir, event, actualFile).catch(error =>
this.emit('error', error),
);
}
});
} else {
this._processChange(dir, event, path.normalize(file)).catch(error =>
this.emit('error', error),
);
}
}
/**
* Process changes.
*/
async _processChange(dir: string, event: string, file: string) {
const fullPath = path.join(dir, file);
const relativePath = path.join(path.relative(this.root, dir), file);
const registered = this._registered(fullPath);
try {
const stat = await fsPromises.lstat(fullPath);
if (stat.isDirectory()) {
// win32 emits usless change events on dirs.
if (event === 'change') {
return;
}
if (
!common.isIncluded(
'd',
this.globs,
this.dot,
this.doIgnore,
relativePath,
)
) {
return;
}
common.recReaddir(
path.resolve(this.root, relativePath),
(dir, stats) => {
if (this._watchdir(dir)) {
this._emitEvent(ADD_EVENT, path.relative(this.root, dir), {
modifiedTime: stats.mtime.getTime(),
size: stats.size,
type: 'd',
});
}
},
(file, stats) => {
if (this._register(file, 'f')) {
this._emitEvent(ADD_EVENT, path.relative(this.root, file), {
modifiedTime: stats.mtime.getTime(),
size: stats.size,
type: 'f',
});
}
},
(symlink, stats) => {
if (this._register(symlink, 'l')) {
this._rawEmitEvent(ADD_EVENT, path.relative(this.root, symlink), {
modifiedTime: stats.mtime.getTime(),
size: stats.size,
type: 'l',
});
}
},
function endCallback() {},
this._checkedEmitError,
this.ignored,
);
} else {
const type = common.typeFromStat(stat);
if (type == null) {
return;
}
const metadata = {
modifiedTime: stat.mtime.getTime(),
size: stat.size,
type,
};
if (registered) {
this._emitEvent(CHANGE_EVENT, relativePath, metadata);
} else {
if (this._register(fullPath, type)) {
this._emitEvent(ADD_EVENT, relativePath, metadata);
}
}
}
} catch (error) {
if (!isIgnorableFileError(error)) {
this.emit('error', error);
return;
}
this._unregister(fullPath);
this._stopWatching(fullPath);
this._unregisterDir(fullPath);
if (registered) {
this._emitEvent(DELETE_EVENT, relativePath);
}
}
}
/**
* Emits the given event after debouncing, to 1) suppress 'change' events
* immediately following an 'add', and 2) to only emit the latest 'change'
* event when received in quick succession for a given file.
*
* See also note above for DEBOUNCE_MS.
*/
_emitEvent(type: string, file: string, metadata?: ChangeEventMetadata) {
const key = type + '-' + file;
const addKey = ADD_EVENT + '-' + file;
if (type === CHANGE_EVENT && this._changeTimers.has(addKey)) {
// Ignore the change event that is immediately fired after an add event.
// (This happens on Linux).
return;
}
const existingTimer = this._changeTimers.get(key);
if (existingTimer) {
clearTimeout(existingTimer);
}
this._changeTimers.set(
key,
setTimeout(() => {
this._changeTimers.delete(key);
this._rawEmitEvent(type, file, metadata);
}, DEBOUNCE_MS),
);
}
/**
* Actually emit the events
*/
_rawEmitEvent(
eventType: string,
file: string,
metadata: ?ChangeEventMetadata,
) {
this.emit(eventType, file, this.root, metadata);
this.emit(ALL_EVENT, eventType, file, this.root, metadata);
}
getPauseReason(): ?string {
return null;
}
};
/**
* Determine if a given FS error can be ignored
*/
function isIgnorableFileError(error: Error | {code: string}) {
return (
error.code === 'ENOENT' ||
// Workaround Windows EPERM on watched folder deletion, and when
// reading locked files (pending further writes or pending deletion).
// In such cases, we'll receive a subsequent event when the file is
// deleted or ready to read.
// https://github.com/facebook/metro/issues/1001
// https://github.com/nodejs/node-v0.x-archive/issues/4337
(error.code === 'EPERM' && platform === 'win32')
);
}

View File

@@ -0,0 +1,48 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
class RecrawlWarning {
static RECRAWL_WARNINGS = [];
static REGEXP =
/Recrawled this watch (\d+) times?, most recently because:\n([^:]+)/;
constructor(root, count) {
this.root = root;
this.count = count;
}
static findByRoot(root) {
for (let i = 0; i < this.RECRAWL_WARNINGS.length; i++) {
const warning = this.RECRAWL_WARNINGS[i];
if (warning.root === root) {
return warning;
}
}
return undefined;
}
static isRecrawlWarningDupe(warningMessage) {
if (typeof warningMessage !== "string") {
return false;
}
const match = warningMessage.match(this.REGEXP);
if (!match) {
return false;
}
const count = Number(match[1]);
const root = match[2];
const warning = this.findByRoot(root);
if (warning) {
if (warning.count >= count) {
return true;
} else {
warning.count = count;
return false;
}
} else {
this.RECRAWL_WARNINGS.push(new RecrawlWarning(root, count));
return false;
}
}
}
exports.default = RecrawlWarning;

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/
/**
* Originally vendored from
* https://github.com/amasad/sane/blob/64ff3a870c42e84f744086884bf55a4f9c22d376/src/utils/recrawl-warning-dedupe.js
*/
'use strict';
export default class RecrawlWarning {
static RECRAWL_WARNINGS: Array<RecrawlWarning> = [];
static REGEXP: RegExp =
/Recrawled this watch (\d+) times?, most recently because:\n([^:]+)/;
root: string;
count: number;
constructor(root: string, count: number) {
this.root = root;
this.count = count;
}
static findByRoot(root: string): ?RecrawlWarning {
for (let i = 0; i < this.RECRAWL_WARNINGS.length; i++) {
const warning = this.RECRAWL_WARNINGS[i];
if (warning.root === root) {
return warning;
}
}
return undefined;
}
static isRecrawlWarningDupe(warningMessage: mixed): boolean {
if (typeof warningMessage !== 'string') {
return false;
}
const match = warningMessage.match(this.REGEXP);
if (!match) {
return false;
}
const count = Number(match[1]);
const root = match[2];
const warning = this.findByRoot(root);
if (warning) {
// only keep the highest count, assume count to either stay the same or
// increase.
if (warning.count >= count) {
return true;
} else {
// update the existing warning to the latest (highest) count
warning.count = count;
return false;
}
} else {
this.RECRAWL_WARNINGS.push(new RecrawlWarning(root, count));
return false;
}
}
}

View File

@@ -0,0 +1,304 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
var common = _interopRequireWildcard(require("./common"));
var _RecrawlWarning = _interopRequireDefault(require("./RecrawlWarning"));
var _assert = _interopRequireDefault(require("assert"));
var _crypto = require("crypto");
var _events = _interopRequireDefault(require("events"));
var _fbWatchman = _interopRequireDefault(require("fb-watchman"));
var _invariant = _interopRequireDefault(require("invariant"));
var _path = _interopRequireDefault(require("path"));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function (nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interopRequireWildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) {
return { default: obj };
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {};
var hasPropertyDescriptor =
Object.defineProperty && Object.getOwnPropertyDescriptor;
for (var key in obj) {
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor
? Object.getOwnPropertyDescriptor(obj, key)
: null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
const debug = require("debug")("Metro:WatchmanWatcher");
const CHANGE_EVENT = common.CHANGE_EVENT;
const DELETE_EVENT = common.DELETE_EVENT;
const ADD_EVENT = common.ADD_EVENT;
const ALL_EVENT = common.ALL_EVENT;
const SUB_PREFIX = "metro-file-map";
class WatchmanWatcher extends _events.default {
#deferringStates = null;
constructor(dir, opts) {
super();
common.assignOptions(this, opts);
this.root = _path.default.resolve(dir);
const watchKey = (0, _crypto.createHash)("md5")
.update(this.root)
.digest("hex");
const readablePath = this.root
.replace(/[\/\\]/g, "-")
.replace(/[^\-\w]/g, "");
this.subscriptionName = `${SUB_PREFIX}-${process.pid}-${readablePath}-${watchKey}`;
this._init();
}
_init() {
if (this.client) {
this.client.removeAllListeners();
}
const self = this;
this.client = new _fbWatchman.default.Client();
this.client.on("error", (error) => {
self.emit("error", error);
});
this.client.on("subscription", (changeEvent) =>
this._handleChangeEvent(changeEvent)
);
this.client.on("end", () => {
console.warn(
"[metro-file-map] Warning: Lost connection to Watchman, reconnecting.."
);
self._init();
});
this.watchProjectInfo = null;
function getWatchRoot() {
return self.watchProjectInfo ? self.watchProjectInfo.root : self.root;
}
function onWatchProject(error, resp) {
if (handleError(self, error)) {
return;
}
debug("Received watch-project response: %s", resp.relative_path);
handleWarning(resp);
self.watchProjectInfo = {
relativePath: resp.relative_path ? resp.relative_path : "",
root: resp.watch,
};
self.client.command(["clock", getWatchRoot()], onClock);
}
function onClock(error, resp) {
if (handleError(self, error)) {
return;
}
debug("Received clock response: %s", resp.clock);
const watchProjectInfo = self.watchProjectInfo;
(0, _invariant.default)(
watchProjectInfo != null,
"watch-project response should have been set before clock response"
);
handleWarning(resp);
const options = {
fields: ["name", "exists", "new", "type", "size", "mtime_ms"],
since: resp.clock,
defer: self.watchmanDeferStates,
relative_root: watchProjectInfo.relativePath,
};
if (self.globs.length === 0 && !self.dot) {
options.expression = [
"match",
"**",
"wholename",
{
includedotfiles: false,
},
];
}
self.client.command(
["subscribe", getWatchRoot(), self.subscriptionName, options],
onSubscribe
);
}
const onSubscribe = (error, resp) => {
if (handleError(self, error)) {
return;
}
debug("Received subscribe response: %s", resp.subscribe);
handleWarning(resp);
if (resp["asserted-states"] != null) {
this.#deferringStates = new Set(resp["asserted-states"]);
}
self.emit("ready");
};
self.client.command(["watch-project", getWatchRoot()], onWatchProject);
}
_handleChangeEvent(resp) {
debug(
"Received subscription response: %s (fresh: %s, files: %s, enter: %s, leave: %s)",
resp.subscription,
resp.is_fresh_instance,
resp.files?.length,
resp["state-enter"],
resp["state-leave"]
);
_assert.default.equal(
resp.subscription,
this.subscriptionName,
"Invalid subscription event."
);
if (resp.is_fresh_instance) {
this.emit("fresh_instance");
}
if (resp.is_fresh_instance) {
this.emit("fresh_instance");
}
if (Array.isArray(resp.files)) {
resp.files.forEach((change) => this._handleFileChange(change));
}
const { "state-enter": stateEnter, "state-leave": stateLeave } = resp;
if (
stateEnter != null &&
(this.watchmanDeferStates ?? []).includes(stateEnter)
) {
this.#deferringStates?.add(stateEnter);
debug(
'Watchman reports "%s" just started. Filesystem notifications are paused.',
stateEnter
);
}
if (
stateLeave != null &&
(this.watchmanDeferStates ?? []).includes(stateLeave)
) {
this.#deferringStates?.delete(stateLeave);
debug(
'Watchman reports "%s" ended. Filesystem notifications resumed.',
stateLeave
);
}
}
_handleFileChange(changeDescriptor) {
const self = this;
const watchProjectInfo = self.watchProjectInfo;
(0, _invariant.default)(
watchProjectInfo != null,
"watch-project response should have been set before receiving subscription events"
);
const {
name: relativePath,
new: isNew = false,
exists = false,
type,
mtime_ms,
size,
} = changeDescriptor;
debug(
"Handling change to: %s (new: %s, exists: %s, type: %s)",
relativePath,
isNew,
exists,
type
);
if (type != null && !(type === "f" || type === "d" || type === "l")) {
return;
}
if (
!common.isIncluded(
type,
this.globs,
this.dot,
this.doIgnore,
relativePath
)
) {
return;
}
if (!exists) {
self._emitEvent(DELETE_EVENT, relativePath, self.root);
} else {
const eventType = isNew ? ADD_EVENT : CHANGE_EVENT;
(0, _invariant.default)(
type != null && mtime_ms != null && size != null,
'Watchman file change event for "%s" missing some requested metadata. ' +
"Got type: %s, mtime_ms: %s, size: %s",
relativePath,
type,
mtime_ms,
size
);
if (!(type === "d" && eventType === CHANGE_EVENT)) {
const mtime = Number(mtime_ms);
self._emitEvent(eventType, relativePath, self.root, {
modifiedTime: mtime !== 0 ? mtime : null,
size,
type,
});
}
}
}
_emitEvent(eventType, filepath, root, changeMetadata) {
this.emit(eventType, filepath, root, changeMetadata);
this.emit(ALL_EVENT, eventType, filepath, root, changeMetadata);
}
async close() {
this.client.removeAllListeners();
this.client.end();
this.#deferringStates = null;
}
getPauseReason() {
if (this.#deferringStates == null || this.#deferringStates.size === 0) {
return null;
}
const states = [...this.#deferringStates];
if (states.length === 1) {
return `The watch is in the '${states[0]}' state.`;
}
return `The watch is in the ${states
.slice(0, -1)
.map((s) => `'${s}'`)
.join(", ")} and '${states[states.length - 1]}' states.`;
}
}
exports.default = WatchmanWatcher;
function handleError(emitter, error) {
if (error != null) {
emitter.emit("error", error);
return true;
} else {
return false;
}
}
function handleWarning(resp) {
if ("warning" in resp) {
if (_RecrawlWarning.default.isRecrawlWarningDupe(resp.warning)) {
return true;
}
console.warn(resp.warning);
return true;
} else {
return false;
}
}

View File

@@ -0,0 +1,364 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import type {ChangeEventMetadata} from '../flow-types';
import type {WatcherOptions} from './common';
import type {
Client,
WatchmanClockResponse,
WatchmanFileChange,
WatchmanQuery,
WatchmanSubscribeResponse,
WatchmanSubscriptionEvent,
WatchmanWatchResponse,
} from 'fb-watchman';
import * as common from './common';
import RecrawlWarning from './RecrawlWarning';
import assert from 'assert';
import {createHash} from 'crypto';
import EventEmitter from 'events';
import watchman from 'fb-watchman';
import invariant from 'invariant';
import path from 'path';
const debug = require('debug')('Metro:WatchmanWatcher');
const CHANGE_EVENT = common.CHANGE_EVENT;
const DELETE_EVENT = common.DELETE_EVENT;
const ADD_EVENT = common.ADD_EVENT;
const ALL_EVENT = common.ALL_EVENT;
const SUB_PREFIX = 'metro-file-map';
/**
* Watches `dir`.
*/
export default class WatchmanWatcher extends EventEmitter {
client: Client;
dot: boolean;
doIgnore: string => boolean;
globs: $ReadOnlyArray<string>;
root: string;
subscriptionName: string;
watchProjectInfo: ?$ReadOnly<{
relativePath: string,
root: string,
}>;
watchmanDeferStates: $ReadOnlyArray<string>;
#deferringStates: ?Set<string> = null;
constructor(dir: string, opts: WatcherOptions) {
super();
common.assignOptions(this, opts);
this.root = path.resolve(dir);
// Use a unique subscription name per process per watched directory
const watchKey = createHash('md5').update(this.root).digest('hex');
const readablePath = this.root
.replace(/[\/\\]/g, '-') // \ and / to -
.replace(/[^\-\w]/g, ''); // Remove non-word/hyphen
this.subscriptionName = `${SUB_PREFIX}-${process.pid}-${readablePath}-${watchKey}`;
this._init();
}
/**
* Run the watchman `watch` command on the root and subscribe to changes.
*/
_init() {
if (this.client) {
this.client.removeAllListeners();
}
const self = this;
this.client = new watchman.Client();
this.client.on('error', error => {
self.emit('error', error);
});
this.client.on('subscription', changeEvent =>
this._handleChangeEvent(changeEvent),
);
this.client.on('end', () => {
console.warn(
'[metro-file-map] Warning: Lost connection to Watchman, reconnecting..',
);
self._init();
});
this.watchProjectInfo = null;
function getWatchRoot() {
return self.watchProjectInfo ? self.watchProjectInfo.root : self.root;
}
function onWatchProject(error: ?Error, resp: WatchmanWatchResponse) {
if (handleError(self, error)) {
return;
}
debug('Received watch-project response: %s', resp.relative_path);
handleWarning(resp);
self.watchProjectInfo = {
relativePath: resp.relative_path ? resp.relative_path : '',
root: resp.watch,
};
self.client.command(['clock', getWatchRoot()], onClock);
}
function onClock(error: ?Error, resp: WatchmanClockResponse) {
if (handleError(self, error)) {
return;
}
debug('Received clock response: %s', resp.clock);
const watchProjectInfo = self.watchProjectInfo;
invariant(
watchProjectInfo != null,
'watch-project response should have been set before clock response',
);
handleWarning(resp);
const options: WatchmanQuery = {
fields: ['name', 'exists', 'new', 'type', 'size', 'mtime_ms'],
since: resp.clock,
defer: self.watchmanDeferStates,
relative_root: watchProjectInfo.relativePath,
};
// Make sure we honor the dot option if even we're not using globs.
if (self.globs.length === 0 && !self.dot) {
options.expression = [
'match',
'**',
'wholename',
{
includedotfiles: false,
},
];
}
self.client.command(
['subscribe', getWatchRoot(), self.subscriptionName, options],
onSubscribe,
);
}
const onSubscribe = (error: ?Error, resp: WatchmanSubscribeResponse) => {
if (handleError(self, error)) {
return;
}
debug('Received subscribe response: %s', resp.subscribe);
handleWarning(resp);
if (resp['asserted-states'] != null) {
this.#deferringStates = new Set(resp['asserted-states']);
}
self.emit('ready');
};
self.client.command(['watch-project', getWatchRoot()], onWatchProject);
}
/**
* Handles a change event coming from the subscription.
*/
_handleChangeEvent(resp: WatchmanSubscriptionEvent) {
debug(
'Received subscription response: %s (fresh: %s, files: %s, enter: %s, leave: %s)',
resp.subscription,
resp.is_fresh_instance,
resp.files?.length,
resp['state-enter'],
resp['state-leave'],
);
assert.equal(
resp.subscription,
this.subscriptionName,
'Invalid subscription event.',
);
if (resp.is_fresh_instance) {
this.emit('fresh_instance');
}
if (resp.is_fresh_instance) {
this.emit('fresh_instance');
}
if (Array.isArray(resp.files)) {
resp.files.forEach(change => this._handleFileChange(change));
}
const {'state-enter': stateEnter, 'state-leave': stateLeave} = resp;
if (
stateEnter != null &&
(this.watchmanDeferStates ?? []).includes(stateEnter)
) {
this.#deferringStates?.add(stateEnter);
debug(
'Watchman reports "%s" just started. Filesystem notifications are paused.',
stateEnter,
);
}
if (
stateLeave != null &&
(this.watchmanDeferStates ?? []).includes(stateLeave)
) {
this.#deferringStates?.delete(stateLeave);
debug(
'Watchman reports "%s" ended. Filesystem notifications resumed.',
stateLeave,
);
}
}
/**
* Handles a single change event record.
*/
_handleFileChange(changeDescriptor: WatchmanFileChange) {
const self = this;
const watchProjectInfo = self.watchProjectInfo;
invariant(
watchProjectInfo != null,
'watch-project response should have been set before receiving subscription events',
);
const {
name: relativePath,
new: isNew = false,
exists = false,
type,
mtime_ms,
size,
} = changeDescriptor;
debug(
'Handling change to: %s (new: %s, exists: %s, type: %s)',
relativePath,
isNew,
exists,
type,
);
// Ignore files of an unrecognized type
if (type != null && !(type === 'f' || type === 'd' || type === 'l')) {
return;
}
if (
!common.isIncluded(
type,
this.globs,
this.dot,
this.doIgnore,
relativePath,
)
) {
return;
}
if (!exists) {
self._emitEvent(DELETE_EVENT, relativePath, self.root);
} else {
const eventType = isNew ? ADD_EVENT : CHANGE_EVENT;
invariant(
type != null && mtime_ms != null && size != null,
'Watchman file change event for "%s" missing some requested metadata. ' +
'Got type: %s, mtime_ms: %s, size: %s',
relativePath,
type,
mtime_ms,
size,
);
if (
// Change event on dirs are mostly useless.
!(type === 'd' && eventType === CHANGE_EVENT)
) {
const mtime = Number(mtime_ms);
self._emitEvent(eventType, relativePath, self.root, {
modifiedTime: mtime !== 0 ? mtime : null,
size,
type,
});
}
}
}
/**
* Dispatches the event.
*/
_emitEvent(
eventType: string,
filepath: string,
root: string,
changeMetadata?: ChangeEventMetadata,
) {
this.emit(eventType, filepath, root, changeMetadata);
this.emit(ALL_EVENT, eventType, filepath, root, changeMetadata);
}
/**
* Closes the watcher.
*/
async close() {
this.client.removeAllListeners();
this.client.end();
this.#deferringStates = null;
}
getPauseReason(): ?string {
if (this.#deferringStates == null || this.#deferringStates.size === 0) {
return null;
}
const states = [...this.#deferringStates];
if (states.length === 1) {
return `The watch is in the '${states[0]}' state.`;
}
return `The watch is in the ${states
.slice(0, -1)
.map(s => `'${s}'`)
.join(', ')} and '${states[states.length - 1]}' states.`;
}
}
/**
* Handles an error and returns true if exists.
*/
function handleError(emitter: EventEmitter, error: ?Error) {
if (error != null) {
emitter.emit('error', error);
return true;
} else {
return false;
}
}
/**
* Handles a warning in the watchman resp object.
*/
function handleWarning(resp: $ReadOnly<{warning?: mixed, ...}>) {
if ('warning' in resp) {
if (RecrawlWarning.isRecrawlWarningDupe(resp.warning)) {
return true;
}
console.warn(resp.warning);
return true;
} else {
return false;
}
}

View File

@@ -0,0 +1,94 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.assignOptions =
exports.DELETE_EVENT =
exports.CHANGE_EVENT =
exports.ALL_EVENT =
exports.ADD_EVENT =
void 0;
exports.isIncluded = isIncluded;
exports.recReaddir = recReaddir;
exports.typeFromStat = typeFromStat;
const anymatch = require("anymatch");
const micromatch = require("micromatch");
const platform = require("os").platform();
const path = require("path");
const walker = require("walker");
const CHANGE_EVENT = "change";
exports.CHANGE_EVENT = CHANGE_EVENT;
const DELETE_EVENT = "delete";
exports.DELETE_EVENT = DELETE_EVENT;
const ADD_EVENT = "add";
exports.ADD_EVENT = ADD_EVENT;
const ALL_EVENT = "all";
exports.ALL_EVENT = ALL_EVENT;
const assignOptions = function (watcher, opts) {
watcher.globs = opts.glob ?? [];
watcher.dot = opts.dot ?? false;
watcher.ignored = opts.ignored ?? false;
watcher.watchmanDeferStates = opts.watchmanDeferStates;
if (!Array.isArray(watcher.globs)) {
watcher.globs = [watcher.globs];
}
watcher.doIgnore =
opts.ignored != null && opts.ignored !== false
? anymatch(opts.ignored)
: () => false;
if (opts.watchman == true && opts.watchmanPath != null) {
watcher.watchmanPath = opts.watchmanPath;
}
return opts;
};
exports.assignOptions = assignOptions;
function isIncluded(type, globs, dot, doIgnore, relativePath) {
if (doIgnore(relativePath)) {
return false;
}
if (globs.length === 0 || type !== "f") {
return dot || micromatch.some(relativePath, "**/*");
}
return micromatch.some(relativePath, globs, {
dot,
});
}
function recReaddir(
dir,
dirCallback,
fileCallback,
symlinkCallback,
endCallback,
errorCallback,
ignored
) {
walker(dir)
.filterDir((currentDir) => !anymatch(ignored, currentDir))
.on("dir", normalizeProxy(dirCallback))
.on("file", normalizeProxy(fileCallback))
.on("symlink", normalizeProxy(symlinkCallback))
.on("error", errorCallback)
.on("end", () => {
if (platform === "win32") {
setTimeout(endCallback, 1000);
} else {
endCallback();
}
});
}
function normalizeProxy(callback) {
return (filepath, stats) => callback(path.normalize(filepath), stats);
}
function typeFromStat(stat) {
if (stat.isSymbolicLink()) {
return "l";
}
if (stat.isDirectory()) {
return "d";
}
if (stat.isFile()) {
return "f";
}
return null;
}

View File

@@ -0,0 +1,160 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
/**
* Originally vendored from
* https://github.com/amasad/sane/blob/64ff3a870c42e84f744086884bf55a4f9c22d376/src/common.js
*/
'use strict';
import type {ChangeEventMetadata} from '../flow-types';
import type {Stats} from 'fs';
// $FlowFixMe[untyped-import] - Write libdefs for `anymatch`
const anymatch = require('anymatch');
// $FlowFixMe[untyped-import] - Write libdefs for `micromatch`
const micromatch = require('micromatch');
const platform = require('os').platform();
const path = require('path');
// $FlowFixMe[untyped-import] - Write libdefs for `walker`
const walker = require('walker');
/**
* Constants
*/
export const CHANGE_EVENT = 'change';
export const DELETE_EVENT = 'delete';
export const ADD_EVENT = 'add';
export const ALL_EVENT = 'all';
export type WatcherOptions = $ReadOnly<{
glob: $ReadOnlyArray<string>,
dot: boolean,
ignored: boolean | RegExp,
watchmanDeferStates: $ReadOnlyArray<string>,
watchman?: mixed,
watchmanPath?: string,
}>;
interface Watcher {
doIgnore: string => boolean;
dot: boolean;
globs: $ReadOnlyArray<string>;
ignored?: ?(boolean | RegExp);
watchmanDeferStates: $ReadOnlyArray<string>;
watchmanPath?: ?string;
}
/**
* Assigns options to the watcher.
*
* @param {NodeWatcher|PollWatcher|WatchmanWatcher} watcher
* @param {?object} opts
* @return {boolean}
* @public
*/
export const assignOptions = function (
watcher: Watcher,
opts: WatcherOptions,
): WatcherOptions {
watcher.globs = opts.glob ?? [];
watcher.dot = opts.dot ?? false;
watcher.ignored = opts.ignored ?? false;
watcher.watchmanDeferStates = opts.watchmanDeferStates;
if (!Array.isArray(watcher.globs)) {
watcher.globs = [watcher.globs];
}
watcher.doIgnore =
opts.ignored != null && opts.ignored !== false
? anymatch(opts.ignored)
: () => false;
if (opts.watchman == true && opts.watchmanPath != null) {
watcher.watchmanPath = opts.watchmanPath;
}
return opts;
};
/**
* Checks a file relative path against the globs array.
*/
export function isIncluded(
type: ?('f' | 'l' | 'd'),
globs: $ReadOnlyArray<string>,
dot: boolean,
doIgnore: string => boolean,
relativePath: string,
): boolean {
if (doIgnore(relativePath)) {
return false;
}
// For non-regular files or if there are no glob matchers, just respect the
// `dot` option to filter dotfiles if dot === false.
if (globs.length === 0 || type !== 'f') {
return dot || micromatch.some(relativePath, '**/*');
}
return micromatch.some(relativePath, globs, {dot});
}
/**
* Traverse a directory recursively calling `callback` on every directory.
*/
export function recReaddir(
dir: string,
dirCallback: (string, Stats) => void,
fileCallback: (string, Stats) => void,
symlinkCallback: (string, Stats) => void,
endCallback: () => void,
errorCallback: Error => void,
ignored: ?(boolean | RegExp),
) {
walker(dir)
.filterDir(currentDir => !anymatch(ignored, currentDir))
.on('dir', normalizeProxy(dirCallback))
.on('file', normalizeProxy(fileCallback))
.on('symlink', normalizeProxy(symlinkCallback))
.on('error', errorCallback)
.on('end', () => {
if (platform === 'win32') {
setTimeout(endCallback, 1000);
} else {
endCallback();
}
});
}
/**
* Returns a callback that when called will normalize a path and call the
* original callback
*/
function normalizeProxy<T>(
callback: (filepath: string, stats: Stats) => T,
): (string, Stats) => T {
return (filepath: string, stats: Stats) =>
callback(path.normalize(filepath), stats);
}
export function typeFromStat(stat: Stats): ?ChangeEventMetadata['type'] {
// Note: These tests are not mutually exclusive - a symlink passes isFile
if (stat.isSymbolicLink()) {
return 'l';
}
if (stat.isDirectory()) {
return 'd';
}
if (stat.isFile()) {
return 'f'; // "Regular" file
}
return null;
}

View File

@@ -0,0 +1,98 @@
"use strict";
const H = require("./constants");
const dependencyExtractor = require("./lib/dependencyExtractor");
const excludedExtensions = require("./workerExclusionList");
const { createHash } = require("crypto");
const { promises: fsPromises } = require("fs");
const fs = require("graceful-fs");
const path = require("path");
const PACKAGE_JSON = path.sep + "package.json";
let hasteImpl = null;
let hasteImplModulePath = null;
function getHasteImpl(requestedModulePath) {
if (hasteImpl) {
if (requestedModulePath !== hasteImplModulePath) {
throw new Error("metro-file-map: hasteImplModulePath changed");
}
return hasteImpl;
}
hasteImplModulePath = requestedModulePath;
hasteImpl = require(hasteImplModulePath);
return hasteImpl;
}
function sha1hex(content) {
return createHash("sha1").update(content).digest("hex");
}
async function worker(data) {
let content;
let dependencies;
let id;
let module;
let sha1;
let symlinkTarget;
const {
computeDependencies,
computeSha1,
enableHastePackages,
readLink,
rootDir,
filePath,
} = data;
const getContent = () => {
if (content == null) {
content = fs.readFileSync(filePath);
}
return content;
};
if (enableHastePackages && filePath.endsWith(PACKAGE_JSON)) {
try {
const fileData = JSON.parse(getContent().toString());
if (fileData.name) {
const relativeFilePath = path.relative(rootDir, filePath);
id = fileData.name;
module = [relativeFilePath, H.PACKAGE];
}
} catch (err) {
throw new Error(`Cannot parse ${filePath} as JSON: ${err.message}`);
}
} else if (
(data.hasteImplModulePath != null || computeDependencies) &&
!excludedExtensions.has(filePath.substr(filePath.lastIndexOf(".")))
) {
if (data.hasteImplModulePath != null) {
id = getHasteImpl(data.hasteImplModulePath).getHasteName(filePath);
if (id != null) {
const relativeFilePath = path.relative(rootDir, filePath);
module = [relativeFilePath, H.MODULE];
}
}
if (computeDependencies) {
dependencies = Array.from(
data.dependencyExtractor != null
? require(data.dependencyExtractor).extract(
getContent().toString(),
filePath,
dependencyExtractor.extract
)
: dependencyExtractor.extract(getContent().toString())
);
}
}
if (computeSha1) {
sha1 = sha1hex(getContent());
}
if (readLink) {
symlinkTarget = await fsPromises.readlink(filePath);
}
return {
dependencies,
id,
module,
sha1,
symlinkTarget,
};
}
module.exports = {
worker,
};

View File

@@ -0,0 +1,130 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
/*::
import type {WorkerMessage, WorkerMetadata} from './flow-types';
*/
'use strict';
const H = require('./constants');
const dependencyExtractor = require('./lib/dependencyExtractor');
const excludedExtensions = require('./workerExclusionList');
const {createHash} = require('crypto');
const {promises: fsPromises} = require('fs');
const fs = require('graceful-fs');
const path = require('path');
const PACKAGE_JSON = path.sep + 'package.json';
let hasteImpl /*: ?{getHasteName: string => ?string} */ = null;
let hasteImplModulePath /*: ?string */ = null;
function getHasteImpl(
requestedModulePath /*: string */,
) /*: {getHasteName: string => ?string} */ {
if (hasteImpl) {
if (requestedModulePath !== hasteImplModulePath) {
throw new Error('metro-file-map: hasteImplModulePath changed');
}
return hasteImpl;
}
hasteImplModulePath = requestedModulePath;
// $FlowFixMe[unsupported-syntax] - dynamic require
hasteImpl = require(hasteImplModulePath);
return hasteImpl;
}
function sha1hex(content /*: string | Buffer */) /*: string */ {
return createHash('sha1').update(content).digest('hex');
}
async function worker(
data /*: WorkerMessage */,
) /*: Promise<WorkerMetadata> */ {
let content /*: ?Buffer */;
let dependencies /*: WorkerMetadata['dependencies'] */;
let id /*: WorkerMetadata['id'] */;
let module /*: WorkerMetadata['module'] */;
let sha1 /*: WorkerMetadata['sha1'] */;
let symlinkTarget /*: WorkerMetadata['symlinkTarget'] */;
const {
computeDependencies,
computeSha1,
enableHastePackages,
readLink,
rootDir,
filePath,
} = data;
const getContent = () /*: Buffer */ => {
if (content == null) {
content = fs.readFileSync(filePath);
}
return content;
};
if (enableHastePackages && filePath.endsWith(PACKAGE_JSON)) {
// Process a package.json that is returned as a PACKAGE type with its name.
try {
const fileData = JSON.parse(getContent().toString());
if (fileData.name) {
const relativeFilePath = path.relative(rootDir, filePath);
id = fileData.name;
module = [relativeFilePath, H.PACKAGE];
}
} catch (err) {
throw new Error(`Cannot parse ${filePath} as JSON: ${err.message}`);
}
} else if (
(data.hasteImplModulePath != null || computeDependencies) &&
!excludedExtensions.has(filePath.substr(filePath.lastIndexOf('.')))
) {
// Process a random file that is returned as a MODULE.
if (data.hasteImplModulePath != null) {
id = getHasteImpl(data.hasteImplModulePath).getHasteName(filePath);
if (id != null) {
const relativeFilePath = path.relative(rootDir, filePath);
module = [relativeFilePath, H.MODULE];
}
}
if (computeDependencies) {
dependencies = Array.from(
data.dependencyExtractor != null
? // $FlowFixMe[unsupported-syntax] - dynamic require
require(data.dependencyExtractor).extract(
getContent().toString(),
filePath,
dependencyExtractor.extract,
)
: dependencyExtractor.extract(getContent().toString()),
);
}
}
// If a SHA-1 is requested on update, compute it.
if (computeSha1) {
sha1 = sha1hex(getContent());
}
if (readLink) {
symlinkTarget = await fsPromises.readlink(filePath);
}
return {dependencies, id, module, sha1, symlinkTarget};
}
module.exports = {
worker,
};

View File

@@ -0,0 +1,37 @@
"use strict";
const extensions = new Set([
".json",
".bmp",
".gif",
".ico",
".jpeg",
".jpg",
".png",
".svg",
".tiff",
".tif",
".webp",
".avi",
".mp4",
".mpeg",
".mpg",
".ogv",
".webm",
".3gp",
".3g2",
".aac",
".midi",
".mid",
".mp3",
".oga",
".wav",
".3gp",
".3g2",
".eot",
".otf",
".ttf",
".woff",
".woff2",
]);
module.exports = extensions;

View File

@@ -0,0 +1,67 @@
/**
* 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
* @flow strict
*/
// This list is compiled after the MDN list of the most common MIME types (see
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/
// Complete_list_of_MIME_types).
//
// Only MIME types starting with "image/", "video/", "audio/" and "font/" are
// reflected in the list. Adding "application/" is too risky since some text
// file formats (like ".js" and ".json") have an "application/" MIME type.
//
// Feel free to add any extensions that cannot be a Haste module.
'use strict';
const extensions /*: $ReadOnlySet<string> */ = new Set([
// JSONs are never haste modules, except for "package.json", which is handled.
'.json',
// Image extensions.
'.bmp',
'.gif',
'.ico',
'.jpeg',
'.jpg',
'.png',
'.svg',
'.tiff',
'.tif',
'.webp',
// Video extensions.
'.avi',
'.mp4',
'.mpeg',
'.mpg',
'.ogv',
'.webm',
'.3gp',
'.3g2',
// Audio extensions.
'.aac',
'.midi',
'.mid',
'.mp3',
'.oga',
'.wav',
'.3gp',
'.3g2',
// Font extensions.
'.eot',
'.otf',
'.ttf',
'.woff',
'.woff2',
]);
module.exports = extensions;