Skip to content

metro-file-map: Restore change events for dotfiles like .env - #2024

Merged
robhogan merged 1 commit into
mainfrom
pr2023
Oct 5, 2026
Merged

robhogan merged 1 commit into
mainfrom
pr2023

Conversation

@robhogan

@robhogan robhogan commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

#2000 replaced the watchers' globs with included, comparing extensions using path.extname. That isn't quite what the **/*.<ext> globs did with dot: true - path.extname('.env') is '', so .env no longer matches an env extension, and a multi-part extension like d.ts never matches.

This was picked up by Expo CI, which adds env to watcher.additionalExts so that editing .env triggers a reload.

This matches an extension when the basename ends with . followed by that extension, after any dot, which is what the globs matched.

Changelog: Internal (#2000 is unreleased)

Test plan:
Extended common-test.js with dotfile and multi-part extension cases.

Compared against micromatch.some(path, globs, {dot: true}) with the globs Metro used to build, on 21 edge-case paths (.env, .env.local, .envrc, src/.js, a.d.ts, a.js.map, a..js, ...), with no mismatches.

Found by Expo's hmr-env-vars E2E test failing on the port of #2000 to Expo's file map, that PR now includes this fix and CI passes: expo/expo#51093

#2000 replaced the watchers' globs with `included`, comparing extensions using `path.extname`. That isn't quite what the `**/*.<ext>` globs did with `dot: true` - `path.extname('.env')` is `''`, so `.env` no longer matches an `env` extension, and a multi-part extension like `d.ts` never matches.

That's a regression for Expo, which adds `env` to `watcher.additionalExts` so that editing `.env` triggers a reload:

https://github.com/expo/expo/blob/7c9b0878c1942d24552adfab7de396c9e4a69103/packages/@expo/metro-config/src/ExpoMetroConfig.ts#L345

This matches an extension when the basename ends with `.` followed by that extension, after any dot, which is what the globs matched.

Changelog: Internal (#2000 is unreleased)

Test plan:
Extended `common-test.js` with dotfile and multi-part extension cases.

Compared against `micromatch.some(path, globs, {dot: true})` with the globs Metro used to build, on 21 edge-case paths (`.env`, `.env.local`, `.envrc`, `src/.js`, `a.d.ts`, `a.js.map`, `a..js`, ...), with no mismatches.

Found by Expo's `hmr-env-vars` E2E test failing on the port of #2000 to Expo's file map, expo/expo#51093, which carries the same fix.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 5, 2026
@robhogan
robhogan requested review from huntie and vzaidman October 5, 2026 13:43

@huntie huntie left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❗

@robhogan
robhogan merged commit 3865c00 into main Oct 5, 2026
16 checks passed
robhogan added a commit to expo/expo that referenced this pull request Oct 5, 2026
# Why

Port of react/metro#2000 +
react/metro#2024 to `@expo/metro-file-map`.

`micromatch` depends on `braces`, which has a CVE
([GHSA-vfj7-8cjw-p6xm](GHSA-vfj7-8cjw-p6xm)).
The fork only uses `micromatch` to filter watcher change events, against
globs it builds itself from `extensions`, `package.json` and the health
check file prefix. None of them are configurable, so all it ever checks
is an extension, a file name or a prefix.

# How

This replaces the watchers' `globs` option with `included`, a set of
`extensions`, `basenames` and `basenamePrefixes` checked with `Set`
lookups and `startsWith`. A file matches an extension when its basename
ends with `.` followed by that extension, so `.env` matches `env` and
`foo.d.ts` matches `d.ts`.

It also removes the watchers' `dot` option, which was always `true`, and
the unused `Glob` type. With that, `micromatch` and `@types/micromatch`
are no longer dependencies of `@expo/metro-file-map`.

One difference from upstream: the fork's `healthCheckFilePrefix` is
nullable, so a null prefix now adds no prefix match rather than a
literal `**/null*` glob.

# Test Plan

New unit test for `isIncluded`, ported from upstream. `pnpm test`, `pnpm
typecheck` and `pnpm lint` pass in `packages/@expo/metro-file-map`. The
CLI E2E suite exercises the watchers through the injected file map.

# Checklist

- [x] I added a changeset and followed [this short
guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting)
- [ ] This diff will work correctly for `npx expo prebuild` & EAS Build
(eg: updated a module plugin).
- [ ] Conforms with the [Documentation Writing Style
Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md)
robhogan added a commit to expo/expo that referenced this pull request Oct 5, 2026
# Why

Cherry-pick of #51093 onto `sdk-58`. It ports react/metro#2000 and its follow-up react/metro#2024 to `@expo/metro-file-map`.

`micromatch` depends on `braces`, which has a CVE ([GHSA-vfj7-8cjw-p6xm](GHSA-vfj7-8cjw-p6xm)). The fork only uses `micromatch` to filter watcher change events, against globs it builds itself from `extensions`, `package.json` and the health check file prefix. None of them are configurable, so all it ever checks is an extension, a file name or a prefix.

# How

The same change as on `main`. The watchers' `globs` option is replaced by `included`, a set of `extensions`, `basenames` and `basenamePrefixes` checked with `Set` lookups and `startsWith`. A file matches an extension when its basename ends with `.` followed by that extension, as the `**/*.<ext>` globs did with `dot: true`. This keeps `.env` matching the `env` extension that `@expo/metro-config` adds to `watcher.additionalExts`, which `path.extname` would miss. The watchers' `dot` option, which was always `true`, and the unused `Glob` type are removed, and with them the `micromatch` and `@types/micromatch` dependencies.

One difference from upstream: the fork's `healthCheckFilePrefix` is nullable, so a null prefix now adds no prefix match rather than a literal `**/null*` glob.

The merged commit from #51093 (52acc2c) cherry-picked cleanly. The `.changeset` file is replaced by a `CHANGELOG.md` entry, because `sdk-58` does not use changesets.

# Test Plan

New unit test for `isIncluded`, ported from upstream and extended with dotfile and multi-part extension cases. A one-off script checked `isIncluded` against `micromatch.some(path, globs, {dot: true})` on 21 edge cases (`.env`, `.env.local`, `.envrc`, `src/.js`, `a.d.ts`, `a.js.map`, `a..js`, …) with no mismatches. On this branch, `pnpm test` (725 tests), `pnpm typecheck` and `pnpm lint` pass in `packages/@expo/metro-file-map`, and `pnpm install --frozen-lockfile` accepts the lockfile. The CLI E2E suite exercises the watchers through the injected file map, including `.env` reloads in `hmr-env-vars.test.ts`.

# Checklist

- [x] I added a `CHANGELOG.md` entry.
- [ ] Docs: not applicable.

(cherry picked from commit 52acc2c)
robhogan added a commit to expo/expo that referenced this pull request Oct 5, 2026
# Why

Cherry-pick of #51093 onto `sdk-58`. It ports react/metro#2000 and its follow-up react/metro#2024 to `@expo/metro-file-map`.

`micromatch` depends on `braces`, which has a CVE ([GHSA-vfj7-8cjw-p6xm](GHSA-vfj7-8cjw-p6xm)). The fork only uses `micromatch` to filter watcher change events, against globs it builds itself from `extensions`, `package.json` and the health check file prefix. None of them are configurable, so all it ever checks is an extension, a file name or a prefix.

# How

The same change as on `main`. The watchers' `globs` option is replaced by `included`, a set of `extensions`, `basenames` and `basenamePrefixes` checked with `Set` lookups and `startsWith`. A file matches an extension when its basename ends with `.` followed by that extension, as the `**/*.<ext>` globs did with `dot: true`. This keeps `.env` matching the `env` extension that `@expo/metro-config` adds to `watcher.additionalExts`, which `path.extname` would miss. The watchers' `dot` option, which was always `true`, and the unused `Glob` type are removed, and with them the `micromatch` and `@types/micromatch` dependencies.

One difference from upstream: the fork's `healthCheckFilePrefix` is nullable, so a null prefix now adds no prefix match rather than a literal `**/null*` glob.

The merged commit from #51093 (52acc2c) cherry-picked cleanly. The `.changeset` file is replaced by a `CHANGELOG.md` entry, because `sdk-58` does not use changesets.

# Test Plan

New unit test for `isIncluded`, ported from upstream and extended with dotfile and multi-part extension cases. A one-off script checked `isIncluded` against `micromatch.some(path, globs, {dot: true})` on 21 edge cases (`.env`, `.env.local`, `.envrc`, `src/.js`, `a.d.ts`, `a.js.map`, `a..js`, …) with no mismatches. On this branch, `pnpm test` (725 tests), `pnpm typecheck` and `pnpm lint` pass in `packages/@expo/metro-file-map`, and `pnpm install --frozen-lockfile` accepts the lockfile. The CLI E2E suite exercises the watchers through the injected file map, including `.env` reloads in `hmr-env-vars.test.ts`.

# Checklist

- [x] I added a `CHANGELOG.md` entry.
- [ ] Docs: not applicable.

(cherry picked from commit 52acc2c)
robhogan added a commit to expo/expo that referenced this pull request Oct 5, 2026
# Why

Cherry-pick of #51093 onto `sdk-58`. It ports react/metro#2000 and its follow-up react/metro#2024 to `@expo/metro-file-map`.

`micromatch` depends on `braces`, which has a CVE ([GHSA-vfj7-8cjw-p6xm](GHSA-vfj7-8cjw-p6xm)). The fork only uses `micromatch` to filter watcher change events, against globs it builds itself from `extensions`, `package.json` and the health check file prefix. None of them are configurable, so all it ever checks is an extension, a file name or a prefix.

# How

The same change as on `main`. The watchers' `globs` option is replaced by `included`, a set of `extensions`, `basenames` and `basenamePrefixes` checked with `Set` lookups and `startsWith`. A file matches an extension when its basename ends with `.` followed by that extension, as the `**/*.<ext>` globs did with `dot: true`. This keeps `.env` matching the `env` extension that `@expo/metro-config` adds to `watcher.additionalExts`, which `path.extname` would miss. The watchers' `dot` option, which was always `true`, and the unused `Glob` type are removed, and with them the `micromatch` and `@types/micromatch` dependencies.

One difference from upstream: the fork's `healthCheckFilePrefix` is nullable, so a null prefix now adds no prefix match rather than a literal `**/null*` glob.

The merged commit from #51093 (52acc2c) cherry-picked cleanly. The `.changeset` file is replaced by a `CHANGELOG.md` entry, because `sdk-58` does not use changesets.

# Test Plan

New unit test for `isIncluded`, ported from upstream and extended with dotfile and multi-part extension cases. A one-off script checked `isIncluded` against `micromatch.some(path, globs, {dot: true})` on 21 edge cases (`.env`, `.env.local`, `.envrc`, `src/.js`, `a.d.ts`, `a.js.map`, `a..js`, …) with no mismatches. On this branch, `pnpm test` (725 tests), `pnpm typecheck` and `pnpm lint` pass in `packages/@expo/metro-file-map`, and `pnpm install --frozen-lockfile` accepts the lockfile. The CLI E2E suite exercises the watchers through the injected file map, including `.env` reloads in `hmr-env-vars.test.ts`.

# Checklist

- [x] I added a `CHANGELOG.md` entry.
- [ ] Docs: not applicable.

(cherry picked from commit 52acc2c)
robhogan added a commit to expo/expo that referenced this pull request Oct 5, 2026
# Why

Cherry-pick of #51093 onto `sdk-57`. It ports react/metro#2000 and its follow-up react/metro#2024 to `@expo/metro-file-map`.

`micromatch` depends on `braces`, which has a CVE ([GHSA-vfj7-8cjw-p6xm](GHSA-vfj7-8cjw-p6xm)). The fork only uses `micromatch` to filter watcher change events, against globs it builds itself from `extensions`, `package.json` and the health check file prefix. None of them are configurable, so all it ever checks is an extension, a file name or a prefix.

# How

The same change as on `main`. The watchers' `globs` option is replaced by `included`, a set of `extensions`, `basenames` and `basenamePrefixes` checked with `Set` lookups and `startsWith`. A file matches an extension when its basename ends with `.` followed by that extension, as the `**/*.<ext>` globs did with `dot: true`. This keeps `.env` matching the `env` extension that `@expo/metro-config` adds to `watcher.additionalExts`, which `path.extname` would miss. The watchers' `dot` option, which was always `true`, and the unused `Glob` type are removed, and with them the `micromatch` and `@types/micromatch` dependencies.

One difference from upstream: the fork's `healthCheckFilePrefix` is nullable, so a null prefix now adds no prefix match rather than a literal `**/null*` glob.

The merged commit from #51093 (52acc2c) cherry-picked cleanly. The `.changeset` file is replaced by a `CHANGELOG.md` entry, because `sdk-57` does not use changesets.

# Test Plan

New unit test for `isIncluded`, ported from upstream and extended with dotfile and multi-part extension cases. A one-off script checked `isIncluded` against `micromatch.some(path, globs, {dot: true})` on 21 edge cases (`.env`, `.env.local`, `.envrc`, `src/.js`, `a.d.ts`, `a.js.map`, `a..js`, …) with no mismatches. On this branch, `pnpm test` (725 tests), `pnpm typecheck` and `pnpm lint` pass in `packages/@expo/metro-file-map`, and `pnpm install --frozen-lockfile` accepts the lockfile. The CLI E2E suite exercises the watchers through the injected file map, including `.env` reloads in `hmr-env-vars.test.ts`.

# Checklist

- [x] I added a `CHANGELOG.md` entry.
- [ ] Docs: not applicable.

(cherry picked from commit 52acc2c)
robhogan added a commit to expo/expo that referenced this pull request Oct 5, 2026
# Why

Cherry-pick of #51093 onto `sdk-56`. It ports react/metro#2000 and its follow-up react/metro#2024 to `@expo/metro-file-map`.

`micromatch` depends on `braces`, which has a CVE ([GHSA-vfj7-8cjw-p6xm](GHSA-vfj7-8cjw-p6xm)). The fork only uses `micromatch` to filter watcher change events, against globs it builds itself from `extensions`, `package.json` and the health check file prefix. None of them are configurable, so all it ever checks is an extension, a file name or a prefix.

# How

The same change as on `main`. The watchers' `globs` option is replaced by `included`, a set of `extensions`, `basenames` and `basenamePrefixes` checked with `Set` lookups and `startsWith`. A file matches an extension when its basename ends with `.` followed by that extension, as the `**/*.<ext>` globs did with `dot: true`. This keeps `.env` matching the `env` extension that `@expo/metro-config` adds to `watcher.additionalExts`, which `path.extname` would miss. The watchers' `dot` option, which was always `true`, and the unused `Glob` type are removed, and with them the `micromatch` and `@types/micromatch` dependencies.

One difference from upstream: the fork's `healthCheckFilePrefix` is nullable, so a null prefix now adds no prefix match rather than a literal `**/null*` glob.

The cherry-pick of the merged commit from #51093 (52acc2c) had one conflict. `FallbackWatcher.test.ts` doesn't exist on `sdk-56`, and the pick only updated the watcher options it passes, so I resolved it by keeping the file deleted. The `.changeset` file is replaced by a `CHANGELOG.md` entry, because `sdk-56` does not use changesets.

# Test Plan

New unit test for `isIncluded`, ported from upstream and extended with dotfile and multi-part extension cases. A one-off script checked `isIncluded` against `micromatch.some(path, globs, {dot: true})` on 21 edge cases (`.env`, `.env.local`, `.envrc`, `src/.js`, `a.d.ts`, `a.js.map`, `a..js`, …) with no mismatches. On this branch, `pnpm test` (720 tests), `pnpm typecheck` and `pnpm lint` pass in `packages/@expo/metro-file-map`, and `pnpm install --frozen-lockfile` with pnpm 10 (as CI uses on this branch) accepts the lockfile. The CLI E2E suite exercises the watchers through the injected file map, including `.env` reloads in `hmr-env-vars.test.ts`.

# Checklist

- [x] I added a `CHANGELOG.md` entry.
- [ ] Docs: not applicable.

(cherry picked from commit 52acc2c)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants