@rm30/metro-config 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # @rm30/metro-config
2
+
3
+ The shared Metro configuration for RM30's Expo apps.
4
+
5
+ ```sh
6
+ pnpm add -D @rm30/metro-config
7
+ ```
8
+
9
+ ```js
10
+ // apps/mobile/metro.config.cjs
11
+ const { getDefaultConfig } = require('expo/metro-config')
12
+ const { withRm30Metro } = require('@rm30/metro-config')
13
+
14
+ module.exports = withRm30Metro(getDefaultConfig(__dirname), {
15
+ workspaceAliases: { '@myproject/types': '../../packages/types/src' },
16
+ uniwind: { cssEntryFile: './global.css', dtsFile: './src/uniwind-types.d.ts' },
17
+ })
18
+ ```
19
+
20
+ ## Why the base config is passed in
21
+
22
+ The projects do not agree on how the base config is made, and both ways are right:
23
+ most call `getDefaultConfig` from `expo/metro-config`, while superwallet calls
24
+ `getSentryExpoConfig` so Sentry can attach to the bundle. Creating the config inside this
25
+ package would take that choice away, so it takes one instead.
26
+
27
+ ```js
28
+ const { getSentryExpoConfig } = require('@sentry/react-native/metro')
29
+ module.exports = withRm30Metro(getSentryExpoConfig(__dirname), { /* … */ })
30
+ ```
31
+
32
+ ## Options
33
+
34
+ | Option | Default | |
35
+ |---|---|---|
36
+ | `projectRoot` | `config.projectRoot` | Throws if neither is set, rather than silently building paths from `undefined`. |
37
+ | `workspaceAliases` | `{}` | Package name to path, absolute or relative to `projectRoot`. |
38
+ | `blockList` | `[]` | Extra patterns, **added** to the built-in test-file ones. |
39
+ | `platforms` | `ios, android, native, web` | |
40
+ | `uniwind` | omitted | `{ cssEntryFile, dtsFile }`, or omitted/`false` to skip the wrapper. |
41
+
42
+ ## What it does for you
43
+
44
+ - **Blocks test files from the bundle.** All three apps had their own copy of the same two
45
+ regexes.
46
+ - **Adds every alias target to `watchFolders`.** Metro watches only the project root by
47
+ default, so a symlinked workspace package outside it is read once and never again —
48
+ edits to a shared package appear to do nothing until the bundler is restarted. This is
49
+ the single easiest thing to forget, which is why it is automatic here.
50
+ - **Applies Uniwind outermost.** It has to be the last wrapper, and doing it by hand is
51
+ how it ends up in the wrong place.
52
+
53
+ `watchFolders` are merged and de-duplicated, so passing an alias that is already watched
54
+ is harmless.
55
+
56
+ ## Project-specific blocks stay in the project
57
+
58
+ superpool blocks Hardhat's build output:
59
+
60
+ ```js
61
+ blockList: [/[/\\]packages[/\\]contracts[/\\](cache|artifacts|coverage|typechain-types)[/\\].*/]
62
+ ```
63
+
64
+ That is not shared policy — it exists because Metro watches the whole monorepo while
65
+ Hardhat creates and deletes lock files under `packages/contracts/cache` during compilation,
66
+ and a file vanishing mid-watch kills the watcher with `ENOENT`, taking the dev server down
67
+ whenever contracts are compiled. Real variance, and it belongs where the reason lives.
package/index.cjs ADDED
@@ -0,0 +1,72 @@
1
+ const path = require('node:path')
2
+
3
+ /**
4
+ * Test files are never bundled. Every RM30 mobile app blocked these already,
5
+ * each with its own copy of the same two regexes.
6
+ */
7
+ const TEST_FILE_PATTERNS = [/.*\.test\.(js|jsx|ts|tsx)$/, /.*\.spec\.(js|jsx|ts|tsx)$/]
8
+
9
+ const DEFAULT_PLATFORMS = ['ios', 'android', 'native', 'web']
10
+
11
+ /**
12
+ * Apply the shared RM30 Metro configuration.
13
+ *
14
+ * The base config is passed in rather than created here, because the projects do not
15
+ * agree on how it is made: most call `getDefaultConfig` from `expo/metro-config`, and
16
+ * superwallet calls `getSentryExpoConfig` so Sentry can attach. Both are valid, and
17
+ * wrapping either would take that choice away.
18
+ *
19
+ * @param {object} config A Metro config from expo/metro-config or @sentry/react-native.
20
+ * @param {object} [options]
21
+ * @param {string} [options.projectRoot] Defaults to `config.projectRoot`.
22
+ * @param {Record<string,string>} [options.workspaceAliases]
23
+ * Package name to path, absolute or relative to `projectRoot`. Each alias target is
24
+ * also added to `watchFolders`, so Metro reloads when the shared package changes —
25
+ * forgetting that is the usual reason edits to a workspace package appear to do
26
+ * nothing.
27
+ * @param {RegExp[]} [options.blockList] Extra patterns, added to the test-file ones.
28
+ * @param {string[]} [options.platforms]
29
+ * @param {{cssEntryFile: string, dtsFile: string}|false} [options.uniwind]
30
+ * Uniwind options, or false to skip the wrapper. Uniwind must be outermost, so this
31
+ * function applies it last.
32
+ * @returns {object} The configured Metro config.
33
+ */
34
+ function withRm30Metro(config, options = {}) {
35
+ const { projectRoot = config.projectRoot, workspaceAliases = {}, blockList = [], platforms = DEFAULT_PLATFORMS, uniwind } = options
36
+
37
+ if (!projectRoot) {
38
+ throw new Error('@rm30/metro-config: projectRoot is required when config.projectRoot is unset')
39
+ }
40
+
41
+ const resolved = Object.fromEntries(
42
+ Object.entries(workspaceAliases).map(([name, target]) => [name, path.isAbsolute(target) ? target : path.resolve(projectRoot, target)])
43
+ )
44
+
45
+ config.resolver = config.resolver ?? {}
46
+ config.resolver.alias = { ...config.resolver.alias, ...resolved }
47
+ config.resolver.blockList = [...TEST_FILE_PATTERNS, ...blockList]
48
+ config.resolver.platforms = platforms
49
+
50
+ // Metro only watches the project root by default; a symlinked workspace package
51
+ // outside it is read once and never again.
52
+ const watched = new Set([...(config.watchFolders ?? []), ...Object.values(resolved)])
53
+ config.watchFolders = [...watched]
54
+
55
+ if (uniwind === false || uniwind === undefined) return config
56
+
57
+ // Resolved from the consuming project, not from here. A bare require would look in
58
+ // this package's own directory, and under pnpm's symlinked store an optional peer is
59
+ // not present there — so it resolves only if something happened to hoist it.
60
+ let withUniwindConfig
61
+ try {
62
+ withUniwindConfig = require(require.resolve('uniwind/metro', { paths: [projectRoot] })).withUniwindConfig
63
+ } catch (cause) {
64
+ throw new Error(
65
+ `@rm30/metro-config: uniwind options were given but 'uniwind/metro' could not be resolved from ${projectRoot}. Install uniwind in that project, or pass uniwind: false.`,
66
+ { cause }
67
+ )
68
+ }
69
+ return withUniwindConfig(config, uniwind)
70
+ }
71
+
72
+ module.exports = { withRm30Metro, TEST_FILE_PATTERNS, DEFAULT_PLATFORMS }
package/index.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ export interface UniwindOptions {
2
+ cssEntryFile: string
3
+ dtsFile: string
4
+ }
5
+
6
+ export interface Rm30MetroOptions {
7
+ /** Defaults to `config.projectRoot`. */
8
+ projectRoot?: string
9
+ /** Package name to path, absolute or relative to `projectRoot`. Also watched. */
10
+ workspaceAliases?: Record<string, string>
11
+ /** Extra block patterns, added to the built-in test-file ones. */
12
+ blockList?: RegExp[]
13
+ /** Defaults to ios, android, native, web. */
14
+ platforms?: string[]
15
+ /** Uniwind options, or false/omitted to skip the wrapper. */
16
+ uniwind?: UniwindOptions | false
17
+ }
18
+
19
+ /** Metro's config object is untyped upstream; this mirrors that rather than inventing a shape. */
20
+ // biome-ignore lint/suspicious/noExplicitAny: Metro does not ship a config type
21
+ export type MetroConfig = any
22
+
23
+ export declare function withRm30Metro(config: MetroConfig, options?: Rm30MetroOptions): MetroConfig
24
+
25
+ export declare const TEST_FILE_PATTERNS: RegExp[]
26
+ export declare const DEFAULT_PLATFORMS: string[]
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@rm30/metro-config",
3
+ "version": "0.1.0",
4
+ "description": "Shared RM30 Metro configuration for Expo monorepos.",
5
+ "license": "MIT",
6
+ "author": "Rafael Miziara",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/rafamiziara/cli.git",
10
+ "directory": "packages/metro-config"
11
+ },
12
+ "keywords": ["metro", "expo", "react-native", "uniwind"],
13
+ "main": "./index.cjs",
14
+ "types": "./index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./index.d.ts",
18
+ "default": "./index.cjs"
19
+ }
20
+ },
21
+ "files": ["index.cjs", "index.d.ts", "README.md"],
22
+ "peerDependencies": {
23
+ "uniwind": ">=1.9.0"
24
+ },
25
+ "peerDependenciesMeta": {
26
+ "uniwind": {
27
+ "optional": true
28
+ }
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ }
33
+ }